From 615017a31e49cfbe542d11c49428822f46ae1232 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 8 May 2024 14:28:21 -0600 Subject: [PATCH 0001/1230] Add an output-base-name option to the Sphinx plot directive This allows specifying the output base name of the generated image files. The name can include '{counter}', which is automatically string formatted to an incrementing counter. The default if it is not specified is left intact as the current behavior, which is to use the base name of the provided script or the RST document. This is required to use the plot directive with MyST, because the directive is broken with MyST (an issue I don't want to fix), requiring the use of eval-rst. But the way eval-rst works, the incrementing counter is not maintained across different eval-rst directives, meaning if you try to include multiple of them in the same document, the images will overwrite each other. This allows you to manually work around this with something like ```{eval-rst} .. plot:: :output-base-name: plot-1 ... ``` ```{eval-rst} .. plot:: :output-base-name: plot-2 ... ``` Aside from this, it's generally useful to be able to specify the image name used for a plot, as a more informative name can be used rather than just '-1.png'. --- lib/matplotlib/sphinxext/plot_directive.py | 26 ++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 65b25fb913a5..249979c942ad 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -47,6 +47,15 @@ The ``.. plot::`` directive supports the following options: +``:output-base-name:`` : str + The base name (without the extension) of the outputted image files. The + default is to use the same name as the input script, or the name of + the RST document if no script is provided. The string can include the + format ``{counter}`` to use an incremented counter. For example, + ``'plot-{counter}'`` will create files like ``plot-1.png``, ``plot-2.png``, + and so on. If the ``{counter}`` is not provided, two plots with the same + output-base-name may overwrite each other. + ``:format:`` : {'python', 'doctest'} The format of the input. If unset, the format is auto-detected. @@ -88,6 +97,10 @@ The plot directive has the following configuration options: +plot_output_base_name + Default value for the output-base-name option (default is to use the name + of the input script, or the name of the RST file if no script is provided) + plot_include_source Default value for the include-source option (default: False). @@ -265,6 +278,7 @@ class PlotDirective(Directive): 'scale': directives.nonnegative_int, 'align': Image.align, 'class': directives.class_option, + 'output-base-name': directives.unchanged, 'include-source': _option_boolean, 'show-source-link': _option_boolean, 'format': _option_format, @@ -299,6 +313,7 @@ def setup(app): app.add_config_value('plot_pre_code', None, True) app.add_config_value('plot_include_source', False, True) app.add_config_value('plot_html_show_source_link', True, True) + app.add_config_value('plot_output_base_name', None, True) app.add_config_value('plot_formats', ['png', 'hires.png', 'pdf'], True) app.add_config_value('plot_basedir', None, True) app.add_config_value('plot_html_show_formats', True, True) @@ -734,6 +749,7 @@ def run(arguments, content, options, state_machine, state, lineno): options.setdefault('include-source', config.plot_include_source) options.setdefault('show-source-link', config.plot_html_show_source_link) + options.setdefault('output-base-name', config.plot_output_base_name) if 'class' in options: # classes are parsed into a list of string, and output by simply @@ -775,14 +791,20 @@ def run(arguments, content, options, state_machine, state, lineno): function_name = None code = Path(source_file_name).read_text(encoding='utf-8') - output_base = os.path.basename(source_file_name) + if options['output-base-name']: + output_base = options['output-base-name'] + else: + output_base = os.path.basename(source_file_name) else: source_file_name = rst_file code = textwrap.dedent("\n".join(map(str, content))) counter = document.attributes.get('_plot_counter', 0) + 1 document.attributes['_plot_counter'] = counter base, ext = os.path.splitext(os.path.basename(source_file_name)) - output_base = '%s-%d.py' % (base, counter) + if options['output-base-name']: + output_base = options['output-base-name'].format(counter=counter) + else: + output_base = '%s-%d.py' % (base, counter) function_name = None caption = options.get('caption', '') From b4329626dde5665918c4b42fe6e9669d68aa4c73 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 8 May 2024 23:15:19 -0600 Subject: [PATCH 0002/1230] Add tests for output-base-name --- lib/matplotlib/tests/test_sphinxext.py | 3 +++ lib/matplotlib/tests/tinypages/some_plots.rst | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 6624e3b17ba5..29d6f3168621 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -97,6 +97,9 @@ def plot_directive_file(num): assert filecmp.cmp(range_6, plot_file(17)) # plot 22 is from the range6.py file again, but a different function assert filecmp.cmp(range_10, img_dir / 'range6_range10.png') + # plots 23 and 24 use a custom base name with {counter} + assert filecmp.cmp(range_4, img_dir / 'custom-base-name-18.png') + assert filecmp.cmp(range_6, img_dir / 'custom-base-name-19.png') # Modify the included plot contents = (tmp_path / 'included_plot_21.rst').read_bytes() diff --git a/lib/matplotlib/tests/tinypages/some_plots.rst b/lib/matplotlib/tests/tinypages/some_plots.rst index dd1f79892b0e..b484d705ae1c 100644 --- a/lib/matplotlib/tests/tinypages/some_plots.rst +++ b/lib/matplotlib/tests/tinypages/some_plots.rst @@ -174,3 +174,15 @@ Plot 21 is generated via an include directive: Plot 22 uses a different specific function in a file with plot commands: .. plot:: range6.py range10 + +Plots 23 and 24 use output-base-name with a {counter}. + +.. plot:: + :output-base-name: custom-base-name-{counter} + + plt.plot(range(4)) + +.. plot:: + :output-base-name: custom-base-name-{counter} + + plt.plot(range(6)) From f7b56892cc3a8fc180de24cc6f418635ed9d2aec Mon Sep 17 00:00:00 2001 From: Kaustbh Date: Mon, 20 May 2024 02:00:14 +0530 Subject: [PATCH 0003/1230] Fix PolygonSelector cursor to temporarily hide during active zoom/pan --- lib/matplotlib/widgets.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index eaa35e25440b..e9f5c6f9eea8 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -4000,11 +4000,18 @@ def onmove(self, event): # needs to process the move callback even if there is no button press. # _SelectorWidget.onmove include logic to ignore move event if # _eventpress is None. - if not self.ignore(event): + + # Hide the cursor when interactive zoom/pan is active + if self.ignore(event): + if not self.canvas.widgetlock.available(self) and self._xys: + self._xys[-1] = (np.nan, np.nan) + self._draw_polygon() + return False + + else: event = self._clean_event(event) self._onmove(event) return True - return False def _onmove(self, event): """Cursor move event handler.""" From 255875d1b742bc282770956ca86591f1c4b7d516 Mon Sep 17 00:00:00 2001 From: vittoboa Date: Tue, 21 May 2024 20:37:43 +0200 Subject: [PATCH 0004/1230] Fix draggable legend disappearing when picking while use_blit=True --- lib/matplotlib/offsetbox.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 32c5bafcde1d..cc0121e0a030 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -1486,11 +1486,13 @@ def on_motion(self, evt): self.canvas.draw() def on_pick(self, evt): - if self._check_still_parented() and evt.artist == self.ref_artist: - self.mouse_x = evt.mouseevent.x - self.mouse_y = evt.mouseevent.y - self.got_artist = True - if self._use_blit: + if self._check_still_parented(): + if evt.artist == self.ref_artist: + self.mouse_x = evt.mouseevent.x + self.mouse_y = evt.mouseevent.y + self.save_offset() + self.got_artist = True + if self.got_artist and self._use_blit: self.ref_artist.set_animated(True) self.canvas.draw() self.background = \ @@ -1498,7 +1500,6 @@ def on_pick(self, evt): self.ref_artist.draw( self.ref_artist.figure._get_renderer()) self.canvas.blit() - self.save_offset() def on_release(self, event): if self._check_still_parented() and self.got_artist: From 9c0327888824fa2152abd99b9a7dd7ae25a173a4 Mon Sep 17 00:00:00 2001 From: vittoboa Date: Wed, 22 May 2024 20:35:09 +0200 Subject: [PATCH 0005/1230] Add draw/blit to on_release of draggable lenged --- lib/matplotlib/offsetbox.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index cc0121e0a030..6c11e557a457 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -1506,6 +1506,9 @@ def on_release(self, event): self.finalize_offset() self.got_artist = False if self._use_blit: + self.ref_artist.draw( + self.ref_artist.figure._get_renderer()) + self.canvas.blit() self.ref_artist.set_animated(False) def _check_still_parented(self): From ee36329cbd40e75f2141e089efc4b8dacd26caf2 Mon Sep 17 00:00:00 2001 From: vittoboa Date: Wed, 22 May 2024 20:54:39 +0200 Subject: [PATCH 0006/1230] Restore background before draw/blit in on_release of draggable legend --- lib/matplotlib/offsetbox.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 6c11e557a457..417806e4599d 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -1506,6 +1506,7 @@ def on_release(self, event): self.finalize_offset() self.got_artist = False if self._use_blit: + self.canvas.restore_region(self.background) self.ref_artist.draw( self.ref_artist.figure._get_renderer()) self.canvas.blit() From 7e6d1144824589c7a48899bcb615e6a03bc20215 Mon Sep 17 00:00:00 2001 From: MadPhysicist Date: Tue, 11 Jun 2024 10:10:42 -0500 Subject: [PATCH 0007/1230] FIX: Made AffineDeltaTransform pass-through properly --- lib/matplotlib/transforms.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 5003e2113930..e6e7a6bca637 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -2711,9 +2711,12 @@ class AffineDeltaTransform(Affine2DBase): This class is experimental as of 3.3, and the API may change. """ + pass_through = True + def __init__(self, transform, **kwargs): super().__init__(**kwargs) self._base_transform = transform + self.set_children(transform) __str__ = _make_str_method("_base_transform") From caf5111a9b78f6cecd6fecefd2fba2ca1b66a7cb Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 5 Jun 2024 18:22:32 -0700 Subject: [PATCH 0008/1230] Add compilers to conda environment This fixes issues with `libstdc++.so` when using a system with a newer compiler. In such cases, the compiler will create links to new symbols, but the shared library available at runtime from conda-forge will not have them available. This results in errors such as: ``` ImportError: /home/elliott/micromamba/envs/mpl-dev/bin/../lib/libstdc++.so.6: version `CXXABI_1.3.15' not found (required by /home/elliott/code/matplotlib/build/cp39/src/_c_internal_utils.cpython-39-x86_64-linux-gnu.so) ``` --- .appveyor.yml | 2 +- environment.yml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 87f6cbde6384..f40c897736a0 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -68,7 +68,7 @@ install: test_script: # Now build the thing.. - set LINK=/LIBPATH:%cd%\lib - - pip install -v --no-build-isolation --config-settings=setup-args="--vsenv" --editable .[dev] + - pip install -v --no-build-isolation --editable .[dev] # this should show no freetype dll... - set "DUMPBIN=%VS140COMNTOOLS%\..\..\VC\bin\dumpbin.exe" - '"%DUMPBIN%" /DEPENDENTS lib\matplotlib\ft2font*.pyd | findstr freetype.*.dll && exit /b 1 || exit /b 0' diff --git a/environment.yml b/environment.yml index 2930ccf17e83..a5af68cb37da 100644 --- a/environment.yml +++ b/environment.yml @@ -11,6 +11,8 @@ channels: dependencies: # runtime dependencies - cairocffi + - c-compiler + - cxx-compiler - contourpy>=1.0.1 - cycler>=0.10.0 - fonttools>=4.22.0 @@ -24,6 +26,7 @@ dependencies: - pygobject - pyparsing>=2.3.1 - pyqt + - python - python-dateutil>=2.1 - setuptools_scm - wxpython From 1a6ec0f477892b13aa08e915a9d478e841d55c16 Mon Sep 17 00:00:00 2001 From: Pranav Date: Sat, 13 Apr 2024 23:20:29 +0530 Subject: [PATCH 0009/1230] Add support for multiple hatches, edgecolor and linewidth in histograms --- lib/matplotlib/axes/_axes.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 52c99b125d36..65441b7a1ef4 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -7210,9 +7210,34 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, # If None, make all labels None (via zip_longest below); otherwise, # cast each element to str, but keep a single str as it. labels = [] if label is None else np.atleast_1d(np.asarray(label, str)) + + if 'hatch' in kwargs: + if not isinstance(kwargs['hatch'], str): + hatches = itertools.cycle(kwargs['hatch']) + else: + hatches = itertools.cycle([kwargs['hatch']]) + + if 'edgecolor' in kwargs: + if not isinstance(kwargs['edgecolor'], str): + edgecolors = itertools.cycle(kwargs['edgecolor']) + else: + edgecolors = itertools.cycle([kwargs['edgecolor']]) + + if 'linewidth' in kwargs: + if isinstance(kwargs['linewidth'], list or tuple): + linewidths = itertools.cycle(kwargs['linewidth']) + else: + linewidths = itertools.cycle([kwargs['linewidth']]) + for patch, lbl in itertools.zip_longest(patches, labels): if patch: p = patch[0] + if 'hatch' in kwargs: + kwargs['hatch'] = next(hatches) + if 'edgecolor' in kwargs: + kwargs['edgecolor'] = next(edgecolors) + if 'linewidth' in kwargs: + kwargs['linewidth'] = next(linewidths) p._internal_update(kwargs) if lbl is not None: p.set_label(lbl) From 0e52daad459d0f3b1f1238016533ebf4583a3438 Mon Sep 17 00:00:00 2001 From: Pranav Date: Sun, 14 Apr 2024 11:13:46 +0530 Subject: [PATCH 0010/1230] Add hatch, linewidth and edgecolor parameters to multihist example --- .../examples/statistics/histogram_multihist.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/galleries/examples/statistics/histogram_multihist.py b/galleries/examples/statistics/histogram_multihist.py index f1957dc38939..6d2a2d1fc55a 100644 --- a/galleries/examples/statistics/histogram_multihist.py +++ b/galleries/examples/statistics/histogram_multihist.py @@ -27,19 +27,24 @@ fig, ((ax0, ax1), (ax2, ax3)) = plt.subplots(nrows=2, ncols=2) colors = ['red', 'tan', 'lime'] -ax0.hist(x, n_bins, density=True, histtype='bar', color=colors, label=colors) +ax0.hist(x, n_bins, density=True, histtype='bar', color=colors, + label=colors, hatch=['o', '*', '.'], + edgecolor=['black', 'red', 'blue'], linewidth=[1, 2, 3]) ax0.legend(prop={'size': 10}) ax0.set_title('bars with legend') -ax1.hist(x, n_bins, density=True, histtype='bar', stacked=True) +ax1.hist( + x, n_bins, density=True, histtype="bar", stacked=True, + edgecolor=["black", "yellow", "blue"]) ax1.set_title('stacked bar') -ax2.hist(x, n_bins, histtype='step', stacked=True, fill=False) +ax2.hist(x, n_bins, histtype='step', stacked=True, fill=False, + linewidth=[1, 2, 3]) ax2.set_title('stack step (unfilled)') # Make a multiple-histogram of data-sets with different length. x_multi = [np.random.randn(n) for n in [10000, 5000, 2000]] -ax3.hist(x_multi, n_bins, histtype='bar') +ax3.hist(x_multi, n_bins, histtype='bar', hatch=['\\', '/', '-']) ax3.set_title('different sample sizes') fig.tight_layout() From 34df06a477c7bf05517b92d886d676a35ac33fe2 Mon Sep 17 00:00:00 2001 From: Pranav Date: Thu, 25 Apr 2024 18:23:12 +0530 Subject: [PATCH 0011/1230] Add test for added parameters Specify extensions for test Added modified baseline images Modified test for histogram with single parameters Fixed test Add modified baseline images Made changes concise according to suggestion Made a more detailed gallery example Fix Docs Added whats new note, documentation for vectorization, doc fix Added new test, and reverted changes in old test Added baseline images Modified test to pass codecov, added plot in whats new entry Fix test Added baseline images Altered whats new entry, docs and gallery example Resolved edgecolor and facecolor setting Minor fix Fix docs Modified files to include facecolor and added test Removed figsize from test Add multiple baseline image names Fixed test? Fixed test? Removed parametrize usage Add baseline images Add baseline image Fix docs Fix docs Deleted baseline images, changed test Fix test Fix test Handled None array passing to color Handled passing None list Modified nested patch condition Minor Fix Grammar nits Modified test, edited None handling in sequence so that it errors out --- .../histogram_vectorized_parameters.rst | 46 ++++++++ .../statistics/histogram_multihist.py | 102 ++++++++++++++++-- lib/matplotlib/axes/_axes.py | 56 +++++----- lib/matplotlib/tests/test_axes.py | 33 ++++++ 4 files changed, 197 insertions(+), 40 deletions(-) create mode 100644 doc/users/next_whats_new/histogram_vectorized_parameters.rst diff --git a/doc/users/next_whats_new/histogram_vectorized_parameters.rst b/doc/users/next_whats_new/histogram_vectorized_parameters.rst new file mode 100644 index 000000000000..4f063c14651d --- /dev/null +++ b/doc/users/next_whats_new/histogram_vectorized_parameters.rst @@ -0,0 +1,46 @@ +Vectorize ``hatch``, ``edgecolor``, ``facecolor``, ``linewidth`` and ``linestyle`` in *hist* methods +---------------------------------------------------------------------------------------------------- + +The parameters ``hatch``, ``edgecolor``, ``facecolor``, ``linewidth`` and ``linestyle`` +of the `~matplotlib.axes.Axes.hist` method are now vectorized. +This means that you can pass in unique parameters for each histogram that is generated +when the input *x* has multiple datasets. + + +.. plot:: + :include-source: true + :alt: Four charts, each displaying stacked histograms of three Poisson distributions. Each chart differentiates the histograms using various parameters: ax1 uses different linewidths, ax2 uses different hatches, ax3 uses different edgecolors, and ax4 uses different facecolors. Each histogram in ax1 and ax3 also has a different edgecolor. + + import matplotlib.pyplot as plt + import numpy as np + np.random.seed(19680801) + + fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(9, 9)) + + data1 = np.random.poisson(5, 1000) + data2 = np.random.poisson(7, 1000) + data3 = np.random.poisson(10, 1000) + + labels = ["Data 1", "Data 2", "Data 3"] + + ax1.hist([data1, data2, data3], bins=range(17), histtype="step", stacked=True, + edgecolor=["red", "green", "blue"], linewidth=[1, 2, 3]) + ax1.set_title("Different linewidths") + ax1.legend(labels) + + ax2.hist([data1, data2, data3], bins=range(17), histtype="barstacked", + hatch=["/", ".", "*"]) + ax2.set_title("Different hatch patterns") + ax2.legend(labels) + + ax3.hist([data1, data2, data3], bins=range(17), histtype="bar", fill=False, + edgecolor=["red", "green", "blue"], linestyle=["--", "-.", ":"]) + ax3.set_title("Different linestyles") + ax3.legend(labels) + + ax4.hist([data1, data2, data3], bins=range(17), histtype="barstacked", + facecolor=["red", "green", "blue"]) + ax4.set_title("Different facecolors") + ax4.legend(labels) + + plt.show() diff --git a/galleries/examples/statistics/histogram_multihist.py b/galleries/examples/statistics/histogram_multihist.py index 6d2a2d1fc55a..0f12b34855d8 100644 --- a/galleries/examples/statistics/histogram_multihist.py +++ b/galleries/examples/statistics/histogram_multihist.py @@ -15,7 +15,7 @@ select these parameters: http://docs.astropy.org/en/stable/visualization/histogram.html """ - +# %% import matplotlib.pyplot as plt import numpy as np @@ -27,29 +27,111 @@ fig, ((ax0, ax1), (ax2, ax3)) = plt.subplots(nrows=2, ncols=2) colors = ['red', 'tan', 'lime'] -ax0.hist(x, n_bins, density=True, histtype='bar', color=colors, - label=colors, hatch=['o', '*', '.'], - edgecolor=['black', 'red', 'blue'], linewidth=[1, 2, 3]) +ax0.hist(x, n_bins, density=True, histtype='bar', color=colors, label=colors) ax0.legend(prop={'size': 10}) ax0.set_title('bars with legend') -ax1.hist( - x, n_bins, density=True, histtype="bar", stacked=True, - edgecolor=["black", "yellow", "blue"]) +ax1.hist(x, n_bins, density=True, histtype='bar', stacked=True) ax1.set_title('stacked bar') -ax2.hist(x, n_bins, histtype='step', stacked=True, fill=False, - linewidth=[1, 2, 3]) +ax2.hist(x, n_bins, histtype='step', stacked=True, fill=False) ax2.set_title('stack step (unfilled)') # Make a multiple-histogram of data-sets with different length. x_multi = [np.random.randn(n) for n in [10000, 5000, 2000]] -ax3.hist(x_multi, n_bins, histtype='bar', hatch=['\\', '/', '-']) +ax3.hist(x_multi, n_bins, histtype='bar') ax3.set_title('different sample sizes') fig.tight_layout() plt.show() +# %% +# ----------------------------------- +# Setting properties for each dataset +# ----------------------------------- +# +# Plotting bar charts with datasets differentiated using: +# +# * edgecolors +# * facecolors +# * hatches +# * linewidths +# * linestyles +# +# +# Histograms with Edge-Colors +# ........................... + +fig, ax = plt.subplots() + +edgecolors = ['green', 'red', 'blue'] + +ax.hist(x, n_bins, fill=False, histtype="step", stacked=True, + edgecolor=edgecolors, label=edgecolors) +ax.legend() +ax.set_title('Stacked Steps with Edgecolors') + +plt.show() + +# %% +# Face colors +# ........................... + +fig, ax = plt.subplots() + +facecolors = ['green', 'red', 'blue'] + +ax.hist(x, n_bins, histtype="barstacked", facecolor=facecolors, label=facecolors) +ax.legend() +ax.set_title("Bars with different Facecolors") + +plt.show() + +# %% +# Hatches +# ....................... + +fig, ax = plt.subplots() + +hatches = [".", "o", "x"] + +ax.hist(x, n_bins, histtype="barstacked", hatch=hatches, label=hatches) +ax.legend() +ax.set_title("Hatches on Stacked Bars") + +plt.show() + +# %% +# Linewidths +# .......................... + +fig, ax = plt.subplots() + +linewidths = [1, 2, 3] +edgecolors = ["green", "red", "blue"] + +ax.hist(x, n_bins, fill=False, histtype="bar", linewidth=linewidths, + edgecolor=edgecolors, label=linewidths) +ax.legend() +ax.set_title("Bars with Linewidths") + +plt.show() + +# %% +# LineStyles +# .......................... + +fig, ax = plt.subplots() + +linestyles = ['-', ':', '--'] + +ax.hist(x, n_bins, fill=False, histtype='bar', linestyle=linestyles, + edgecolor=edgecolors, label=linestyles) +ax.legend() +ax.set_title('Bars with Linestyles') + +plt.show() + # %% # # .. admonition:: References diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 65441b7a1ef4..f936dba5580c 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6937,7 +6937,10 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, DATA_PARAMETER_PLACEHOLDER **kwargs - `~matplotlib.patches.Patch` properties + `~matplotlib.patches.Patch` properties. The following properties + additionally accept a sequence of values corresponding to the + datasets in *x*: + *edgecolors*, *facecolors*, *linewidths*, *linestyles*, *hatches*. See Also -------- @@ -7211,39 +7214,32 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, # cast each element to str, but keep a single str as it. labels = [] if label is None else np.atleast_1d(np.asarray(label, str)) - if 'hatch' in kwargs: - if not isinstance(kwargs['hatch'], str): - hatches = itertools.cycle(kwargs['hatch']) - else: - hatches = itertools.cycle([kwargs['hatch']]) - - if 'edgecolor' in kwargs: - if not isinstance(kwargs['edgecolor'], str): - edgecolors = itertools.cycle(kwargs['edgecolor']) - else: - edgecolors = itertools.cycle([kwargs['edgecolor']]) + if histtype == "step": + edgecolors = itertools.cycle(np.atleast_1d(kwargs.get('edgecolor', + colors))) + else: + edgecolors = itertools.cycle(np.atleast_1d(kwargs.get("edgecolor", None))) - if 'linewidth' in kwargs: - if isinstance(kwargs['linewidth'], list or tuple): - linewidths = itertools.cycle(kwargs['linewidth']) - else: - linewidths = itertools.cycle([kwargs['linewidth']]) + facecolors = itertools.cycle(np.atleast_1d(kwargs.get('facecolor', colors))) + hatches = itertools.cycle(np.atleast_1d(kwargs.get('hatch', None))) + linewidths = itertools.cycle(np.atleast_1d(kwargs.get('linewidth', None))) + linestyles = itertools.cycle(np.atleast_1d(kwargs.get('linestyle', None))) for patch, lbl in itertools.zip_longest(patches, labels): - if patch: - p = patch[0] - if 'hatch' in kwargs: - kwargs['hatch'] = next(hatches) - if 'edgecolor' in kwargs: - kwargs['edgecolor'] = next(edgecolors) - if 'linewidth' in kwargs: - kwargs['linewidth'] = next(linewidths) + p = patch[0] + kwargs.update({ + 'hatch': next(hatches), + 'linewidth': next(linewidths), + 'linestyle': next(linestyles), + 'edgecolor': next(edgecolors), + 'facecolor': next(facecolors), + }) + p._internal_update(kwargs) + if lbl is not None: + p.set_label(lbl) + for p in patch[1:]: p._internal_update(kwargs) - if lbl is not None: - p.set_label(lbl) - for p in patch[1:]: - p._internal_update(kwargs) - p.set_label('_nolegend_') + p.set_label('_nolegend_') if nx == 1: return tops[0], bins, patches[0] diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index dd37d3d8ee80..9119c1050e3f 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4603,6 +4603,39 @@ def test_hist_stacked_bar(): ax.legend(loc='upper right', bbox_to_anchor=(1.0, 1.0), ncols=1) +@pytest.mark.parametrize("histtype", ["step", "stepfilled"]) +@pytest.mark.parametrize("color", [["blue", "green", "brown"], None]) +@pytest.mark.parametrize("edgecolor", [["red", "black", "blue"], [None]*3]) +@pytest.mark.parametrize("facecolor", [["blue", "green", "brown"], [None]*3]) +@check_figures_equal(extensions=["png"]) +def test_hist_vectorized_params(fig_test, fig_ref, histtype, color, edgecolor, + facecolor): + np.random.seed(19680801) + x = [np.random.randn(n) for n in [2000, 5000, 10000]] + linewidth = [1, 1.5, 2] + hatch = ["/", "\\", "."] + linestyle = ["-", "--", ":"] + + facecolor = facecolor if facecolor[0] is not None else color + if histtype == "step": + edgecolor = edgecolor if edgecolor[0] is not None else color + + _, bins, _ = fig_test.subplots().hist(x, bins=10, histtype=histtype, color=color, + edgecolor=edgecolor, facecolor=facecolor, + linewidth=linewidth, hatch=hatch, + linestyle=linestyle) + ref_ax = fig_ref.subplots() + color = [None]*3 if color is None else color + edgecolor = [None]*3 if edgecolor is None else edgecolor + facecolor = [None]*3 if facecolor is None else facecolor + + for i in range(2, -1, -1): + ref_ax.hist(x[i], bins=bins, histtype=histtype, color=color[i], + edgecolor=edgecolor[i], facecolor=facecolor[i], + linewidth=linewidth[i], hatch=hatch[i], + linestyle=linestyle[i]) + + def test_hist_barstacked_bottom_unchanged(): b = np.array([10, 20]) plt.hist([[0, 1], [0, 1]], 2, histtype="barstacked", bottom=b) From 4fcb70982e45ab9885f829a52d39219ecf428347 Mon Sep 17 00:00:00 2001 From: Pranav Date: Sun, 16 Jun 2024 15:41:59 +0530 Subject: [PATCH 0012/1230] Reduced to 13 tests in total Separated tests Modified test Modified test to pass by using halved zorders --- lib/matplotlib/tests/test_axes.py | 69 ++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 9119c1050e3f..c8e5b1eaa5a2 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4603,37 +4603,56 @@ def test_hist_stacked_bar(): ax.legend(loc='upper right', bbox_to_anchor=(1.0, 1.0), ncols=1) -@pytest.mark.parametrize("histtype", ["step", "stepfilled"]) -@pytest.mark.parametrize("color", [["blue", "green", "brown"], None]) -@pytest.mark.parametrize("edgecolor", [["red", "black", "blue"], [None]*3]) -@pytest.mark.parametrize("facecolor", [["blue", "green", "brown"], [None]*3]) @check_figures_equal(extensions=["png"]) -def test_hist_vectorized_params(fig_test, fig_ref, histtype, color, edgecolor, - facecolor): +def test_hist_vectorized_params(fig_test, fig_ref): np.random.seed(19680801) x = [np.random.randn(n) for n in [2000, 5000, 10000]] - linewidth = [1, 1.5, 2] + + facecolor = ["b", "g", "r"] + edgecolor = ["b", "g", "r"] hatch = ["/", "\\", "."] linestyle = ["-", "--", ":"] + linewidth = [1, 1.5, 2] + colors = ["b", "g", "r"] + ((axt1, axt2, axt3), (axt4, axt5, axt6)) = fig_test.subplots(2, 3) + ((axr1, axr2, axr3), (axr4, axr5, axr6)) = fig_ref.subplots(2, 3) + + _, bins, _ = axt1.hist(x, bins=10, histtype="stepfilled", facecolor=facecolor) + + for i, (xi, fc) in enumerate(zip(x, facecolor)): + axr1.hist(xi, bins=bins, histtype="stepfilled", facecolor=fc, + zorder=(len(x)-i)/2) + + _, bins, _ = axt2.hist(x, bins=10, histtype="step", edgecolor=edgecolor) + + for i, (xi, ec) in enumerate(zip(x, edgecolor)): + axr2.hist(xi, bins=bins, histtype="step", edgecolor=ec, zorder=(len(x)-i)/2) + + _, bins, _ = axt3.hist(x, bins=10, histtype="stepfilled", hatch=hatch, + facecolor=facecolor) + + for i, (xi, fc, ht) in enumerate(zip(x, facecolor, hatch)): + axr3.hist(xi, bins=bins, histtype="stepfilled", hatch=ht, facecolor=fc, + zorder=(len(x)-i)/2) + + _, bins, _ = axt4.hist(x, bins=10, histtype="step", linestyle=linestyle, + edgecolor=edgecolor) + + for i, (xi, ec, ls) in enumerate(zip(x, edgecolor, linestyle)): + axr4.hist(xi, bins=bins, histtype="step", linestyle=ls, edgecolor=ec, + zorder=(len(x)-i)/2) + + _, bins, _ = axt5.hist(x, bins=10, histtype="step", linewidth=linewidth, + edgecolor=edgecolor) + + for i, (xi, ec, lw) in enumerate(zip(x, edgecolor, linewidth)): + axr5.hist(xi, bins=bins, histtype="step", linewidth=lw, edgecolor=ec, + zorder=(len(x)-i)/2) + + _, bins, _ = axt6.hist(x, bins=10, histtype="stepfilled", color=colors) - facecolor = facecolor if facecolor[0] is not None else color - if histtype == "step": - edgecolor = edgecolor if edgecolor[0] is not None else color - - _, bins, _ = fig_test.subplots().hist(x, bins=10, histtype=histtype, color=color, - edgecolor=edgecolor, facecolor=facecolor, - linewidth=linewidth, hatch=hatch, - linestyle=linestyle) - ref_ax = fig_ref.subplots() - color = [None]*3 if color is None else color - edgecolor = [None]*3 if edgecolor is None else edgecolor - facecolor = [None]*3 if facecolor is None else facecolor - - for i in range(2, -1, -1): - ref_ax.hist(x[i], bins=bins, histtype=histtype, color=color[i], - edgecolor=edgecolor[i], facecolor=facecolor[i], - linewidth=linewidth[i], hatch=hatch[i], - linestyle=linestyle[i]) + for i, (xi, c) in enumerate(zip(x, colors)): + axr6.hist(xi, bins=bins, histtype="stepfilled", color=c, zorder=(len(x)-i)/2) def test_hist_barstacked_bottom_unchanged(): From 347ce221f0b289b2ec577114b29729c844760402 Mon Sep 17 00:00:00 2001 From: Pranav Date: Tue, 25 Jun 2024 11:47:38 +0530 Subject: [PATCH 0013/1230] Added color semantics test and modified vectorized parameters test Edited expected facecolors for step Changed blue to C0 --- lib/matplotlib/tests/test_axes.py | 98 ++++++++++++++++--------------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index c8e5b1eaa5a2..2456a3880a81 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4603,56 +4603,62 @@ def test_hist_stacked_bar(): ax.legend(loc='upper right', bbox_to_anchor=(1.0, 1.0), ncols=1) +@pytest.mark.parametrize('kwargs', ({'facecolor': ["b", "g", "r"]}, + {'edgecolor': ["b", "g", "r"]}, + {'hatch': ["/", "\\", "."]}, + {'linestyle': ["-", "--", ":"]}, + {'linewidth': [1, 1.5, 2]}, + {'color': ["b", "g", "r"]})) @check_figures_equal(extensions=["png"]) -def test_hist_vectorized_params(fig_test, fig_ref): +def test_hist_vectorized_params(fig_test, fig_ref, kwargs): np.random.seed(19680801) x = [np.random.randn(n) for n in [2000, 5000, 10000]] - facecolor = ["b", "g", "r"] - edgecolor = ["b", "g", "r"] - hatch = ["/", "\\", "."] - linestyle = ["-", "--", ":"] - linewidth = [1, 1.5, 2] - colors = ["b", "g", "r"] - ((axt1, axt2, axt3), (axt4, axt5, axt6)) = fig_test.subplots(2, 3) - ((axr1, axr2, axr3), (axr4, axr5, axr6)) = fig_ref.subplots(2, 3) - - _, bins, _ = axt1.hist(x, bins=10, histtype="stepfilled", facecolor=facecolor) - - for i, (xi, fc) in enumerate(zip(x, facecolor)): - axr1.hist(xi, bins=bins, histtype="stepfilled", facecolor=fc, - zorder=(len(x)-i)/2) - - _, bins, _ = axt2.hist(x, bins=10, histtype="step", edgecolor=edgecolor) - - for i, (xi, ec) in enumerate(zip(x, edgecolor)): - axr2.hist(xi, bins=bins, histtype="step", edgecolor=ec, zorder=(len(x)-i)/2) - - _, bins, _ = axt3.hist(x, bins=10, histtype="stepfilled", hatch=hatch, - facecolor=facecolor) - - for i, (xi, fc, ht) in enumerate(zip(x, facecolor, hatch)): - axr3.hist(xi, bins=bins, histtype="stepfilled", hatch=ht, facecolor=fc, - zorder=(len(x)-i)/2) - - _, bins, _ = axt4.hist(x, bins=10, histtype="step", linestyle=linestyle, - edgecolor=edgecolor) - - for i, (xi, ec, ls) in enumerate(zip(x, edgecolor, linestyle)): - axr4.hist(xi, bins=bins, histtype="step", linestyle=ls, edgecolor=ec, - zorder=(len(x)-i)/2) - - _, bins, _ = axt5.hist(x, bins=10, histtype="step", linewidth=linewidth, - edgecolor=edgecolor) - - for i, (xi, ec, lw) in enumerate(zip(x, edgecolor, linewidth)): - axr5.hist(xi, bins=bins, histtype="step", linewidth=lw, edgecolor=ec, - zorder=(len(x)-i)/2) - - _, bins, _ = axt6.hist(x, bins=10, histtype="stepfilled", color=colors) - - for i, (xi, c) in enumerate(zip(x, colors)): - axr6.hist(xi, bins=bins, histtype="stepfilled", color=c, zorder=(len(x)-i)/2) + (axt1, axt2) = fig_test.subplots(2) + (axr1, axr2) = fig_ref.subplots(2) + + for histtype, axt, axr in [("stepfilled", axt1, axr1), ("step", axt2, axr2)]: + _, bins, _ = axt.hist(x, bins=10, histtype=histtype, **kwargs) + + kw, values = next(iter(kwargs.items())) + for i, (xi, value) in enumerate(zip(x, values)): + axr.hist(xi, bins=bins, histtype=histtype, **{kw: value}, + zorder=(len(x)-i)/2) + + +@pytest.mark.parametrize('kwargs, patch_face, patch_edge', + [({'histtype': 'stepfilled', 'color': 'r', + 'facecolor': 'y', 'edgecolor': 'g'}, 'y', 'g'), + ({'histtype': 'step', 'color': 'r', + 'facecolor': 'y', 'edgecolor': 'g'}, ('y', 0), 'g'), + ({'histtype': 'stepfilled', 'color': 'r', + 'edgecolor': 'g'}, 'r', 'g'), + ({'histtype': 'step', 'color': 'r', + 'edgecolor': 'g'}, ('r', 0), 'g'), + ({'histtype': 'stepfilled', 'color': 'r', + 'facecolor': 'y'}, 'y', 'k'), + ({'histtype': 'step', 'color': 'r', + 'facecolor': 'y'}, ('y', 0), 'r'), + ({'histtype': 'stepfilled', + 'facecolor': 'y', 'edgecolor': 'g'}, 'y', 'g'), + ({'histtype': 'step', 'facecolor': 'y', + 'edgecolor': 'g'}, ('y', 0), 'g'), + ({'histtype': 'stepfilled', 'color': 'r'}, 'r', 'k'), + ({'histtype': 'step', 'color': 'r'}, ('r', 0), 'r'), + ({'histtype': 'stepfilled', 'facecolor': 'y'}, 'y', 'k'), + ({'histtype': 'step', 'facecolor': 'y'}, ('y', 0), 'C0'), + ({'histtype': 'stepfilled', 'edgecolor': 'g'}, 'C0', 'g'), + ({'histtype': 'step', 'edgecolor': 'g'}, ('C0', 0), 'g'), + ({'histtype': 'stepfilled'}, 'C0', 'k'), + ({'histtype': 'step'}, ('C0', 0), 'C0')]) +def test_hist_color_semantics(kwargs, patch_face, patch_edge): + _, _, patches = plt.figure().subplots().hist([1, 2, 3], **kwargs) + # 'C0'(blue) stands for the first color of the default color cycle + # as well as the patch.facecolor rcParam + # When the expected edgecolor is 'k'(black), it corresponds to the + # patch.edgecolor rcParam + assert all(mcolors.same_color([p.get_facecolor(), p.get_edgecolor()], + [patch_face, patch_edge]) for p in patches) def test_hist_barstacked_bottom_unchanged(): From d7ffeaa16c5ee7dc5306adc426bcd2ca4b65e27f Mon Sep 17 00:00:00 2001 From: Pranav Date: Fri, 28 Jun 2024 10:21:01 +0530 Subject: [PATCH 0014/1230] Added None patch test --- lib/matplotlib/axes/_axes.py | 27 ++++++++++++++------------- lib/matplotlib/tests/test_axes.py | 5 +++++ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index f936dba5580c..a619d458ff07 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -7226,20 +7226,21 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, linestyles = itertools.cycle(np.atleast_1d(kwargs.get('linestyle', None))) for patch, lbl in itertools.zip_longest(patches, labels): - p = patch[0] - kwargs.update({ - 'hatch': next(hatches), - 'linewidth': next(linewidths), - 'linestyle': next(linestyles), - 'edgecolor': next(edgecolors), - 'facecolor': next(facecolors), - }) - p._internal_update(kwargs) - if lbl is not None: - p.set_label(lbl) - for p in patch[1:]: + if patch: + p = patch[0] + kwargs.update({ + 'hatch': next(hatches), + 'linewidth': next(linewidths), + 'linestyle': next(linestyles), + 'edgecolor': next(edgecolors), + 'facecolor': next(facecolors), + }) p._internal_update(kwargs) - p.set_label('_nolegend_') + if lbl is not None: + p.set_label(lbl) + for p in patch[1:]: + p._internal_update(kwargs) + p.set_label('_nolegend_') if nx == 1: return tops[0], bins, patches[0] diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 2456a3880a81..35da74dfea62 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4672,6 +4672,11 @@ def test_hist_emptydata(): ax.hist([[], range(10), range(10)], histtype="step") +def test_hist_none_patch(): + x = [[1, 2], [2, 3]] + plt.hist(x, label=["First", "Second", "Third"]) + + def test_hist_labels(): # test singleton labels OK fig, ax = plt.subplots() From 9024be4fc848af32b09f7b62e81c1062adab4e45 Mon Sep 17 00:00:00 2001 From: Pranav Date: Fri, 28 Jun 2024 10:27:02 +0530 Subject: [PATCH 0015/1230] Heading edit --- galleries/examples/statistics/histogram_multihist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/galleries/examples/statistics/histogram_multihist.py b/galleries/examples/statistics/histogram_multihist.py index 0f12b34855d8..ba9570f97036 100644 --- a/galleries/examples/statistics/histogram_multihist.py +++ b/galleries/examples/statistics/histogram_multihist.py @@ -59,7 +59,7 @@ # * linestyles # # -# Histograms with Edge-Colors +# Edge-Colors # ........................... fig, ax = plt.subplots() @@ -74,7 +74,7 @@ plt.show() # %% -# Face colors +# Face-Colors # ........................... fig, ax = plt.subplots() From 3bfca5fe17e36798f0f836654319b4f72f67575b Mon Sep 17 00:00:00 2001 From: Pranav Date: Fri, 28 Jun 2024 10:50:10 +0530 Subject: [PATCH 0016/1230] Simplified test --- lib/matplotlib/tests/test_axes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 35da74dfea62..051cc488676b 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4673,8 +4673,7 @@ def test_hist_emptydata(): def test_hist_none_patch(): - x = [[1, 2], [2, 3]] - plt.hist(x, label=["First", "Second", "Third"]) + plt.hist([1, 2], label=["First", "Second"]) def test_hist_labels(): From 6029e32e35dcc2573b3572103d6b9aa73b73399e Mon Sep 17 00:00:00 2001 From: Pranav Date: Mon, 1 Jul 2024 11:51:48 +0530 Subject: [PATCH 0017/1230] Updated none patch test --- lib/matplotlib/tests/test_axes.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 051cc488676b..dbdf73fa7e73 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4673,7 +4673,13 @@ def test_hist_emptydata(): def test_hist_none_patch(): - plt.hist([1, 2], label=["First", "Second"]) + # To cover None patches when excess labels are provided + labels = ["First", "Second"] + patches = [[1, 2]] + fig, ax = plt.subplots() + ax.hist(patches, label=labels) + _, lbls = ax.get_legend_handles_labels() + assert (len(lbls) < len(labels) and len(patches) < len(labels)) def test_hist_labels(): From 0349e7d42b6ef256a45f21b769c59a8d786d9aaf Mon Sep 17 00:00:00 2001 From: Pranav Date: Tue, 2 Jul 2024 08:35:26 +0530 Subject: [PATCH 0018/1230] Made None patch condition explicit --- lib/matplotlib/axes/_axes.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index a619d458ff07..c0329abc02c7 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -7226,21 +7226,22 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, linestyles = itertools.cycle(np.atleast_1d(kwargs.get('linestyle', None))) for patch, lbl in itertools.zip_longest(patches, labels): - if patch: - p = patch[0] - kwargs.update({ - 'hatch': next(hatches), - 'linewidth': next(linewidths), - 'linestyle': next(linestyles), - 'edgecolor': next(edgecolors), - 'facecolor': next(facecolors), - }) + if not patch: + continue + p = patch[0] + kwargs.update({ + 'hatch': next(hatches), + 'linewidth': next(linewidths), + 'linestyle': next(linestyles), + 'edgecolor': next(edgecolors), + 'facecolor': next(facecolors), + }) + p._internal_update(kwargs) + if lbl is not None: + p.set_label(lbl) + for p in patch[1:]: p._internal_update(kwargs) - if lbl is not None: - p.set_label(lbl) - for p in patch[1:]: - p._internal_update(kwargs) - p.set_label('_nolegend_') + p.set_label('_nolegend_') if nx == 1: return tops[0], bins, patches[0] From 613a8e88d6753fcc985760395db35b5d951aa41d Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sat, 6 Jul 2024 17:51:53 +0200 Subject: [PATCH 0019/1230] [TYP] Fix overload of `pyplot.subplots` --- lib/matplotlib/figure.pyi | 14 ++++++++++++++ lib/matplotlib/pyplot.py | 17 +++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi index b079312695c1..91196f6add8e 100644 --- a/lib/matplotlib/figure.pyi +++ b/lib/matplotlib/figure.pyi @@ -106,6 +106,20 @@ class FigureBase(Artist): gridspec_kw: dict[str, Any] | None = ..., ) -> Axes: ... @overload + def subplots( + self, + nrows: int = ..., + ncols: int = ..., + *, + sharex: bool | Literal["none", "all", "row", "col"] = ..., + sharey: bool | Literal["none", "all", "row", "col"] = ..., + squeeze: Literal[True], + width_ratios: Sequence[float] | None = ..., + height_ratios: Sequence[float] | None = ..., + subplot_kw: dict[str, Any] | None = ..., + gridspec_kw: dict[str, Any] | None = ..., + ) -> np.ndarray: ... # TODO numpy/numpy#24738 + @overload def subplots( self, nrows: int = ..., diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 441af598dbc6..f196a8e9d440 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -1579,6 +1579,23 @@ def subplots( ... +@overload +def subplots( + nrows: int = ..., + ncols: int = ..., + *, + sharex: bool | Literal["none", "all", "row", "col"] = ..., + sharey: bool | Literal["none", "all", "row", "col"] = ..., + squeeze: Literal[True] = ..., + width_ratios: Sequence[float] | None = ..., + height_ratios: Sequence[float] | None = ..., + subplot_kw: dict[str, Any] | None = ..., + gridspec_kw: dict[str, Any] | None = ..., + **fig_kw +) -> tuple[Figure, np.ndarray]: # TODO numpy/numpy#24738 + ... + + @overload def subplots( nrows: int = ..., From 5ea9a7f6b23ffa7a0f9a37e8422feddefa75d0db Mon Sep 17 00:00:00 2001 From: hannah Date: Sun, 7 Jul 2024 06:10:23 -0400 Subject: [PATCH 0020/1230] Backport PR #28517: DOC: better cross referencing for animations --- doc/api/animation_api.rst | 3 +++ galleries/users_explain/animations/animations.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/api/animation_api.rst b/doc/api/animation_api.rst index df39b5942199..1233b482165d 100644 --- a/doc/api/animation_api.rst +++ b/doc/api/animation_api.rst @@ -18,6 +18,9 @@ Animation The easiest way to make a live animation in Matplotlib is to use one of the `Animation` classes. +.. seealso:: + - :ref:`animations` + .. inheritance-diagram:: matplotlib.animation.FuncAnimation matplotlib.animation.ArtistAnimation :parts: 1 diff --git a/galleries/users_explain/animations/animations.py b/galleries/users_explain/animations/animations.py index 0391d9bc030a..2711663196f2 100644 --- a/galleries/users_explain/animations/animations.py +++ b/galleries/users_explain/animations/animations.py @@ -11,7 +11,7 @@ generate animations using the `~matplotlib.animation` module. An animation is a sequence of frames where each frame corresponds to a plot on a `~matplotlib.figure.Figure`. This tutorial covers a general guideline on -how to create such animations and the different options available. +how to create such animations and the different options available. More information is available in the API description: `~matplotlib.animation` """ import matplotlib.pyplot as plt From af6cc664bcdff66b1f1c3d4afdc93832681253ef Mon Sep 17 00:00:00 2001 From: hannah Date: Sun, 7 Jul 2024 06:10:23 -0400 Subject: [PATCH 0021/1230] Backport PR #28517: DOC: better cross referencing for animations --- doc/api/animation_api.rst | 3 +++ galleries/users_explain/animations/animations.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/api/animation_api.rst b/doc/api/animation_api.rst index df39b5942199..1233b482165d 100644 --- a/doc/api/animation_api.rst +++ b/doc/api/animation_api.rst @@ -18,6 +18,9 @@ Animation The easiest way to make a live animation in Matplotlib is to use one of the `Animation` classes. +.. seealso:: + - :ref:`animations` + .. inheritance-diagram:: matplotlib.animation.FuncAnimation matplotlib.animation.ArtistAnimation :parts: 1 diff --git a/galleries/users_explain/animations/animations.py b/galleries/users_explain/animations/animations.py index 0391d9bc030a..2711663196f2 100644 --- a/galleries/users_explain/animations/animations.py +++ b/galleries/users_explain/animations/animations.py @@ -11,7 +11,7 @@ generate animations using the `~matplotlib.animation` module. An animation is a sequence of frames where each frame corresponds to a plot on a `~matplotlib.figure.Figure`. This tutorial covers a general guideline on -how to create such animations and the different options available. +how to create such animations and the different options available. More information is available in the API description: `~matplotlib.animation` """ import matplotlib.pyplot as plt From b7afb01dab684ddb8d6631ca2a0698f7f54bbfe0 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Mon, 8 Jul 2024 08:44:59 -0500 Subject: [PATCH 0022/1230] Backport PR #28523: Fix value error when set widget size to zero while using FiCureCanvasQT --- lib/matplotlib/backends/backend_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index a93b37799971..6603883075d4 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -492,7 +492,7 @@ def _draw_idle(self): if not self._draw_pending: return self._draw_pending = False - if self.height() < 0 or self.width() < 0: + if self.height() <= 0 or self.width() <= 0: return try: self.draw() From d1a80797948cd4bdbd70e04d05b4c183e931d781 Mon Sep 17 00:00:00 2001 From: Pranav Date: Tue, 9 Jul 2024 18:50:30 +0530 Subject: [PATCH 0023/1230] Made suggested changes --- .../histogram_vectorized_parameters.rst | 6 ++--- .../statistics/histogram_multihist.py | 23 ++++++++++--------- lib/matplotlib/tests/test_axes.py | 23 +++++++++---------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/doc/users/next_whats_new/histogram_vectorized_parameters.rst b/doc/users/next_whats_new/histogram_vectorized_parameters.rst index 4f063c14651d..f4b6b38e1ce6 100644 --- a/doc/users/next_whats_new/histogram_vectorized_parameters.rst +++ b/doc/users/next_whats_new/histogram_vectorized_parameters.rst @@ -1,9 +1,9 @@ -Vectorize ``hatch``, ``edgecolor``, ``facecolor``, ``linewidth`` and ``linestyle`` in *hist* methods ----------------------------------------------------------------------------------------------------- +Vectorized ``hist`` style parameters +------------------------------------ The parameters ``hatch``, ``edgecolor``, ``facecolor``, ``linewidth`` and ``linestyle`` of the `~matplotlib.axes.Axes.hist` method are now vectorized. -This means that you can pass in unique parameters for each histogram that is generated +This means that you can pass in individual parameters for each histogram when the input *x* has multiple datasets. diff --git a/galleries/examples/statistics/histogram_multihist.py b/galleries/examples/statistics/histogram_multihist.py index ba9570f97036..63cfde06c053 100644 --- a/galleries/examples/statistics/histogram_multihist.py +++ b/galleries/examples/statistics/histogram_multihist.py @@ -50,16 +50,17 @@ # Setting properties for each dataset # ----------------------------------- # -# Plotting bar charts with datasets differentiated using: +# You can style the histograms individually by passing a list of values to the +# following parameters: # -# * edgecolors -# * facecolors -# * hatches -# * linewidths -# * linestyles +# * edgecolor +# * facecolor +# * hatch +# * linewidth +# * linestyle # # -# Edge-Colors +# edgecolor # ........................... fig, ax = plt.subplots() @@ -74,7 +75,7 @@ plt.show() # %% -# Face-Colors +# facecolor # ........................... fig, ax = plt.subplots() @@ -88,7 +89,7 @@ plt.show() # %% -# Hatches +# hatch # ....................... fig, ax = plt.subplots() @@ -102,7 +103,7 @@ plt.show() # %% -# Linewidths +# linewidth # .......................... fig, ax = plt.subplots() @@ -118,7 +119,7 @@ plt.show() # %% -# LineStyles +# linestyle # .......................... fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index dbdf73fa7e73..56c3d53a617c 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4612,18 +4612,18 @@ def test_hist_stacked_bar(): @check_figures_equal(extensions=["png"]) def test_hist_vectorized_params(fig_test, fig_ref, kwargs): np.random.seed(19680801) - x = [np.random.randn(n) for n in [2000, 5000, 10000]] + xs = [np.random.randn(n) for n in [20, 50, 100]] (axt1, axt2) = fig_test.subplots(2) (axr1, axr2) = fig_ref.subplots(2) for histtype, axt, axr in [("stepfilled", axt1, axr1), ("step", axt2, axr2)]: - _, bins, _ = axt.hist(x, bins=10, histtype=histtype, **kwargs) + _, bins, _ = axt.hist(xs, bins=10, histtype=histtype, **kwargs) kw, values = next(iter(kwargs.items())) - for i, (xi, value) in enumerate(zip(x, values)): - axr.hist(xi, bins=bins, histtype=histtype, **{kw: value}, - zorder=(len(x)-i)/2) + for i, (x, value) in enumerate(zip(xs, values)): + axr.hist(x, bins=bins, histtype=histtype, **{kw: value}, + zorder=(len(xs)-i)/2) @pytest.mark.parametrize('kwargs, patch_face, patch_edge', @@ -4672,14 +4672,13 @@ def test_hist_emptydata(): ax.hist([[], range(10), range(10)], histtype="step") -def test_hist_none_patch(): - # To cover None patches when excess labels are provided - labels = ["First", "Second"] - patches = [[1, 2]] +def test_hist_unused_labels(): + # When a list with one dataset and N elements is provided and N labels, ensure + # that the first label is used for the dataset and all other labels are ignored fig, ax = plt.subplots() - ax.hist(patches, label=labels) - _, lbls = ax.get_legend_handles_labels() - assert (len(lbls) < len(labels) and len(patches) < len(labels)) + ax.hist([[1, 2, 3]], label=["values", "unused", "also unused"]) + _, labels = ax.get_legend_handles_labels() + assert labels == ["values"] def test_hist_labels(): From 2345e12e704b13d0597599d66efc9c8421b0f44b Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Mon, 27 Nov 2023 21:59:01 -0700 Subject: [PATCH 0024/1230] Fix 3D lines being visible when behind camera Deprecate proj_transform_clip Deprecate proj_transform_clip --- .../deprecations/27385-SS.rst | 3 +++ lib/matplotlib/path.py | 2 +- lib/mpl_toolkits/mplot3d/art3d.py | 24 ++++++++++++------- lib/mpl_toolkits/mplot3d/proj3d.py | 23 +++++++++++------- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 15 ++++++++++++ 5 files changed, 49 insertions(+), 18 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/27385-SS.rst diff --git a/doc/api/next_api_changes/deprecations/27385-SS.rst b/doc/api/next_api_changes/deprecations/27385-SS.rst new file mode 100644 index 000000000000..b388ce22eb2b --- /dev/null +++ b/doc/api/next_api_changes/deprecations/27385-SS.rst @@ -0,0 +1,3 @@ +``proj3d.proj_transform_clip`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... is deprecated with no replacement. diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 94fd97d7b599..0e870161a48d 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -129,7 +129,7 @@ def __init__(self, vertices, codes=None, _interpolation_steps=1, vertices = _to_unmasked_float_array(vertices) _api.check_shape((None, 2), vertices=vertices) - if codes is not None: + if codes is not None and len(vertices): codes = np.asarray(codes, self.code_type) if codes.ndim != 1 or len(codes) != len(vertices): raise ValueError("'codes' must be a 1D list or array with the " diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index feeff130b0cd..38ebe88dc80e 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -267,7 +267,9 @@ def get_data_3d(self): @artist.allow_rasterization def draw(self, renderer): xs3d, ys3d, zs3d = self._verts3d - xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M) + xs, ys, zs, tis = proj3d._proj_transform_clip(xs3d, ys3d, zs3d, + self.axes.M, + self.axes._focal_length) self.set_data(xs, ys) super().draw(renderer) self.stale = False @@ -458,8 +460,9 @@ def get_path(self): def do_3d_projection(self): s = self._segment3d xs, ys, zs = zip(*s) - vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, - self.axes.M) + vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, + self.axes.M, + self.axes._focal_length) self._path2d = mpath.Path(np.column_stack([vxs, vys])) return min(vzs) @@ -505,8 +508,9 @@ def set_3d_properties(self, path, zs=0, zdir='z'): def do_3d_projection(self): s = self._segment3d xs, ys, zs = zip(*s) - vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, - self.axes.M) + vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, + self.axes.M, + self.axes._focal_length) self._path2d = mpath.Path(np.column_stack([vxs, vys]), self._code3d) return min(vzs) @@ -611,8 +615,9 @@ def set_3d_properties(self, zs, zdir): def do_3d_projection(self): xs, ys, zs = self._offsets3d - vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, - self.axes.M) + vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, + self.axes.M, + self.axes._focal_length) self._vzs = vzs super().set_offsets(np.column_stack([vxs, vys])) @@ -752,8 +757,9 @@ def set_depthshade(self, depthshade): def do_3d_projection(self): xs, ys, zs = self._offsets3d - vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, - self.axes.M) + vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, + self.axes.M, + self.axes._focal_length) # Sort the points based on z coordinates # Performance optimization: Create a sorted index array and reorder # points and point properties according to the index array diff --git a/lib/mpl_toolkits/mplot3d/proj3d.py b/lib/mpl_toolkits/mplot3d/proj3d.py index 098a7b6f6667..f010ddda44a9 100644 --- a/lib/mpl_toolkits/mplot3d/proj3d.py +++ b/lib/mpl_toolkits/mplot3d/proj3d.py @@ -173,19 +173,21 @@ def _ortho_transformation(zfront, zback): def _proj_transform_vec(vec, M): vecw = np.dot(M, vec) w = vecw[3] - # clip here.. txs, tys, tzs = vecw[0]/w, vecw[1]/w, vecw[2]/w return txs, tys, tzs -def _proj_transform_vec_clip(vec, M): +def _proj_transform_vec_clip(vec, M, focal_length): vecw = np.dot(M, vec) w = vecw[3] - # clip here. txs, tys, tzs = vecw[0] / w, vecw[1] / w, vecw[2] / w - tis = (0 <= vecw[0]) & (vecw[0] <= 1) & (0 <= vecw[1]) & (vecw[1] <= 1) - if np.any(tis): - tis = vecw[1] < 1 + if np.isinf(focal_length): # don't clip orthographic projection + tis = np.ones(txs.shape, dtype=bool) + else: + tis = (-1 <= txs) & (txs <= 1) & (-1 <= tys) & (tys <= 1) & (tzs <= 0) + txs = np.ma.masked_array(txs, ~tis) + tys = np.ma.masked_array(tys, ~tis) + tzs = np.ma.masked_array(tzs, ~tis) return txs, tys, tzs, tis @@ -220,14 +222,19 @@ def proj_transform(xs, ys, zs, M): alternative="proj_transform")(proj_transform) -def proj_transform_clip(xs, ys, zs, M): +@_api.deprecated("3.10") +def proj_transform_clip(xs, ys, zs, M, focal_length=np.inf): + return _proj_transform_clip(xs, ys, zs, M, focal_length) + + +def _proj_transform_clip(xs, ys, zs, M, focal_length): """ Transform the points by the projection matrix and return the clipping result returns txs, tys, tzs, tis """ vec = _vec_pad_ones(xs, ys, zs) - return _proj_transform_vec_clip(vec, M) + return _proj_transform_vec_clip(vec, M, focal_length) @_api.deprecated("3.8") diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index be988c31ee75..2f05df87d145 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -1325,6 +1325,21 @@ def test_unautoscale(axis, auto): np.testing.assert_array_equal(get_lim(), (-0.5, 0.5)) +@check_figures_equal(extensions=["png"]) +def test_culling(fig_test, fig_ref): + xmins = (-100, -50) + for fig, xmin in zip((fig_test, fig_ref), xmins): + ax = fig.add_subplot(projection='3d') + n = abs(xmin) + 1 + xs = np.linspace(0, xmin, n) + ys = np.ones(n) + zs = np.zeros(n) + ax.plot(xs, ys, zs, 'k') + + ax.set(xlim=(-5, 5), ylim=(-5, 5), zlim=(-5, 5)) + ax.view_init(5, 180, 0) + + def test_axes3d_focal_length_checks(): fig = plt.figure() ax = fig.add_subplot(projection='3d') From 9a813f490822c6299e05cb7d37c064480e31f3e4 Mon Sep 17 00:00:00 2001 From: Pranav Date: Wed, 10 Jul 2024 14:26:37 +0530 Subject: [PATCH 0025/1230] Added version-added directive --- lib/matplotlib/axes/_axes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index c0329abc02c7..4721b26980d4 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6942,6 +6942,8 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, datasets in *x*: *edgecolors*, *facecolors*, *linewidths*, *linestyles*, *hatches*. + .. versionadded:: 3.10 + See Also -------- hist2d : 2D histogram with rectangular bins From 23adb361b96f628293f9d56464ec312921536b93 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Wed, 10 Jul 2024 19:54:37 +0200 Subject: [PATCH 0026/1230] Simplify the stub --- lib/matplotlib/figure.pyi | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi index 91196f6add8e..13e2adfe1b69 100644 --- a/lib/matplotlib/figure.pyi +++ b/lib/matplotlib/figure.pyi @@ -92,34 +92,6 @@ class FigureBase(Artist): @overload def add_subplot(self, **kwargs) -> Axes: ... @overload - def subplots( - self, - nrows: Literal[1] = ..., - ncols: Literal[1] = ..., - *, - sharex: bool | Literal["none", "all", "row", "col"] = ..., - sharey: bool | Literal["none", "all", "row", "col"] = ..., - squeeze: Literal[True] = ..., - width_ratios: Sequence[float] | None = ..., - height_ratios: Sequence[float] | None = ..., - subplot_kw: dict[str, Any] | None = ..., - gridspec_kw: dict[str, Any] | None = ..., - ) -> Axes: ... - @overload - def subplots( - self, - nrows: int = ..., - ncols: int = ..., - *, - sharex: bool | Literal["none", "all", "row", "col"] = ..., - sharey: bool | Literal["none", "all", "row", "col"] = ..., - squeeze: Literal[True], - width_ratios: Sequence[float] | None = ..., - height_ratios: Sequence[float] | None = ..., - subplot_kw: dict[str, Any] | None = ..., - gridspec_kw: dict[str, Any] | None = ..., - ) -> np.ndarray: ... # TODO numpy/numpy#24738 - @overload def subplots( self, nrows: int = ..., @@ -146,7 +118,7 @@ class FigureBase(Artist): height_ratios: Sequence[float] | None = ..., subplot_kw: dict[str, Any] | None = ..., gridspec_kw: dict[str, Any] | None = ..., - ) -> Axes | np.ndarray: ... + ) -> Any: ... def delaxes(self, ax: Axes) -> None: ... def clear(self, keep_observers: bool = ...) -> None: ... def clf(self, keep_observers: bool = ...) -> None: ... From 64cac364f678c5d34e1b12da6a02c231b8eb2f7a Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Wed, 10 Jul 2024 21:58:50 +0200 Subject: [PATCH 0027/1230] Simplify the code --- lib/matplotlib/pyplot.py | 51 ---------------------------------------- 1 file changed, 51 deletions(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index f196a8e9d440..b0ffe23efeea 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -1562,40 +1562,6 @@ def subplot(*args, **kwargs) -> Axes: return ax -@overload -def subplots( - nrows: Literal[1] = ..., - ncols: Literal[1] = ..., - *, - sharex: bool | Literal["none", "all", "row", "col"] = ..., - sharey: bool | Literal["none", "all", "row", "col"] = ..., - squeeze: Literal[True] = ..., - width_ratios: Sequence[float] | None = ..., - height_ratios: Sequence[float] | None = ..., - subplot_kw: dict[str, Any] | None = ..., - gridspec_kw: dict[str, Any] | None = ..., - **fig_kw -) -> tuple[Figure, Axes]: - ... - - -@overload -def subplots( - nrows: int = ..., - ncols: int = ..., - *, - sharex: bool | Literal["none", "all", "row", "col"] = ..., - sharey: bool | Literal["none", "all", "row", "col"] = ..., - squeeze: Literal[True] = ..., - width_ratios: Sequence[float] | None = ..., - height_ratios: Sequence[float] | None = ..., - subplot_kw: dict[str, Any] | None = ..., - gridspec_kw: dict[str, Any] | None = ..., - **fig_kw -) -> tuple[Figure, np.ndarray]: # TODO numpy/numpy#24738 - ... - - @overload def subplots( nrows: int = ..., @@ -1613,23 +1579,6 @@ def subplots( ... -@overload -def subplots( - nrows: int = ..., - ncols: int = ..., - *, - sharex: bool | Literal["none", "all", "row", "col"] = ..., - sharey: bool | Literal["none", "all", "row", "col"] = ..., - squeeze: bool = ..., - width_ratios: Sequence[float] | None = ..., - height_ratios: Sequence[float] | None = ..., - subplot_kw: dict[str, Any] | None = ..., - gridspec_kw: dict[str, Any] | None = ..., - **fig_kw -) -> tuple[Figure, Axes | np.ndarray]: - ... - - def subplots( nrows: int = 1, ncols: int = 1, *, sharex: bool | Literal["none", "all", "row", "col"] = False, From c7a128eec1123866183d412947270ab2c2b11ebc Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Wed, 10 Jul 2024 22:06:48 +0200 Subject: [PATCH 0028/1230] Code needs two overloads --- lib/matplotlib/pyplot.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index b0ffe23efeea..9660f17ddac1 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -1562,6 +1562,23 @@ def subplot(*args, **kwargs) -> Axes: return ax +@overload +def subplots( + nrows: int = ..., + ncols: int = ..., + *, + sharex: bool | Literal["none", "all", "row", "col"] = ..., + sharey: bool | Literal["none", "all", "row", "col"] = ..., + squeeze: Literal[True], + width_ratios: Sequence[float] | None = ..., + height_ratios: Sequence[float] | None = ..., + subplot_kw: dict[str, Any] | None = ..., + gridspec_kw: dict[str, Any] | None = ..., + **fig_kw +) -> tuple[Figure, Any]: + ... + + @overload def subplots( nrows: int = ..., From 4242506cd51bb635af41d9fb10a08469bca66d46 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Thu, 11 Jul 2024 10:15:14 +0200 Subject: [PATCH 0029/1230] Avoid union for dynamic type hints --- lib/matplotlib/figure.pyi | 14 ++++++++++++++ lib/matplotlib/pyplot.py | 25 +++++++++++++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi index 13e2adfe1b69..c31f90b4b2a8 100644 --- a/lib/matplotlib/figure.pyi +++ b/lib/matplotlib/figure.pyi @@ -92,6 +92,20 @@ class FigureBase(Artist): @overload def add_subplot(self, **kwargs) -> Axes: ... @overload + def subplots( + self, + nrows: Literal[1] = ..., + ncols: Literal[1] = ..., + *, + sharex: bool | Literal["none", "all", "row", "col"] = ..., + sharey: bool | Literal["none", "all", "row", "col"] = ..., + squeeze: Literal[True] = ..., + width_ratios: Sequence[float] | None = ..., + height_ratios: Sequence[float] | None = ..., + subplot_kw: dict[str, Any] | None = ..., + gridspec_kw: dict[str, Any] | None = ..., + ) -> Axes: ... + @overload def subplots( self, nrows: int = ..., diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 9660f17ddac1..9587850173d6 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -1564,18 +1564,18 @@ def subplot(*args, **kwargs) -> Axes: @overload def subplots( - nrows: int = ..., - ncols: int = ..., + nrows: Literal[1] = ..., + ncols: Literal[1] = ..., *, sharex: bool | Literal["none", "all", "row", "col"] = ..., sharey: bool | Literal["none", "all", "row", "col"] = ..., - squeeze: Literal[True], + squeeze: Literal[True] = ..., width_ratios: Sequence[float] | None = ..., height_ratios: Sequence[float] | None = ..., subplot_kw: dict[str, Any] | None = ..., gridspec_kw: dict[str, Any] | None = ..., **fig_kw -) -> tuple[Figure, Any]: +) -> tuple[Figure, Axes]: ... @@ -1596,6 +1596,23 @@ def subplots( ... +@overload +def subplots( + nrows: int = ..., + ncols: int = ..., + *, + sharex: bool | Literal["none", "all", "row", "col"] = ..., + sharey: bool | Literal["none", "all", "row", "col"] = ..., + squeeze: bool = ..., + width_ratios: Sequence[float] | None = ..., + height_ratios: Sequence[float] | None = ..., + subplot_kw: dict[str, Any] | None = ..., + gridspec_kw: dict[str, Any] | None = ..., + **fig_kw +) -> tuple[Figure, Any]: + ... + + def subplots( nrows: int = 1, ncols: int = 1, *, sharex: bool | Literal["none", "all", "row", "col"] = False, From 3d1b29b738f5ffb2b83f87ca03f30acd308e30c1 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 12 Jul 2024 14:14:47 -0400 Subject: [PATCH 0030/1230] Backport PR #28541: MNT: be more careful about disk I/O failures when writing font cache --- lib/matplotlib/font_manager.py | 8 ++++---- lib/matplotlib/tests/test_font_manager.py | 24 ++++++++++++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 813bee6eb623..d9560ec0cc0f 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -965,11 +965,11 @@ def json_dump(data, filename): This function temporarily locks the output file to prevent multiple processes from overwriting one another's output. """ - with cbook._lock_path(filename), open(filename, 'w') as fh: - try: + try: + with cbook._lock_path(filename), open(filename, 'w') as fh: json.dump(data, fh, cls=_JSONEncoder, indent=2) - except OSError as e: - _log.warning('Could not save font_manager cache %s', e) + except OSError as e: + _log.warning('Could not save font_manager cache %s', e) def json_load(filename): diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 9563e4bf0869..776af16eeaaf 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -16,7 +16,7 @@ json_dump, json_load, get_font, is_opentype_cff_font, MSUserFontDirectories, _get_fontconfig_fonts, ttfFontProperty) from matplotlib import cbook, ft2font, pyplot as plt, rc_context, figure as mfigure -from matplotlib.testing import subprocess_run_helper +from matplotlib.testing import subprocess_run_helper, subprocess_run_for_testing has_fclist = shutil.which('fc-list') is not None @@ -280,6 +280,28 @@ def test_fontcache_thread_safe(): subprocess_run_helper(_test_threading, timeout=10) +def test_lockfilefailure(tmp_path): + # The logic here: + # 1. get a temp directory from pytest + # 2. import matplotlib which makes sure it exists + # 3. get the cache dir (where we check it is writable) + # 4. make it not writable + # 5. try to write into it via font manager + proc = subprocess_run_for_testing( + [ + sys.executable, + "-c", + "import matplotlib;" + "import os;" + "p = matplotlib.get_cachedir();" + "os.chmod(p, 0o555);" + "import matplotlib.font_manager;" + ], + env={**os.environ, 'MPLCONFIGDIR': str(tmp_path)}, + check=True + ) + + def test_fontentry_dataclass(): fontent = FontEntry(name='font-name') From 8ede65d3a5a0b9f8047f175b632201cb113a151a Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Sat, 13 Jul 2024 12:06:20 -0500 Subject: [PATCH 0031/1230] Backport PR #28526: Bump pypa/cibuildwheel from 2.19.1 to 2.19.2 in the actions group --- .github/workflows/cibuildwheel.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index a4c0c0781813..050ff16cfbbd 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -135,7 +135,7 @@ jobs: path: dist/ - name: Build wheels for CPython 3.12 - uses: pypa/cibuildwheel@932529cab190fafca8c735a551657247fa8f8eaf # v2.19.1 + uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -143,7 +143,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.11 - uses: pypa/cibuildwheel@932529cab190fafca8c735a551657247fa8f8eaf # v2.19.1 + uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -151,7 +151,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.10 - uses: pypa/cibuildwheel@932529cab190fafca8c735a551657247fa8f8eaf # v2.19.1 + uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -159,7 +159,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.9 - uses: pypa/cibuildwheel@932529cab190fafca8c735a551657247fa8f8eaf # v2.19.1 + uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -167,7 +167,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for PyPy - uses: pypa/cibuildwheel@932529cab190fafca8c735a551657247fa8f8eaf # v2.19.1 + uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: From fadfe844470759dfb77f3589c9b2e9d57c0587b8 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 13 Jul 2024 11:20:24 -0700 Subject: [PATCH 0032/1230] Backport PR #28534: [BLD] Fix WSL build warning --- src/_image_resample.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_image_resample.h b/src/_image_resample.h index 745fe9f10cd7..a6404092ea2d 100644 --- a/src/_image_resample.h +++ b/src/_image_resample.h @@ -500,7 +500,7 @@ typedef enum { // T is rgba if and only if it has an T::r field. template struct is_grayscale : std::true_type {}; -template struct is_grayscale : std::false_type {}; +template struct is_grayscale> : std::false_type {}; template From 73df1322432503d6c4e9f6a6d70ec9d936f91a46 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 14 Jul 2024 08:52:46 +0200 Subject: [PATCH 0033/1230] Backport PR #28571: DOC: Add version directive to hatch parameter in stackplot --- lib/matplotlib/stackplot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/matplotlib/stackplot.py b/lib/matplotlib/stackplot.py index dd579bcd5877..43da57c25da5 100644 --- a/lib/matplotlib/stackplot.py +++ b/lib/matplotlib/stackplot.py @@ -64,6 +64,9 @@ def stackplot(axes, x, *args, of provided *y*, in which case the styles will repeat from the beginning. + .. versionadded:: 3.9 + Support for list input + data : indexable object, optional DATA_PARAMETER_PLACEHOLDER From 43ce57d8155fd3839967a5f0439bfc22f1b52c18 Mon Sep 17 00:00:00 2001 From: Pranav Date: Sun, 14 Jul 2024 13:04:56 +0530 Subject: [PATCH 0034/1230] Made suggested changes --- .../next_whats_new/histogram_vectorized_parameters.rst | 4 ++-- galleries/examples/statistics/histogram_multihist.py | 10 +++++----- lib/matplotlib/axes/_axes.py | 3 ++- lib/matplotlib/tests/test_axes.py | 8 ++++---- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/doc/users/next_whats_new/histogram_vectorized_parameters.rst b/doc/users/next_whats_new/histogram_vectorized_parameters.rst index f4b6b38e1ce6..7b9c04e71739 100644 --- a/doc/users/next_whats_new/histogram_vectorized_parameters.rst +++ b/doc/users/next_whats_new/histogram_vectorized_parameters.rst @@ -1,7 +1,7 @@ Vectorized ``hist`` style parameters ------------------------------------ -The parameters ``hatch``, ``edgecolor``, ``facecolor``, ``linewidth`` and ``linestyle`` +The parameters *hatch*, *edgecolor*, *facecolor*, *linewidth* and *linestyle* of the `~matplotlib.axes.Axes.hist` method are now vectorized. This means that you can pass in individual parameters for each histogram when the input *x* has multiple datasets. @@ -9,7 +9,7 @@ when the input *x* has multiple datasets. .. plot:: :include-source: true - :alt: Four charts, each displaying stacked histograms of three Poisson distributions. Each chart differentiates the histograms using various parameters: ax1 uses different linewidths, ax2 uses different hatches, ax3 uses different edgecolors, and ax4 uses different facecolors. Each histogram in ax1 and ax3 also has a different edgecolor. + :alt: Four charts, each displaying stacked histograms of three Poisson distributions. Each chart differentiates the histograms using various parameters: top left uses different linewidths, top right uses different hatches, bottom left uses different edgecolors, and bottom right uses different facecolors. Each histogram on the left side also has a different edgecolor. import matplotlib.pyplot as plt import numpy as np diff --git a/galleries/examples/statistics/histogram_multihist.py b/galleries/examples/statistics/histogram_multihist.py index 63cfde06c053..b9a9c5f0bf26 100644 --- a/galleries/examples/statistics/histogram_multihist.py +++ b/galleries/examples/statistics/histogram_multihist.py @@ -61,7 +61,7 @@ # # # edgecolor -# ........................... +# ......... fig, ax = plt.subplots() @@ -76,7 +76,7 @@ # %% # facecolor -# ........................... +# ......... fig, ax = plt.subplots() @@ -90,7 +90,7 @@ # %% # hatch -# ....................... +# ..... fig, ax = plt.subplots() @@ -104,7 +104,7 @@ # %% # linewidth -# .......................... +# ......... fig, ax = plt.subplots() @@ -120,7 +120,7 @@ # %% # linestyle -# .......................... +# ......... fig, ax = plt.subplots() diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 4721b26980d4..07393b1028d2 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6940,9 +6940,10 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, `~matplotlib.patches.Patch` properties. The following properties additionally accept a sequence of values corresponding to the datasets in *x*: - *edgecolors*, *facecolors*, *linewidths*, *linestyles*, *hatches*. + *edgecolors*, *facecolors*, *lines*, *linestyles*, *hatches*. .. versionadded:: 3.10 + Allowing sequences of values in above listed Patch properties. See Also -------- diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 56c3d53a617c..b1f97b3f855f 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4627,6 +4627,10 @@ def test_hist_vectorized_params(fig_test, fig_ref, kwargs): @pytest.mark.parametrize('kwargs, patch_face, patch_edge', + # 'C0'(blue) stands for the first color of the + # default color cycle as well as the patch.facecolor rcParam + # When the expected edgecolor is 'k'(black), + # it corresponds to the patch.edgecolor rcParam [({'histtype': 'stepfilled', 'color': 'r', 'facecolor': 'y', 'edgecolor': 'g'}, 'y', 'g'), ({'histtype': 'step', 'color': 'r', @@ -4653,10 +4657,6 @@ def test_hist_vectorized_params(fig_test, fig_ref, kwargs): ({'histtype': 'step'}, ('C0', 0), 'C0')]) def test_hist_color_semantics(kwargs, patch_face, patch_edge): _, _, patches = plt.figure().subplots().hist([1, 2, 3], **kwargs) - # 'C0'(blue) stands for the first color of the default color cycle - # as well as the patch.facecolor rcParam - # When the expected edgecolor is 'k'(black), it corresponds to the - # patch.edgecolor rcParam assert all(mcolors.same_color([p.get_facecolor(), p.get_edgecolor()], [patch_face, patch_edge]) for p in patches) From 93fde9e0c2f4b0e31b4891e5e63c577dff68ea93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 19:41:11 +0000 Subject: [PATCH 0035/1230] Bump actions/attest-build-provenance in the actions group Bumps the actions group with 1 update: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance). Updates `actions/attest-build-provenance` from 1.3.2 to 1.3.3 - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/bdd51370e0416ac948727f861e03c2f05d32d78e...5e9cb68e95676991667494a6a4e59b8a2f13e1d0) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/cibuildwheel.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 50adc91980de..aeb502cf7587 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -195,7 +195,7 @@ jobs: run: ls dist - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 with: subject-path: dist/matplotlib-* From c427a48841f50e9652f24a9f25872d498e7efe6e Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 11 Jul 2024 17:14:28 -0400 Subject: [PATCH 0036/1230] CI: adjust pins in mypy GHA job - pin minimum mypy to not complain about name of positional-only - drop upper pin --- ci/mypy-stubtest-allowlist.txt | 3 --- requirements/testing/mypy.txt | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index d6a0f373048d..06261a543f99 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -35,9 +35,6 @@ matplotlib.figure.Figure.set_constrained_layout matplotlib.figure.Figure.set_constrained_layout_pads matplotlib.figure.Figure.set_tight_layout -# positional-only argument name lacking leading underscores -matplotlib.axes._base._AxesBase.axis - # Maybe should be abstractmethods, required for subclasses, stubs define once matplotlib.tri.*TriInterpolator.__call__ matplotlib.tri.*TriInterpolator.gradient diff --git a/requirements/testing/mypy.txt b/requirements/testing/mypy.txt index 9e3738556a8f..4fec6a8c000f 100644 --- a/requirements/testing/mypy.txt +++ b/requirements/testing/mypy.txt @@ -1,7 +1,7 @@ # Extra pip requirements for the GitHub Actions mypy build -mypy==1.1.1 -typing-extensions>=4.1,<5 +mypy>=1.9 +typing-extensions>=4.1 # Extra stubs distributed separately from the main pypi package pandas-stubs From ecc7036ef7eb16dd114f5b3f1fc5c02fb84b8d8e Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 11 Jul 2024 20:00:52 -0400 Subject: [PATCH 0037/1230] FIX: add missing test files to meson.build --- lib/matplotlib/tests/meson.build | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/meson.build b/lib/matplotlib/tests/meson.build index 148a40f11a2f..107066636f31 100644 --- a/lib/matplotlib/tests/meson.build +++ b/lib/matplotlib/tests/meson.build @@ -2,8 +2,8 @@ python_sources = [ '__init__.py', 'conftest.py', 'test_afm.py', - 'test_agg_filter.py', 'test_agg.py', + 'test_agg_filter.py', 'test_animation.py', 'test_api.py', 'test_arrow_patches.py', @@ -13,20 +13,23 @@ python_sources = [ 'test_backend_bases.py', 'test_backend_cairo.py', 'test_backend_gtk3.py', + 'test_backend_inline.py', 'test_backend_macosx.py', 'test_backend_nbagg.py', 'test_backend_pdf.py', 'test_backend_pgf.py', 'test_backend_ps.py', 'test_backend_qt.py', - 'test_backends_interactive.py', + 'test_backend_registry.py', 'test_backend_svg.py', 'test_backend_template.py', 'test_backend_tk.py', 'test_backend_tools.py', 'test_backend_webagg.py', + 'test_backends_interactive.py', 'test_basic.py', 'test_bbox_tight.py', + 'test_bezier.py', 'test_category.py', 'test_cbook.py', 'test_collections.py', @@ -43,8 +46,8 @@ python_sources = [ 'test_doc.py', 'test_dviread.py', 'test_figure.py', - 'test_fontconfig_pattern.py', 'test_font_manager.py', + 'test_fontconfig_pattern.py', 'test_ft2font.py', 'test_getattr.py', 'test_gridspec.py', @@ -57,8 +60,8 @@ python_sources = [ 'test_mlab.py', 'test_offsetbox.py', 'test_patches.py', - 'test_patheffects.py', 'test_path.py', + 'test_patheffects.py', 'test_pickle.py', 'test_png.py', 'test_polar.py', @@ -78,8 +81,8 @@ python_sources = [ 'test_table.py', 'test_testing.py', 'test_texmanager.py', - 'test_textpath.py', 'test_text.py', + 'test_textpath.py', 'test_ticker.py', 'test_tightlayout.py', 'test_transforms.py', From 49f3ad5f6e2e478479d7020689f122d3b0b43095 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 11 Jul 2024 20:01:23 -0400 Subject: [PATCH 0038/1230] TST: fix tox config --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 00ea746c8923..306d9140a8fe 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ setenv = PIP_ISOLATED = 1 usedevelop = True commands = - pytest --pyargs matplotlib {posargs} + pytest --pyargs matplotlib.tests {posargs} deps = pytest From c7eb84afcf690010631f495af7dc5acfd44cb9f9 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 18 Apr 2024 10:49:47 +0200 Subject: [PATCH 0039/1230] Disable clipping in Agg resamplers. I chose to add macro guards directly in the agg source as that seemed easier than copy-pasting the whole code. I chose to bump the tolerance on test_rgba_antialias (as that's also getting bumped by the interpolation_stage antialias PR) and to test_pngsuite (which is not particularly relevant), and to update the other baselines. --- .../include/agg_span_image_filter_gray.h | 8 ++ lib/matplotlib/image.py | 101 ++-------------- .../imshow_masked_interpolation.png | Bin 31943 -> 31942 bytes .../imshow_masked_interpolation.svg | 112 +++++++++--------- .../test_image/rotate_image.png | Bin 24194 -> 24068 bytes lib/matplotlib/tests/test_image.py | 5 +- lib/matplotlib/tests/test_png.py | 2 +- src/_image_resample.h | 2 + 8 files changed, 78 insertions(+), 152 deletions(-) diff --git a/extern/agg24-svn/include/agg_span_image_filter_gray.h b/extern/agg24-svn/include/agg_span_image_filter_gray.h index e2c688e004cb..7ca583af724d 100644 --- a/extern/agg24-svn/include/agg_span_image_filter_gray.h +++ b/extern/agg24-svn/include/agg_span_image_filter_gray.h @@ -397,7 +397,9 @@ namespace agg fg += weight * *fg_ptr; fg >>= image_filter_shift; +#ifndef MPL_DISABLE_AGG_GRAY_CLIPPING if(fg > color_type::full_value()) fg = color_type::full_value(); +#endif span->v = (value_type)fg; span->a = color_type::full_value(); @@ -491,8 +493,10 @@ namespace agg } fg = color_type::downshift(fg, image_filter_shift); +#ifndef MPL_DISABLE_AGG_GRAY_CLIPPING if(fg < 0) fg = 0; if(fg > color_type::full_value()) fg = color_type::full_value(); +#endif span->v = (value_type)fg; span->a = color_type::full_value(); @@ -593,8 +597,10 @@ namespace agg } fg /= total_weight; +#ifndef MPL_DISABLE_AGG_GRAY_CLIPPING if(fg < 0) fg = 0; if(fg > color_type::full_value()) fg = color_type::full_value(); +#endif span->v = (value_type)fg; span->a = color_type::full_value(); @@ -701,8 +707,10 @@ namespace agg } fg /= total_weight; +#ifndef MPL_DISABLE_AGG_GRAY_CLIPPING if(fg < 0) fg = 0; if(fg > color_type::full_value()) fg = color_type::full_value(); +#endif span->v = (value_type)fg; span->a = color_type::full_value(); diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 3b4dd4c75b5d..95994201b94e 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -457,93 +457,21 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, # input data is not going to match the size on the screen so we # have to resample to the correct number of pixels - # TODO slice input array first - a_min = A.min() - a_max = A.max() - if a_min is np.ma.masked: # All masked; values don't matter. - a_min, a_max = np.int32(0), np.int32(1) if A.dtype.kind == 'f': # Float dtype: scale to same dtype. - scaled_dtype = np.dtype( - np.float64 if A.dtype.itemsize > 4 else np.float32) + scaled_dtype = np.dtype("f8" if A.dtype.itemsize > 4 else "f4") if scaled_dtype.itemsize < A.dtype.itemsize: _api.warn_external(f"Casting input data from {A.dtype}" f" to {scaled_dtype} for imshow.") else: # Int dtype, likely. + # TODO slice input array first # Scale to appropriately sized float: use float32 if the # dynamic range is small, to limit the memory footprint. - da = a_max.astype(np.float64) - a_min.astype(np.float64) - scaled_dtype = np.float64 if da > 1e8 else np.float32 - - # Scale the input data to [.1, .9]. The Agg interpolators clip - # to [0, 1] internally, and we use a smaller input scale to - # identify the interpolated points that need to be flagged as - # over/under. This may introduce numeric instabilities in very - # broadly scaled data. - - # Always copy, and don't allow array subtypes. - A_scaled = np.array(A, dtype=scaled_dtype) - # Clip scaled data around norm if necessary. This is necessary - # for big numbers at the edge of float64's ability to represent - # changes. Applying a norm first would be good, but ruins the - # interpolation of over numbers. - self.norm.autoscale_None(A) - dv = np.float64(self.norm.vmax) - np.float64(self.norm.vmin) - vmid = np.float64(self.norm.vmin) + dv / 2 - fact = 1e7 if scaled_dtype == np.float64 else 1e4 - newmin = vmid - dv * fact - if newmin < a_min: - newmin = None - else: - a_min = np.float64(newmin) - newmax = vmid + dv * fact - if newmax > a_max: - newmax = None - else: - a_max = np.float64(newmax) - if newmax is not None or newmin is not None: - np.clip(A_scaled, newmin, newmax, out=A_scaled) - - # Rescale the raw data to [offset, 1-offset] so that the - # resampling code will run cleanly. Using dyadic numbers here - # could reduce the error, but would not fully eliminate it and - # breaks a number of tests (due to the slightly different - # error bouncing some pixels across a boundary in the (very - # quantized) colormapping step). - offset = .1 - frac = .8 - # Run vmin/vmax through the same rescaling as the raw data; - # otherwise, data values close or equal to the boundaries can - # end up on the wrong side due to floating point error. - vmin, vmax = self.norm.vmin, self.norm.vmax - if vmin is np.ma.masked: - vmin, vmax = a_min, a_max - vrange = np.array([vmin, vmax], dtype=scaled_dtype) - - A_scaled -= a_min - vrange -= a_min - # .item() handles a_min/a_max being ndarray subclasses. - a_min = a_min.astype(scaled_dtype).item() - a_max = a_max.astype(scaled_dtype).item() - - if a_min != a_max: - A_scaled /= ((a_max - a_min) / frac) - vrange /= ((a_max - a_min) / frac) - A_scaled += offset - vrange += offset + da = A.max().astype("f8") - A.min().astype("f8") + scaled_dtype = "f8" if da > 1e8 else "f4" + # resample the input data to the correct resolution and shape - A_resampled = _resample(self, A_scaled, out_shape, t) - del A_scaled # Make sure we don't use A_scaled anymore! - # Un-scale the resampled data to approximately the original - # range. Things that interpolated to outside the original range - # will still be outside, but possibly clipped in the case of - # higher order interpolation + drastically changing data. - A_resampled -= offset - vrange -= offset - if a_min != a_max: - A_resampled *= ((a_max - a_min) / frac) - vrange *= ((a_max - a_min) / frac) - A_resampled += a_min - vrange += a_min + A_resampled = _resample(self, A.astype(scaled_dtype), out_shape, t) + # if using NoNorm, cast back to the original datatype if isinstance(self.norm, mcolors.NoNorm): A_resampled = A_resampled.astype(A.dtype) @@ -564,21 +492,10 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, # Apply the pixel-by-pixel alpha values if present alpha = self.get_alpha() if alpha is not None and np.ndim(alpha) > 0: - out_alpha *= _resample(self, alpha, out_shape, - t, resample=True) + out_alpha *= _resample(self, alpha, out_shape, t, resample=True) # mask and run through the norm resampled_masked = np.ma.masked_array(A_resampled, out_mask) - # we have re-set the vmin/vmax to account for small errors - # that may have moved input values in/out of range - s_vmin, s_vmax = vrange - if isinstance(self.norm, mcolors.LogNorm) and s_vmin <= 0: - # Don't give 0 or negative values to LogNorm - s_vmin = np.finfo(scaled_dtype).eps - # Block the norm from sending an update signal during the - # temporary vmin/vmax change - with self.norm.callbacks.blocked(), \ - cbook._setattr_cm(self.norm, vmin=s_vmin, vmax=s_vmax): - output = self.norm(resampled_masked) + output = self.norm(resampled_masked) else: if A.ndim == 2: # interpolation_stage = 'rgba' self.norm.autoscale_None(A) diff --git a/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.png b/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.png index 72918a27fbc18ff25abd842c1b35e4696cbedeb1..9e68784cff4f2f8badc8917728de2b609e007f08 100644 GIT binary patch delta 7498 zcmY+I_dDBd*v1okk7~7)N9|Ip#EiXH)NYAcl&Dp+<~L?lQAL&5EA|ettJElJg{Bp= ztu58sqj){fd%Qoq_djso$90|8=Q_`O@B}AlA?EB7cwZVKbDg5^6efk!0IE0qC7d4u<^x#^@eslOk-!L>JZ0W7 z`HQgPqgTHZq#0QXguA|!)Jesol^RTO>Tc}%q2K1Y(%#GRem%NzaLW;@WhMD}ZKhRd zPghMv8+29xhG<>0d#$u|%&8H7fw#RGzN72Z!$#uu>7SgtBqo*~4@vrL+0YVh9r9Up z*bj0OVccqvIwQ(LnkZ2Sa>eM;7WpVOH_ zS~pisz$n7%u^jcC=M+As6rQ*+SRs0MOW+uV`tqP@%sPaD&5U#?u(CY+!x15?xZD-% zCBGqF97i#c?z@kC`@BBciNQ~Sxz&XLR7dyEH}<4ZvVB%8VK4Vm9#KfufE90L#E3v< zlBa%p(A{NQu+72;qT)Ub89+Zr=b?gs>W@=LQA{wc1eO^MBJvNm^nL`PZY5bETD1Mk zI-bf=YictO_@vw)lidt4R3Q4RQ~XjiqfVlra`FwQoPD6Eu1d`xg35CM8xW*r?=+nr)kk<7ts2s(#F z##PQHhRWSWkFp<6rVX1Db?u$qK=jX|gY-Az@?(FqSIs1!p6JaVmMoFA#F6Im`%0%p z%lyh16`quOnYdhVeqOc6KOPEl%{_1o;G|;GbqV znOL02X*Xiuy#SrC)h6L3&-p-*{7R%YK##$-v75vXi>zH~NZR;}EhUKp_E{s!3M%ZZgQq$Xo7N;qv!dbcR(DPgokK`X7U_ z4nGcWzsH>uUZ1w9_t1+?*^J+!!D~JLs_$}b^cG?e7XrjYCPLky=XF7tk%~5B_BC^g zzUvSzWxDL-Kjvi?Hu#?An&W_miJ*n?X7Zd=B)qZnVu!>M7>RXjXEG|-!8T1L{e4|@ znSB^83XALx+&o3kM&ScFZuTN}%8#Ssx*Sc;y2moS<2zrMBGt`K-~aP(8#Izc#_kYh zLsZ%+Z!#HqE8sW1#B~^cwy)iZLz)@+ji|p7PFkw6=s8 zOOHVnsSSZBM;zido7=4H2f8T;#?oJO2g(@n1bfYj>&uzNw^6UtqZ~w}v0N7wAYdE*x1K1&(Ob(-&|z?m?Fa=DrO#s1sO{ zX{(ldDxplGgU~%!YB_#dqy9LjVkc{)UE2Bz3t-Z~JVt8biuzuV$n$ZseT5_$xgw`J zLQoH~1@S-7DdTG=Xy(f54YNnKrLeDIc4a26|DLUPYp=J?tM&S{sa;<(&X|_|HVR!! z{`WZB`|bO$t9romLWRZn&Vb3u=_W&)NIRMVxsvPJY4DT@fgvuZShv2MZVT(w(C`7S%_ zQHw`sm>CIws{=C^I0unk=;0NbuGuvlvPofYZ|oI?kkk^hOCLzFfdNQ$>*XpGjYC6vI`yjJ-i`C370dAAZfX1-UF(Gh15C{(O9VPy{yDAa#qM z22m6tdpER+Hnc?1rWQ3sv*5jvP0#fVa_mp7>h|ai+2hx-U`Q7=->|B7nD+AXsu`n_ z2gk4Rh|3P=V3vB(Q%a>iKOw|=bwUohA@`lR8NdaniZ5mT_#xOP@UqOuXoH?fO1hPY z=R1@K7P?u?;yEEI_>0xdfwD)4R*t{!GUM;W#) zgqQycmW(Q1KfAEfU&a(GCzEp6eY~-E86#SHxpL0Q5lVmx&4=xMJHpKlJW$MQU$|_8 zO#<}h7>?FtS?V^+&`?{}uF(p#-GYCN^3^gJvV4%M*3PMNbUTNOZGY$3&r@7+CtWn& zow^7Sa1l8BM%^2^*Zb0ruOa(N-bH7ofNfmj1{f~Oq&1(S;pFDiHAfy1P7)tAba z&-PaSJesRjIJJCDM!}KjB|T|Rd-oYK05wv@G^U?p7Q&Kaac|Pb?x01O2);+W4w+%Y zIO(j?J?oPV7!pv!;@$km4%)?X5|C21kzHX7aQ(X2OsmLM!~Tv1yArvUbuTLlLUl4) z!+Gg`+^wCR6R5WN5RRJ3GkP9XQPk;9-jaUF@i>qiC1rsT&bXO-{UY=`_KhB}d6xV1 znZFhkX+n`xN$=WD;z;nRYB>rj?Z~8FT>6?HG^%iSO5g+aVv6P>p7LLCeL~*`doV62 zxq=9j(3`4kOlGW0A+8q_y9`mXPi3slEbq`P1vM_{u(f8a+t$QxN$?nsgM_8ua)+=4 z7&g>9z7-WeolP#sR2Ff9w}J5dFEA=LBZ~0$(qm46FBn+W0eMG0H61!yX0a+Dxv#3FuAhIjR zToYs~l72B8qj&J$WnQZICo}5dF63Sa>FUxIpGtnns=k{=NIuEL5wLYHHOy(oDfP5_ z8?`llpI_W~(tkOF3}t2+YQj02EIT8`(1l4oX%a0fCIZ{XS=|k=z9Zo;@SKXuDpu5-8o= zQ8km7?W%AhOvCXVa9+9+P0uc5J?C?3?MGe+ednT(m75{F-kM)reMT9FZs!d*M-8wY zn|*sMIWgUgd}7gKg(y~*CBOWoUp75oxgQYl-Q#4nV3AblmTF(%Y$eFxp2zf>2UXeZ zONRDBCoZogSn#hi-fuKSEAwL9c#TjMHg|T_36UW%@m35l{JfXTceN*}z4`BYzqomh zm^%zKHt>yCZmlGC>g=EDZAAX&fcW5~Sf6Pe?T&3{IgiuP_~*l8f3g0(Qxm6Xd_t#C zool_oe4+#Mns3s4>@X{bPcypyI%NLK5XP|&Qtw6mvKTQP^CP{pCIB05sCf+UA|65| zUMJE;Qx5>+Uvmz0muT0WhWDM2Rn#cDt3(73QP)ESBm|0 zhP=q!yyOfE&pfv$%|hD+KRGeFb_E5^EY6B$s=Q@d#qqkc^-NVV0~xnc3GkDo${+m^ zz(*OTlu?gC1?XG#U<3zp{4sJmGj0%Y;3D)39}hSiI=TFo7gW)$isEJ;T9`=i*FnAhr02ZiyJGn_UUW# zAX}Ku`M0j`e0Yl=G1{{}-S3q4$%BDBQe7=G3vDN5Bfc4b(*iJG36+~+!h%Z^c0Zv%xFoy8Xx1fwC9I!jcxso}x z5U8kReKT0vyE3H2F~JlC8f&rRSS(3Q%i8MXMpNrPQ@`;x;qPgyR$7yL2XzWUUx`|k z)Ge{g*JUR*Zx8-wyb6)4u@!zlWe{o68gGT|o(Uh_HQRKahJ?dR#IQ{xY8aScYJpRn z`G(Z&wMI0W;+>Qbiwiqol6UK)?7d>Vz7-Zi7Zyj6;sp9rcvS^ZyuQ4@BOT?_8a=@? zJyr9nKcBT?Y8#4Ks1y5Lf&dY_1)*nSf7WhyQ7X>0@`c<8Ar_J~dKZ zT`<#%@5fhhleydvV!Z4G9?i70zve#`Fu(^8EST(+^!a;QIsu@g_UL^EDaJZ7tooSw zhw8WDp61VEB#pTPx>YaDardR3nu=aU$n}3+iKW=i3H+OJQgJK(a3$m+c5!kHF>b+i^0h~7 z(ZQI#jZZ4az@mwnOpfVSqm=-ulZbE_j6QqwG;<^C)3E}20(8xk%$a2`lYS2Q__rn7 zPBj)^S-9t}C!n)9cd{Wu>m|RR*UG-KbpNJTzmL8nG)YXHnx&v(3iAo#LM9;tLB?DM zVV$=a6NNlFoTZsp9iuGV*-QP5%?8Oi2G8G`QrwuLs4I z=v8&8*Dt-x+D{m*4L^S6*E{`I*Lp!Z=_2?u%e5O@S6!rWRbOT*9k75ikH5>i8)*+W z;VLF?Zg95`d=7xdjos)yn`5{W<9eYvve7x%yrcy-=pyv~Rw|0U5Ot(3o?VrcMtYQK zxRpHvR6>9ILDvD%9OZj3vtFTld9l_GSJ*|=o&o$DanC(IG*cdB7yT!i^_QcG%bLgkKYMOu zKML?~2H#|rkS>3mKwO}skES;27a2~qhR#_14%p4Gy&VpfPNGU~Mhkt<=ur5r&BXZM zK#I^X5gvj5Jls#fRF)@x`S=D@c75TCzW35YZOgy-*x$;4&XP#J&)nEkuTN2C4V?-t zxJUGqmbgf__60VzN##s$%1vUyZHx4Eh2c-1>9=amF0Rj3+yi~hd&XKzm@!Cgc^!o@ z^Mo0Y)XvP9%jMP1AI`xDFKW?j%jxv51r6_9p&62apB9gs4Vn`j{z^z7#W5Ur2MI5LQMDIRJEwte=fwb+mjqO_ z5+dF%W@FGJ1b4HzGI8F%v9w=hkXk)aq!=btSQnPItV>jcgni2m#T~A#&4O>)XJu8Z zZfR)w+mue?GlzT8|b!bk$LzP}@q~3MnP&yU;QtC@%*Im6(NgpYW{WAfgS3%ugkRT1!1My3FWG&G5 zg7y(T_N*=UFot#n8pPlE{v2-$;<;m*Z^yeZ>+8wfRltVS$K^><+)7Ng>po0%O7|!O z3U_%S0W}3^BDX5&E_LgNYz<#D!u}%eRhnK5MM?@uLE`AgTbNivvYkBAO~H=I0CX4b zQ|;wka0E+jylycun>_WATE$GfzcYHEI(nu{z~(ujHR3cC`P7Qj?6EXQ`b`v5(l6T{ zqi+OOFN>O5NB@xGgbPg$D=agcTEzmOu=8Nn==|<5{XOBBc;Jn&Kq1GctP7;)8fpec#1Kj~HdeZtyu>~vf6F%1Bs72B- zm?g{URU|ySxaT=-E8ip}9+MQn8y+|x8{YbIDpJd#g2DWr+t|!f`iPJ4yniqCT4DJF z83y+ibxJIs&$lIXSON;$y?Nd}%B@rl(@3~zadQ~W!O9Iei}6RaSIN0~Z(Xy~B45XJ zko%UJ;PdOLvXP*xPeOFJqI$3K?N>C3JidK`PK-9Vv-ALJp74IUJh!SwZ=QI_BE z=Ffk`INz5PhV^BN83{1|3mDa(hl2%?k3dB(2W3r~gDJYax2%I07|S)(Ir^#I#We&P zq=spR7X86S!jW4cCZ>ajqxMyE-Ck$1fZTEInS~;N3)h40@+_r;%C=qB(?KJwNFV5G z-CkRfCi{mlSA|akh%zOS!(s}Q~T z?!L(-Ro}TY*|0nxGp(GkBsTM`e_8yy)yhLP%v@f7{LHR8pcF(t*wyBxOPs!4>_!rO zwcr3#(44BXc2LzPX5?M4(M+7Q#o<5ZHZ5H`ndwSWJW7oCPY+GXGT~tY&cSQ;U$>9b z?C^B7;!{6EPJ;wy4huudM3C;}UmDpIt1eux3~hn+hNitDF203rf7e(4LV6kYwU!{y z^|hjp>gC*U;>ktc`%=%6qavqJv=&uQ1r;DSB7C>TCQ`K2kNe5P0(tmBEg?_sdT;E6 z+_>y7A|wN?rU}0X?}^-M9^9bWzA@5Jx^ErtGBSy(eI8UtAhysiH}@a5^)9`kLgdC3 zAZ{T5QMoJVPQRv4%r?B9ycvg}<|3o}Ut@ z@wa=ms@xqBE;o!2{M)7Sm`WLTplM|cChfIWw%;D)RJEIA@%p}3nLe4qYtE<@POV4Q zwi<1n?3Ur=ISE&KEiD$BNMfhh@nu{Sa>^u5V?2#`(qmFcmM~mGJWfl6f!Nyt?q4+5 z#Foas)XjK#Cu5SQl7eQR4+b(?o6<<L$ zr>5mdN4BZl!lfeb%LPh59Yrsv>9p*l(5g?1-cv^BYq)0NqMCQ>-AqT?Sv(jsSnvM=c~;3x$X$|N7cu`nXJSKal$?)-g4b*^Y>#3?chF-GW436;1xVH zHk+8Ms(^9}i(~0r4E6BVh+t`Zzp_Eul^u9)uWr*HxYFZmD5yTK>oiPou<|-9AM-r8 zxTd?*J|pvj=vo?35$)RI2F-C7_v%v04^ZZ&qC9B0^7}0t~16|NNVJKp!JmA7!vOvacW{xBCvF zm7OM{kJtWseA0Mn;W~Zlbv)D~k;&Bk!~ESe0ikK0K!n(3<&LjYq*9#(<;J0{9NXox zH1AS#lizXwJ>tP*$Xx@Rr(*+l*1uHLr7iRQ5N5zrZsGUhGkUW0c3i*DG6s7ctZsM% z&9UPQfx8TusJtylZ3i+K7gRPZA_=H!RdmL8u57ta{QGqRt9+{kIn5nq&W= z#I5i1{pW`!t}^p*rClr{m<@KJXRK^3*!lJM7zyXhKL630Y0o*-%h|BL}{bzoo*dttp)~dRByIYQ1|n{bhm-D!;Capl z7kB)PSASr4gke8yG?MAz{}2$Pz7-ok)tgG5FtT1JpHhm3da}!au8y9Tk!G#BL)8BO D2Z4L7 delta 7515 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 diff --git a/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.svg b/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.svg index 8123e200c27a..c0385c18467c 100644 --- a/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.svg +++ b/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.svg @@ -6,11 +6,11 @@ - 2023-04-16T19:34:05.748213 + 2024-04-23T11:45:45.434641 image/svg+xml - Matplotlib v3.8.0.dev855+gc9636b5044.d20230417, https://matplotlib.org/ + Matplotlib v3.9.0.dev1543+gdd88cca65b.d20240423, https://matplotlib.org/ @@ -29,167 +29,167 @@ z " style="fill: #ffffff"/> - + +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" id="image766171a396" transform="scale(1 -1) translate(0 -51.12)" x="57.6" y="-54.994689" width="51.12" height="51.12"/> - + +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" id="image680ce2187c" transform="scale(1 -1) translate(0 -51.12)" x="118.820571" y="-54.994689" width="51.12" height="51.12"/> - + +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" id="imageb4935ef911" transform="scale(1 -1) translate(0 -51.12)" x="180.041143" y="-54.994689" width="51.12" height="51.12"/> - + +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" id="image5eba289983" transform="scale(1 -1) translate(0 -51.12)" x="241.261714" y="-54.994689" width="51.12" height="51.12"/> - + +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" id="imagef875872d3f" transform="scale(1 -1) translate(0 -51.12)" x="302.482286" y="-54.994689" width="51.12" height="51.12"/> - + +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" id="imagede6ab4e840" transform="scale(1 -1) translate(0 -51.12)" x="363.702857" y="-54.994689" width="51.12" height="51.12"/> - + +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" id="image6c6c3130be" transform="scale(1 -1) translate(0 -51.12)" x="57.6" y="-148.916571" width="51.12" height="51.12"/> - + +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" id="image3a51447072" transform="scale(1 -1) translate(0 -51.12)" x="118.820571" y="-148.916571" width="51.12" height="51.12"/> - + +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" id="imagedcd3672a2d" transform="scale(1 -1) translate(0 -51.12)" x="180.041143" y="-148.916571" width="51.12" height="51.12"/> - + +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" id="image2174594e8a" transform="scale(1 -1) translate(0 -51.12)" x="241.261714" y="-148.916571" width="51.12" height="51.12"/> - + +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" id="image26eadf9e54" transform="scale(1 -1) translate(0 -51.12)" x="302.482286" y="-148.916571" width="51.12" height="51.12"/> - + +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" id="image5b8918d3b3" transform="scale(1 -1) translate(0 -51.12)" x="363.702857" y="-148.916571" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAADFklEQVR4nO2bMW4TQRSGd9bjSAiJAoHENSgiUSLBERBVDkBJ4TvQRFyAEqWCM9AiIQpOQBCUFFYUR4BCvBSWnHlvPZ/fGHf5/2p35t8349/vfzPrXafTHw+GrkBfHE+6OiYpmfO+wlvFSdW+vohDvNUY16P48Wu81XmdO0n1mdNnuvGQOACJA8hfL++Yhj4t18eTbun5RZ8pVeY64vrrbAzbN+ZeXXMhzmRrnGJMc129T3CQOID87fJetbN3tvLpavrAgtaqZAcbw49PXLZOMX7Q/qvrhCokDkDiAPLpn/vVTr9ckl9LLvJaasV/cO11y4IXr5vKHIDEAeTvv+6aBtp5linp0/jN4dv18YvPR/UYDTvr8Y45xn398J3pm315Vo1BNlPmACQOQOIA0tMPL43pEnnQ1Jz43TXXsdgy2zJmtG768UdcnM0Nh8QB5J8Xt01DMtapX+jtRykftSr/vB63dcvcyI7KHIDEAUgcQD5f3DINxq/oa38e4zJvcOdValucehhb80ZbEKEKiQPIfy+m9V6fupSfZV+DHXcdb2Qr5MIYEEKZA5A4AIkDyP0C3sJxJhyi9SJam7ZwB6gro8qx7d5jzYM7fz0rj0PiACQOIOeFvw8Adun5ES9tPKQYXbehdtBcsD6VvHhdoTqqzAFIHECeNtgKl3KIwVuA2NhtcerEUQw91NsNEgcgcQB5eu5agjVg7N0G7g7jjeLsaW5lffLXKXMAEgeQD87hzrdlp7vzzroecy9WbeAODf8EuvGQOACJA8gHi/r7KS3e/fh+tj5+9PwYYrZs7ethiPvpZGa6Do+ON/K2xVTmACQOID1+8ir8lAuXxL6+07Qxbef+lnZ6qlfwKB1kqzgkDkDiAPL07Ldt4Zdi1ofR+uMxqg17q09FJ3zlLeMrcwASB5An8wvTMPD7tZuPidd1297ZjcXsnCVwSY7PjbYAyhyAxAFIHEAe5me2pY/WFacr1JUUrQE9f1cpWp/8XKLj65fAOCQOIF/N5/XeBlslXK4Lbov9yGZoKze3qAXdZ1LmACQOQOIA/gFzjLJEhG9/CQAAAABJRU5ErkJggg==" id="image3232f1a745" transform="scale(1 -1) translate(0 -51.12)" x="57.6" y="-242.838454" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAABsklEQVR4nI2UMY7UQBBF36/uFUgzntEEJETADVaCG5CTbIiIOQCChCvABQgJCTfaEGlDAsQ1IAEJobFZN4Hb7mrbsyJozfevX69K457RU10kACSGT0NWNFlLAjOqrNngw5CTEcPhkB9OQEaAh1UQVX7UYU8yLcPSqp8kyOwqm3W8OWxmBcr0ESCWdQf2mdjt7+YAFSytQpga07QEJCt1a/eR+fl89YZ2F6bTNUW3TeC4C7Q74/ryNced0TbDOTYBPX7xLlXb3aLLdqeyIraNluGVxpP1GTx2zQrgP7erwcN1jt02LaZMDaPH7RDfF/9ulyaqhxTIzMflsxf77c0JSP2LRMld15KVzwIxbrocKACt6hlw9PI8U0JKxGb7BylhLmyuYQq7Rl83lzES8d7md9U4Dwy6r/xlPWHqAYj3Nz8XhQ9PPvLyy/PBo4RNiUDR788/8errBYEyUG+/PUtjYxgbvVZPcJt6DVSwQE98eOd7hvQEV5gDgnos+8GD6afhgUR8cPYjNxbTXCC4r6NAxiHegyCIj85+DQ/5LQZp+v8M+d2alLUwjJDvj2FYzgQNXf8AXD2vrUvE87wAAAAASUVORK5CYII=" id="imageae18cef05f" transform="matrix(2.550857 0 0 2.550857 118.820571 242.941311)" style="image-rendering:crisp-edges;image-rendering:pixelated" width="20" height="20"/> - + +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" id="image8b367143a2" transform="scale(1 -1) translate(0 -51.12)" x="180.041143" y="-242.838454" width="51.12" height="51.12"/> - + +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" id="image6bd3f8e689" transform="scale(1 -1) translate(0 -51.12)" x="241.261714" y="-242.838454" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAAIhUlEQVR4nOWcTahkxRXH/1W3Xvd8OuNHhhmToOIgKKJZBHEZUCYQ0U2Ii8kiWQSycKWgKCgI4uDGwaULtxIwO0VRGFAXWQRJyCKCiKIZJw7JC5mMb96b19237nFxb99bH+fUvdWvn9MvHhh4XX3qnFO/OnWq6nb3qAtfHSc4UigFTjTTVkDQZWyIuoxlOYZYVwt2C8XonviM1ZVE/SOAAwCFoMwFzUGrbTC6wqBZXcEyHwOvy4HjoAE8OPXp+eMkDpAZiwxDsrF3gIYwzTYVgYKTSDQ3HiVXE2DcHrkkamywJgL9Rjduamz0gbZObI5uYyMHNACYTTKMcjBo4kHUDuN2VpcC8HPdHPAk6LIZzseQk+FmqxrHhlXFG0DcXggBczYkaCyIpUDbmQ2zSaO4U6CnOSgiwNhGFmwphqGwxRiGgwLqzDPb1Yh9Uxp8CyqwyenvddhmM4DTpmI4eMax58AtnGxtGQ47B5RnY4C/HNhtzdEDoRzUE5y+46P2tRLSsjbVFbl3P78T6/Y60a4XZBSDDPuXJ/8m+k/Jm5/fzYNy4jLbtBYpyDtThVvN+kLBHCuu4MvZD/jUF6DwGcFPYq6E5YTLNLNlx71K7oy2MDNlk9Ywqfy+cqZwMSwHShtPAydVRswkPOd4KRZH8qeNO/D824/gPxeP4KYTl0EPCac7AD995xmsXzyC649t4OGNv+OAntZ2GSipbB2qmyPb813as+tPitmy8W7FH+zqjn/48/04//snAQDnAQBnxAD+8oszrd7mi2dx+uEPY7tioZbOLrx+rmyFy4rxZ646cLiqHYK65S1qoOTJ7a98iq2H3DOVAIWbGGFXlAAOkS07liemaTeTKr4+uAqhjN/+iG3vk/fWX8VJezpxGOufmL72HJmQYTaA4Jxz1QpF0rmFuMfwc/THxQMKJkI63ksZIU3YIrLlrRjen9m2JlDooHCdfvbgS/jg3NPZwZw69FvMJyIHipw9OwN11Y56y4iZMssqNXNfPTACzuUHc+Gxe3Gk+oS3uxAU6X49TMIs5kCZSdkpKSYgP8gCP77vn7Cvn0H1zRqKIzPgtBzAyTdewPTSPhTXTfHD4+vYLqUlLPnj2tOZPVTmWZwqI2Za8TMgOb710H/x/q/Ptq+T14dHuzPQ4399FB9fPhHBTvkTQS3hnNONW47BTK0PRzrSzTtKu1ufXK1GmDETwUIRBs9l9qKy7WxEkj8zbZaV5DgM/vzG9Zh8fRvGN3+Byde3ATfLAfz7wgkc+9FFAMCXV26Au4RzMqV/uefLpDS9dtU9bz7LeuE6ygCH9Q+dL6TLau4ObDMLl9XA4HOCzLFb2xjWX/K3DNgAYKyVPijJG9D8+bQdpLsz2Dl2axvDdKOCbEsGTqvUWRU+1RAA5uiuLmwvc9hBSQFxbVKR3KOwDdnOs6eqoj88Y/26rF8Wtqh6jWEbCpdVInsoeDOVaVFICdjDdBlfjj+2e9ieARsADEqVsCa44wYl6srBrDpsg4or5UJHTkGcjVTb3oBtVKkYhZ5gAJCQijk2WIUVgm2U5SOnnrRT6IEqOuZVVxG2USWvEPYljnwGlL0Iu1tWPQG5b/cNtK+/b2N1YRv2aWMOKKnj/wFso8u0guikN8ie/oKNLNjC28uC7RXkyOZC6TxEV25bJdhxQe7pmJPOex220c5NTHSeCGj3gr/2sI2yUdsCjvkuex22V5DFgBZM0WXCzgKda0M+50AcUFamCLrLgr2zJRV3GQLbW1bJgIYOXskvdwP2xy8/IRhNy11PnfUb+HMOJRWShFXdhXT9BykA1NS++b9AN+Ur6W8ZS82ReTlJZSufOUIwXEMNhEBatY8IVEVQFTo4KtAf6I/ER3ZCbBkyH3cKdlSQc2dUzTNGO2kSZo1jN6d2RB8174RG6NYdtxBbt6zYILvGcEmQ6paPLgna1hlDWqEyQGUUxJ/jYLm74iKiS+otI94hMFSQZo40AKWgiFBMgbUNi7WNGYqJhR0XmB1ew+xwgXJcA1JUg6ttq3hsPdk6RDdXonLC+PILchNQnPp+S1UA0HVdKSYVxpcmMP+6DNq4guLwIajjR1GNxrBrGoDqahAC4NcAylzacpKwGxVkPiDnIxlV/2iLCFAVoKcEfWUKuvQ/vHfpNfy8/B2Kg/uhj46gLUBUg1HWhRJH5KbzwofHDNE2vqGHG4DRM/ka73XU3VtE9WtVNculUFD79uHUwd9A7TsMMhqqIugZgQo/cxprTkCCy2Sh3rlwj2rCMuItq2SRdC+otmlvzjT2wAjqxqNQhw6AxiPY/c23pkoCLNV6wUM1Fzbnz9Pl6pSgO1R0SYmJqVuMKruoPSXm0/fWmCVvadj9BjQ6ANj9QKFQFfXI9dTZEYIzj7ecxdozh9Jfp3JF2dTybT59CAtyq2TdjOJmrjsCkFEo14rm0EP17mTJrzMJ2HHkMZRl1BlX2nKSKsh6VmUF6XtowFH9NzSAirqt282WXthuw4CaxH2vJEMUU5BDR0bZijl0hWuBiw6ABZRS9QAKBWrOPqgAcL+75GALB8VegNLgBoqeBUWQgW3UzH3axR3Q+BmKtt7ETLKw5x2jwxifrfxmsXj2RDcDLrNP/eS59BdaAmEvg9I1gdMdALuzmxEDwGd3jr/wnONlTtIYk1VzKMLs75bd3Mzu7HaN7ttSZhvMStmJdPGcO/mOoeyWXQm2USVz85Qcc1Dkr0kl23ph59jF7sDuMicVUBO8WgIUV1YdtkHJfaqX6KiF6psBZa/ANjSbDXIMxeSosJuoLFCrC9vQdBY1soOTzjHMf9ZDoi4XmABnt8BnQDM0nUaNJAWRM8uC7qqC5+Ly4TjG2mtRRuBDZrs9SewWaCmOHlD+9wfbnS++BD2ofsVFwhtnnC4baKf73QJl4aTk+wTuW5qqiGzXmNVKAAAAAElFTkSuQmCC" id="image56dffcdc8a" transform="scale(1 -1) translate(0 -51.12)" x="302.482286" y="-242.838454" width="51.12" height="51.12"/> - + +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" id="image9088131303" transform="scale(1 -1) translate(0 -51.12)" x="363.702857" y="-242.838454" width="51.12" height="51.12"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/lib/matplotlib/tests/baseline_images/test_image/rotate_image.png b/lib/matplotlib/tests/baseline_images/test_image/rotate_image.png index f0edf02258901642d0c694a1663f799ed589358f..31e62e4ed4cbe591230197e7e84390971880e8d9 100644 GIT binary patch literal 24068 zcmeEuX*ksV`}WsPVkA*ykSWRom&QtB2_`OGw?+)!E+3)!xR8)7|pfa~mheTS9k) zZU|Xey5AHNy~S^3VId)5Zf<&8%uL8a0-V;auFlUTg@qmd>*qpF&#Z(= zwxM)z3L0nB6}eWY&3(S>MeOzu88O=<=YO6 zzm87+sJTBCk&^oRVRhHLt0pB12`?H)NTr6j?bhrbAE%sGMljNlfa?h$d59+i@6dG) zat7jiD}y}o?V=Jy{P@XP5_0esaP$9v`2Y4H=nTUl`6{=?0h^{^nqlwNT3vm8EAPut zgS)!Ax3f>wn zl2Sv9t&!|@-sI`v%Mnp5(6iSF##h11T##$hP4m$Ek(aXxy;%vPjSnE_OD3r#PQU-J z1AqgWQ*(m<{!d5$fAQh1+xF4a`Za%tIDr6B8XZQ!X;w1-owe zMeWkGY~_NKTLxs}4!At4)GU4;=>qNq;oe!}+P=kBwhNV8TKH+OC_&(`Dw|{8xEQ2L zJiEz*G2=4BX_NQZO1puTJ#JO1<vROJ%pn##Ygf>(Kj45H9v%)H>u*<2c00wd5)M

PjTXClW|I^Ko&tuEJPPMrjY~r3`S0Tvx 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 literal 24194 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 diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 4340be96a38b..dfacfccb3e0e 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -696,7 +696,7 @@ def test_jpeg_alpha(): # If this fails, there will be only one color (all black). If this # is working, we should have all 256 shades of grey represented. num_colors = len(image.getcolors(256)) - assert 175 <= num_colors <= 210 + assert 175 <= num_colors <= 230 # The fully transparent part should be red. corner_pixel = image.getpixel((0, 0)) assert corner_pixel == (254, 0, 0) @@ -1404,8 +1404,7 @@ def test_nonuniform_logscale(): ax.add_image(im) -@image_comparison( - ['rgba_antialias.png'], style='mpl20', remove_text=True, tol=0.01) +@image_comparison(['rgba_antialias.png'], style='mpl20', remove_text=True, tol=0.02) def test_rgba_antialias(): fig, axs = plt.subplots(2, 2, figsize=(3.5, 3.5), sharex=False, sharey=False, constrained_layout=True) diff --git a/lib/matplotlib/tests/test_png.py b/lib/matplotlib/tests/test_png.py index aa7591508a67..9208c31df2bf 100644 --- a/lib/matplotlib/tests/test_png.py +++ b/lib/matplotlib/tests/test_png.py @@ -7,7 +7,7 @@ from matplotlib import cm, pyplot as plt -@image_comparison(['pngsuite.png'], tol=0.03) +@image_comparison(['pngsuite.png'], tol=0.04) def test_pngsuite(): files = sorted( (Path(__file__).parent / "baseline_images/pngsuite").glob("basn*.png")) diff --git a/src/_image_resample.h b/src/_image_resample.h index a6404092ea2d..ddf1a4050325 100644 --- a/src/_image_resample.h +++ b/src/_image_resample.h @@ -3,6 +3,8 @@ #ifndef MPL_RESAMPLE_H #define MPL_RESAMPLE_H +#define MPL_DISABLE_AGG_GRAY_CLIPPING + #include "agg_image_accessors.h" #include "agg_path_storage.h" #include "agg_pixfmt_gray.h" From 26ca2f6013b706cc0d54f02c2d580255c1880c6d Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 17 Jul 2024 07:15:46 -0700 Subject: [PATCH 0040/1230] Backport PR #28582: FIX: make sticky edge tolerance relative to data range --- lib/matplotlib/axes/_base.py | 10 ++-------- .../test_axes/sticky_tolerance_cf.png | Bin 0 -> 6222 bytes lib/matplotlib/tests/test_axes.py | 10 ++++++++++ 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_axes/sticky_tolerance_cf.png diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index f83999436cbb..17feef5b2105 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2962,22 +2962,16 @@ def handle_single_axis( # Prevent margin addition from crossing a sticky value. A small # tolerance must be added due to floating point issues with - # streamplot; it is defined relative to x0, x1, x1-x0 but has + # streamplot; it is defined relative to x1-x0 but has # no absolute term (e.g. "+1e-8") to avoid issues when working with # datasets where all values are tiny (less than 1e-8). - tol = 1e-5 * max(abs(x0), abs(x1), abs(x1 - x0)) + tol = 1e-5 * abs(x1 - x0) # Index of largest element < x0 + tol, if any. i0 = stickies.searchsorted(x0 + tol) - 1 x0bound = stickies[i0] if i0 != -1 else None - # Ensure the boundary acts only if the sticky is the extreme value - if x0bound is not None and x0bound > x0: - x0bound = None # Index of smallest element > x1 - tol, if any. i1 = stickies.searchsorted(x1 - tol) x1bound = stickies[i1] if i1 != len(stickies) else None - # Ensure the boundary acts only if the sticky is the extreme value - if x1bound is not None and x1bound < x1: - x1bound = None # Add the margin in figure space and then transform back, to handle # non-linear scales. diff --git a/lib/matplotlib/tests/baseline_images/test_axes/sticky_tolerance_cf.png b/lib/matplotlib/tests/baseline_images/test_axes/sticky_tolerance_cf.png new file mode 100644 index 0000000000000000000000000000000000000000..a2e185c2769d6de5ae19ee2154fc6097a9173a32 GIT binary patch literal 6222 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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 3c0407ee4098..f18e05dc2f1e 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -701,6 +701,16 @@ def test_sticky_tolerance(): axs.flat[3].barh(y=1, width=width, left=-20000.1) +@image_comparison(['sticky_tolerance_cf.png'], remove_text=True, style="mpl20") +def test_sticky_tolerance_contourf(): + fig, ax = plt.subplots() + + x = y = [14496.71, 14496.75] + data = [[0, 1], [2, 3]] + + ax.contourf(x, y, data) + + def test_nargs_stem(): with pytest.raises(TypeError, match='0 were given'): # stem() takes 1-3 arguments. From 2f12c03ab9d32b24c65ea186396d02e71d37b802 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 18 Jul 2024 14:04:45 -0500 Subject: [PATCH 0041/1230] Backport PR #28580: Bump actions/attest-build-provenance from 1.3.2 to 1.3.3 in the actions group --- .github/workflows/cibuildwheel.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 050ff16cfbbd..ef819ea5a438 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -203,7 +203,7 @@ jobs: run: ls dist - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2 + uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 with: subject-path: dist/matplotlib-* From c185751c7398270bc8bf138f67a9a8b09332e65b Mon Sep 17 00:00:00 2001 From: hannah Date: Fri, 12 Jul 2024 18:06:42 -0400 Subject: [PATCH 0042/1230] applying toc styling to remove nesting change "new contributors" section title to "contributing guide" add copy to anchor each section of devel/index small doc cleanups based on above --- doc/devel/contribute.rst | 13 ++++----- doc/devel/index.rst | 58 +++++++++++++++++++++++++--------------- doc/index.rst | 10 ++++--- 3 files changed, 47 insertions(+), 34 deletions(-) diff --git a/doc/devel/contribute.rst b/doc/devel/contribute.rst index 7b2b0e774ec7..4eb900bce7ed 100644 --- a/doc/devel/contribute.rst +++ b/doc/devel/contribute.rst @@ -2,9 +2,10 @@ .. _contributing: -********** -Contribute -********** +****************** +Contributing guide +****************** + You've discovered a bug or something else you want to change in Matplotlib — excellent! @@ -13,10 +14,6 @@ You've worked out a way to fix it — even better! You want to tell us about it — best of all! -This project is a community effort, and everyone is welcome to contribute. Everyone -within the community is expected to abide by our `code of conduct -`_. - Below, you can find a number of ways to contribute, and how to connect with the Matplotlib community. @@ -275,7 +272,7 @@ repository `__ on GitHub, then submit a "pull request" (PR). You can do this by cloning a copy of the Maplotlib repository to your own computer, or alternatively using `GitHub Codespaces `_, a cloud-based -in-browser development environment that comes with the appropriated setup to +in-browser development environment that comes with the appropriate setup to contribute to Matplotlib. Workflow overview diff --git a/doc/devel/index.rst b/doc/devel/index.rst index 9744d757c342..672f2ce9f9d9 100644 --- a/doc/devel/index.rst +++ b/doc/devel/index.rst @@ -16,13 +16,27 @@ Contribute :octicon:`heart;1em;sd-text-info` Thank you for your interest in helping to improve Matplotlib! :octicon:`heart;1em;sd-text-info` -There are various ways to contribute: optimizing and refactoring code, detailing -unclear documentation and writing new examples, helping the community, reporting -and fixing bugs and requesting and implementing new features... +This project is a community effort, and everyone is welcome to contribute. Everyone +within the community is expected to abide by our :ref:`code of conduct `. + +There are various ways to contribute, such as optimizing and refactoring code, +detailing unclear documentation and writing new examples, helping the community, +reporting and fixing bugs, requesting and implementing new features... .. _submitting-a-bug-report: .. _request-a-new-feature: +GitHub issue tracker +==================== + +The `issue tracker `_ serves as the +centralized location for making feature requests, reporting bugs, identifying major +projects to work on, and discussing priorities. + +We have preloaded the issue creation page with markdown forms requesting the information +we need to triage issues and we welcome you to add any additional information or +context that may be necessary for resolving the issue: + .. grid:: 1 1 2 2 .. grid-item-card:: @@ -31,9 +45,7 @@ and fixing bugs and requesting and implementing new features... :octicon:`bug;1em;sd-text-info` **Submit a bug report** ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - We have preloaded the issue creation page with a Markdown form that you can - use to provide relevant context. Thank you for your help in keeping bug reports - complete, targeted and descriptive. + Thank you for your help in keeping bug reports targeted and descriptive. .. button-link:: https://github.com/matplotlib/matplotlib/issues/new/choose :expand: @@ -47,9 +59,7 @@ and fixing bugs and requesting and implementing new features... :octicon:`light-bulb;1em;sd-text-info` **Request a new feature** ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - We will give feedback on the feature proposal. Since - Matplotlib is an open source project with limited resources, we encourage - users to then also :ref:`participate in the implementation `. + Thank you for your help in keeping feature requests well defined and tightly scoped. .. button-link:: https://github.com/matplotlib/matplotlib/issues/new/choose :expand: @@ -57,17 +67,16 @@ and fixing bugs and requesting and implementing new features... Request a feature +Since Matplotlib is an open source project with limited resources, we encourage users +to also :ref:`participate ` in fixing bugs and implementing new +features. + +Contributing guide +================== We welcome you to get more involved with the Matplotlib project! If you are new to contributing, we recommend that you first read our -:ref:`contributing guide`. If you are contributing code or -documentation, please follow our guides for setting up and managing a -:ref:`development environment and workflow`. -For code, documentation, or triage, please follow the corresponding -:ref:`contribution guidelines `. - -New contributors -================ +:ref:`contributing guide`: .. toctree:: :hidden: @@ -115,13 +124,13 @@ New contributors :octicon:`globe;1em;sd-text-info` Build community - - - .. _development_environment: -Development environment -======================= +Development workflow +==================== + +If you are contributing code or documentation, please follow our guide for setting up +and managing a development environment and workflow: .. grid:: 1 1 2 2 @@ -159,6 +168,11 @@ Development environment Policies and guidelines ======================= +These policies and guidelines help us maintain consistency in the various types +of maintenance work. If you are writing code or documentation, following these policies +helps maintainers more easily review your work. If you are helping triage, community +manage, or release manage, these guidelines describe how our current process works. + .. grid:: 1 1 2 2 :class-row: sf-fs-1 :gutter: 2 diff --git a/doc/index.rst b/doc/index.rst index 1a385d2330af..dedd614985df 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -31,6 +31,7 @@ Install .. tab-item:: other + .. rst-class:: section-toc .. toctree:: :maxdepth: 2 @@ -106,6 +107,7 @@ Community .. grid-item:: + .. rst-class:: section-toc .. toctree:: :maxdepth: 2 @@ -144,11 +146,11 @@ Contribute .. grid-item:: - Matplotlib is a community project maintained for and by its users. - - There are many ways you can help! + Matplotlib is a community project maintained for and by its users. See + :ref:`developers-guide-index` for the many ways you can help! .. grid-item:: + .. rst-class:: section-toc .. toctree:: :maxdepth: 2 @@ -168,7 +170,7 @@ About us and hard things possible. .. grid-item:: - + .. rst-class:: section-toc .. toctree:: :maxdepth: 2 From f6ba2ef0fb3a296bec3baa0546273a11d4b8c471 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 18 Jul 2024 21:47:45 -0400 Subject: [PATCH 0043/1230] MNT: Update ruff config to 0.2.0 This release deprecated some locations for settings, and moved them into different TOML tables. --- pyproject.toml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 40222fe266da..61f1432937b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,6 +112,10 @@ exclude = [ ".tox", ".eggs", ] +line-length = 88 +target-version = "py310" + +[tool.ruff.lint] ignore = [ "D100", "D101", @@ -132,7 +136,6 @@ ignore = [ "E741", "F841", ] -line-length = 88 select = [ "D", "E", @@ -140,7 +143,7 @@ select = [ "W", ] -# The following error codes are not supported by ruff v0.0.240 +# The following error codes are not supported by ruff v0.2.0 # They are planned and should be selected once implemented # even if they are deselected by default. # These are primarily whitespace/corrected by autoformatters (which we don't use). @@ -158,12 +161,10 @@ external = [ "E703", ] -target-version = "py310" - -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "numpy" -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "doc/conf.py" = ["E402"] "galleries/examples/animation/frame_grabbing_sgskip.py" = ["E402"] "galleries/examples/lines_bars_and_markers/marker_reference.py" = ["E402"] From c0c4f6aedf440d4f81a511037f46eb4d965b98d8 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 00:06:58 -0400 Subject: [PATCH 0044/1230] Simplify a SymmetricalLogLocator test And also fix a whitespace linting error (E221) in it. --- lib/matplotlib/tests/test_ticker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index ac68a5d90b14..5f3619cb8cf0 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -637,7 +637,7 @@ def test_subs(self): sym = mticker.SymmetricalLogLocator(base=10, linthresh=1, subs=[2.0, 4.0]) sym.create_dummy_axis() sym.axis.set_view_interval(-10, 10) - assert (sym() == [-20., -40., -2., -4., 0., 2., 4., 20., 40.]).all() + assert_array_equal(sym(), [-20, -40, -2, -4, 0, 2, 4, 20, 40]) def test_extending(self): sym = mticker.SymmetricalLogLocator(base=10, linthresh=1) From 376a4af0d45bbcc1bca9b5ee5e3b5ecdbf555939 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 00:31:31 -0400 Subject: [PATCH 0045/1230] Enable ruff's preview whitespace linting rules These are the ones we were using with flake8, but were not available before ruff 0.2.0 (or at least whatever the last tested version was). Also, clean up a bit of the code so that we only use one type of whitespace exception. --- .flake8 | 7 +++--- lib/matplotlib/_mathtext.py | 36 +++++++++++++++--------------- lib/matplotlib/axes/_axes.pyi | 10 ++++----- lib/matplotlib/transforms.py | 12 +++++----- lib/mpl_toolkits/mplot3d/proj3d.py | 8 +++---- pyproject.toml | 31 ++++++++++++++++++------- 6 files changed, 60 insertions(+), 44 deletions(-) diff --git a/.flake8 b/.flake8 index 36e8bcf5476f..7297d72b5841 100644 --- a/.flake8 +++ b/.flake8 @@ -34,17 +34,18 @@ exclude = per-file-ignores = lib/matplotlib/_cm.py: E202, E203, E302 - lib/matplotlib/_mathtext.py: E221, E251 - lib/matplotlib/_mathtext_data.py: E203, E261 + lib/matplotlib/_mathtext.py: E221 + lib/matplotlib/_mathtext_data.py: E203 lib/matplotlib/backends/backend_template.py: F401 lib/matplotlib/mathtext.py: E221 lib/matplotlib/pylab.py: F401, F403 lib/matplotlib/pyplot.py: F811 lib/matplotlib/tests/test_mathtext.py: E501 - lib/matplotlib/transforms.py: E201, E202, E203 + lib/matplotlib/transforms.py: E201, E202 lib/matplotlib/tri/_triinterpolate.py: E201, E221 lib/mpl_toolkits/axes_grid1/axes_size.py: E272 lib/mpl_toolkits/axisartist/angle_helper.py: E221 + lib/mpl_toolkits/mplot3d/proj3d.py: E201 doc/conf.py: E402 galleries/users_explain/quick_start.py: E402 diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index e47c58c72f63..e6ecb038e815 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -379,27 +379,27 @@ def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float, xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox] offset = self._get_offset(font, glyph, fontsize, dpi) metrics = FontMetrics( - advance = glyph.linearHoriAdvance/65536.0, - height = glyph.height/64.0, - width = glyph.width/64.0, - xmin = xmin, - xmax = xmax, - ymin = ymin+offset, - ymax = ymax+offset, + advance=glyph.linearHoriAdvance / 65536, + height=glyph.height / 64, + width=glyph.width / 64, + xmin=xmin, + xmax=xmax, + ymin=ymin + offset, + ymax=ymax + offset, # iceberg is the equivalent of TeX's "height" - iceberg = glyph.horiBearingY/64.0 + offset, - slanted = slanted - ) + iceberg=glyph.horiBearingY / 64 + offset, + slanted=slanted + ) return FontInfo( - font = font, - fontsize = fontsize, - postscript_name = font.postscript_name, - metrics = metrics, - num = num, - glyph = glyph, - offset = offset - ) + font=font, + fontsize=fontsize, + postscript_name=font.postscript_name, + metrics=metrics, + num=num, + glyph=glyph, + offset=offset + ) def get_xheight(self, fontname: str, fontsize: float, dpi: float) -> float: font = self._get_font(fontname) diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 732134850c2b..186177576067 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -202,7 +202,7 @@ class Axes(_AxesBase): *args: float | ArrayLike | str, scalex: bool = ..., scaley: bool = ..., - data = ..., + data=..., **kwargs ) -> list[Line2D]: ... def plot_date( @@ -232,7 +232,7 @@ class Axes(_AxesBase): detrend: Callable[[ArrayLike], ArrayLike] = ..., usevlines: bool = ..., maxlags: int = ..., - data = ..., + data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray, LineCollection | Line2D, Line2D | None]: ... def step( @@ -241,7 +241,7 @@ class Axes(_AxesBase): y: ArrayLike, *args, where: Literal["pre", "post", "mid"] = ..., - data = ..., + data=..., **kwargs ) -> list[Line2D]: ... def bar( @@ -252,7 +252,7 @@ class Axes(_AxesBase): bottom: float | ArrayLike | None = ..., *, align: Literal["center", "edge"] = ..., - data = ..., + data=..., **kwargs ) -> BarContainer: ... def barh( @@ -263,7 +263,7 @@ class Axes(_AxesBase): left: float | ArrayLike | None = ..., *, align: Literal["center", "edge"] = ..., - data = ..., + data=..., **kwargs ) -> BarContainer: ... def bar_label( diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 3575bd1fc14d..9d476cdc1701 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -2574,9 +2574,9 @@ def get_matrix(self): if DEBUG and (x_scale == 0 or y_scale == 0): raise ValueError( "Transforming from or to a singular bounding box") - self._mtx = np.array([[x_scale, 0.0 , (-inl*x_scale+outl)], - [0.0 , y_scale, (-inb*y_scale+outb)], - [0.0 , 0.0 , 1.0 ]], + self._mtx = np.array([[x_scale, 0.0, -inl*x_scale+outl], + [ 0.0, y_scale, -inb*y_scale+outb], + [ 0.0, 0.0, 1.0]], float) self._inverted = None self._invalid = 0 @@ -2668,9 +2668,9 @@ def get_matrix(self): raise ValueError("Transforming from a singular bounding box.") x_scale = 1.0 / inw y_scale = 1.0 / inh - self._mtx = np.array([[x_scale, 0.0 , (-inl*x_scale)], - [0.0 , y_scale, (-inb*y_scale)], - [0.0 , 0.0 , 1.0 ]], + self._mtx = np.array([[x_scale, 0.0, -inl*x_scale], + [ 0.0, y_scale, -inb*y_scale], + [ 0.0, 0.0, 1.0]], float) self._inverted = None self._invalid = 0 diff --git a/lib/mpl_toolkits/mplot3d/proj3d.py b/lib/mpl_toolkits/mplot3d/proj3d.py index 098a7b6f6667..38c09d959b89 100644 --- a/lib/mpl_toolkits/mplot3d/proj3d.py +++ b/lib/mpl_toolkits/mplot3d/proj3d.py @@ -23,10 +23,10 @@ def world_transformation(xmin, xmax, dy /= ay dz /= az - return np.array([[1/dx, 0, 0, -xmin/dx], - [0, 1/dy, 0, -ymin/dy], - [0, 0, 1/dz, -zmin/dz], - [0, 0, 0, 1]]) + return np.array([[1/dx, 0, 0, -xmin/dx], + [ 0, 1/dy, 0, -ymin/dy], + [ 0, 0, 1/dz, -zmin/dz], + [ 0, 0, 0, 1]]) @_api.deprecated("3.8") diff --git a/pyproject.toml b/pyproject.toml index 61f1432937b9..4c528865e3ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,11 +136,22 @@ ignore = [ "E741", "F841", ] +preview = true +explicit-preview-rules = true select = [ "D", "E", "F", "W", + # The following error codes require the preview mode to be enabled. + "E201", + "E202", + "E203", + "E221", + "E251", + "E261", + "E272", + "E703", ] # The following error codes are not supported by ruff v0.2.0 @@ -150,15 +161,7 @@ select = [ # See https://github.com/charliermarsh/ruff/issues/2402 for status on implementation external = [ "E122", - "E201", - "E202", - "E203", - "E221", - "E251", - "E261", - "E272", "E302", - "E703", ] [tool.ruff.lint.pydocstyle] @@ -167,8 +170,12 @@ convention = "numpy" [tool.ruff.lint.per-file-ignores] "doc/conf.py" = ["E402"] "galleries/examples/animation/frame_grabbing_sgskip.py" = ["E402"] +"galleries/examples/images_contours_and_fields/tricontour_demo.py" = ["E201"] +"galleries/examples/images_contours_and_fields/tripcolor_demo.py" = ["E201"] +"galleries/examples/images_contours_and_fields/triplot_demo.py" = ["E201"] "galleries/examples/lines_bars_and_markers/marker_reference.py" = ["E402"] "galleries/examples/misc/print_stdout_sgskip.py" = ["E402"] +"galleries/examples/misc/table_demo.py" = ["E201"] "galleries/examples/style_sheets/bmh.py" = ["E501"] "galleries/examples/subplots_axes_and_figures/demo_constrained_layout.py" = ["E402"] "galleries/examples/text_labels_and_annotations/custom_legends.py" = ["E402"] @@ -188,6 +195,9 @@ convention = "numpy" "lib/matplotlib/__init__.py" = ["E402", "F401"] "lib/matplotlib/_animation_data.py" = ["E501"] "lib/matplotlib/_api/__init__.py" = ["F401"] +"lib/matplotlib/_cm.py" = ["E202", "E203"] +"lib/matplotlib/_mathtext.py" = ["E221"] +"lib/matplotlib/_mathtext_data.py" = ["E203"] "lib/matplotlib/axes/__init__.py" = ["F401", "F403"] "lib/matplotlib/backends/backend_template.py" = ["F401"] "lib/matplotlib/font_manager.py" = ["E501"] @@ -195,7 +205,12 @@ convention = "numpy" "lib/matplotlib/pylab.py" = ["F401", "F403"] "lib/matplotlib/pyplot.py" = ["F401", "F811"] "lib/matplotlib/tests/test_mathtext.py" = ["E501"] +"lib/matplotlib/transforms.py" = ["E201"] +"lib/matplotlib/tri/_triinterpolate.py" = ["E201", "E221"] +"lib/mpl_toolkits/axes_grid1/axes_size.py" = ["E272"] "lib/mpl_toolkits/axisartist/__init__.py" = ["F401"] +"lib/mpl_toolkits/axisartist/angle_helper.py" = ["E221"] +"lib/mpl_toolkits/mplot3d/proj3d.py" = ["E201"] "lib/pylab.py" = ["F401", "F403"] "galleries/users_explain/artists/paths.py" = ["E402"] From 3174980785d47520060a3c9b29da7082212c9360 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 00:58:21 -0400 Subject: [PATCH 0046/1230] Clean up F401 diagnostics from ruff --- lib/matplotlib/_api/__init__.pyi | 2 +- lib/matplotlib/axes/__init__.py | 2 +- lib/matplotlib/axes/_axes.py | 2 +- lib/matplotlib/contour.pyi | 1 - lib/matplotlib/gridspec.pyi | 2 +- lib/matplotlib/lines.pyi | 2 +- lib/matplotlib/mathtext.py | 2 +- lib/matplotlib/pyplot.py | 5 +++-- lib/matplotlib/spines.pyi | 2 +- lib/matplotlib/tests/test_triangulation.py | 2 +- lib/matplotlib/text.pyi | 2 +- lib/matplotlib/widgets.pyi | 2 +- pyproject.toml | 12 +++++------- 13 files changed, 18 insertions(+), 20 deletions(-) diff --git a/lib/matplotlib/_api/__init__.pyi b/lib/matplotlib/_api/__init__.pyi index 8dbef9528a82..fa047f1781bb 100644 --- a/lib/matplotlib/_api/__init__.pyi +++ b/lib/matplotlib/_api/__init__.pyi @@ -4,7 +4,7 @@ from typing_extensions import Self # < Py 3.11 from numpy.typing import NDArray -from .deprecation import ( # noqa: re-exported API +from .deprecation import ( # noqa: F401, re-exported API deprecated as deprecated, warn_deprecated as warn_deprecated, rename_parameter as rename_parameter, diff --git a/lib/matplotlib/axes/__init__.py b/lib/matplotlib/axes/__init__.py index 9f2913957194..cdc31f17aae6 100644 --- a/lib/matplotlib/axes/__init__.py +++ b/lib/matplotlib/axes/__init__.py @@ -1,5 +1,5 @@ from . import _base -from ._axes import Axes # noqa: F401 +from ._axes import Axes # Backcompat. Subplot = Axes diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 5d5248951314..1fe8c771f706 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -14,7 +14,7 @@ import matplotlib.collections as mcoll import matplotlib.colors as mcolors import matplotlib.contour as mcontour -import matplotlib.dates # noqa # Register date unit converter as side effect. +import matplotlib.dates # noqa: F401, Register date unit converter as side effect. import matplotlib.image as mimage import matplotlib.legend as mlegend import matplotlib.lines as mlines diff --git a/lib/matplotlib/contour.pyi b/lib/matplotlib/contour.pyi index c386bea47ab7..9d99fe0f343c 100644 --- a/lib/matplotlib/contour.pyi +++ b/lib/matplotlib/contour.pyi @@ -3,7 +3,6 @@ from matplotlib.artist import Artist from matplotlib.axes import Axes from matplotlib.collections import Collection, PathCollection from matplotlib.colors import Colormap, Normalize -from matplotlib.font_manager import FontProperties from matplotlib.path import Path from matplotlib.patches import Patch from matplotlib.text import Text diff --git a/lib/matplotlib/gridspec.pyi b/lib/matplotlib/gridspec.pyi index b6732ad8fafa..08c4dd7f4e49 100644 --- a/lib/matplotlib/gridspec.pyi +++ b/lib/matplotlib/gridspec.pyi @@ -3,7 +3,7 @@ from typing import Any, Literal, overload from numpy.typing import ArrayLike import numpy as np -from matplotlib.axes import Axes, SubplotBase +from matplotlib.axes import Axes from matplotlib.backend_bases import RendererBase from matplotlib.figure import Figure from matplotlib.transforms import Bbox diff --git a/lib/matplotlib/lines.pyi b/lib/matplotlib/lines.pyi index c91e457e3301..161f99100bf5 100644 --- a/lib/matplotlib/lines.pyi +++ b/lib/matplotlib/lines.pyi @@ -2,7 +2,7 @@ from .artist import Artist from .axes import Axes from .backend_bases import MouseEvent, FigureCanvasBase from .path import Path -from .transforms import Bbox, Transform +from .transforms import Bbox from collections.abc import Callable, Sequence from typing import Any, Literal, overload diff --git a/lib/matplotlib/mathtext.py b/lib/matplotlib/mathtext.py index 3e4b658c141b..cee29c0d2feb 100644 --- a/lib/matplotlib/mathtext.py +++ b/lib/matplotlib/mathtext.py @@ -22,7 +22,7 @@ from matplotlib import _api, _mathtext from matplotlib.ft2font import LOAD_NO_HINTING from matplotlib.font_manager import FontProperties -from ._mathtext import ( # noqa: reexported API +from ._mathtext import ( # noqa: F401, reexported API RasterParse, VectorParse, get_unicode_index) _log = logging.getLogger(__name__) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index e6242271d113..1ea391b16b64 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -55,8 +55,9 @@ import matplotlib.colorbar import matplotlib.image from matplotlib import _api -from matplotlib import ( # noqa: F401 Re-exported for typing. - cm as cm, get_backend as get_backend, rcParams as rcParams, style as style) +# Re-exported (import x as x) for typing. +from matplotlib import cm as cm, get_backend as get_backend, rcParams as rcParams +from matplotlib import style as style # noqa: F401 from matplotlib import _pylab_helpers from matplotlib import interactive # noqa: F401 from matplotlib import cbook diff --git a/lib/matplotlib/spines.pyi b/lib/matplotlib/spines.pyi index 0f06a6d1ce2b..ff2a1a40bf94 100644 --- a/lib/matplotlib/spines.pyi +++ b/lib/matplotlib/spines.pyi @@ -1,5 +1,5 @@ from collections.abc import Callable, Iterator, MutableMapping -from typing import Any, Literal, TypeVar, overload +from typing import Literal, TypeVar, overload import matplotlib.patches as mpatches from matplotlib.axes import Axes diff --git a/lib/matplotlib/tests/test_triangulation.py b/lib/matplotlib/tests/test_triangulation.py index 14c591abd4e5..6e3ec9628fcc 100644 --- a/lib/matplotlib/tests/test_triangulation.py +++ b/lib/matplotlib/tests/test_triangulation.py @@ -1183,7 +1183,7 @@ def test_tricontourf_decreasing_levels(): def test_internal_cpp_api(): # Following github issue 8197. - from matplotlib import _tri # noqa: ensure lazy-loaded module *is* loaded. + from matplotlib import _tri # noqa: F401, ensure lazy-loaded module *is* loaded. # C++ Triangulation. with pytest.raises( diff --git a/lib/matplotlib/text.pyi b/lib/matplotlib/text.pyi index 6a83b1bbbed9..902f0a00dfe8 100644 --- a/lib/matplotlib/text.pyi +++ b/lib/matplotlib/text.pyi @@ -4,7 +4,7 @@ from .font_manager import FontProperties from .offsetbox import DraggableAnnotation from .path import Path from .patches import FancyArrowPatch, FancyBboxPatch -from .textpath import ( # noqa: reexported API +from .textpath import ( # noqa: F401, reexported API TextPath as TextPath, TextToPath as TextToPath, ) diff --git a/lib/matplotlib/widgets.pyi b/lib/matplotlib/widgets.pyi index 58adf85aae60..f5de6cb62414 100644 --- a/lib/matplotlib/widgets.pyi +++ b/lib/matplotlib/widgets.pyi @@ -4,7 +4,7 @@ from .backend_bases import FigureCanvasBase, Event, MouseEvent, MouseButton from .collections import LineCollection from .figure import Figure from .lines import Line2D -from .patches import Circle, Polygon, Rectangle +from .patches import Polygon, Rectangle from .text import Text import PIL.Image diff --git a/pyproject.toml b/pyproject.toml index 4c528865e3ad..48259895051a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -192,26 +192,24 @@ convention = "numpy" "galleries/examples/user_interfaces/pylab_with_gtk4_sgskip.py" = ["E402"] "galleries/examples/userdemo/pgf_preamble_sgskip.py" = ["E402"] -"lib/matplotlib/__init__.py" = ["E402", "F401"] +"lib/matplotlib/__init__.py" = ["E402"] "lib/matplotlib/_animation_data.py" = ["E501"] -"lib/matplotlib/_api/__init__.py" = ["F401"] "lib/matplotlib/_cm.py" = ["E202", "E203"] "lib/matplotlib/_mathtext.py" = ["E221"] "lib/matplotlib/_mathtext_data.py" = ["E203"] -"lib/matplotlib/axes/__init__.py" = ["F401", "F403"] +"lib/matplotlib/axes/__init__.py" = ["F403"] "lib/matplotlib/backends/backend_template.py" = ["F401"] "lib/matplotlib/font_manager.py" = ["E501"] -"lib/matplotlib/image.py" = ["F401", "F403"] +"lib/matplotlib/image.py" = ["F403"] "lib/matplotlib/pylab.py" = ["F401", "F403"] -"lib/matplotlib/pyplot.py" = ["F401", "F811"] +"lib/matplotlib/pyplot.py" = ["F811"] "lib/matplotlib/tests/test_mathtext.py" = ["E501"] "lib/matplotlib/transforms.py" = ["E201"] "lib/matplotlib/tri/_triinterpolate.py" = ["E201", "E221"] "lib/mpl_toolkits/axes_grid1/axes_size.py" = ["E272"] -"lib/mpl_toolkits/axisartist/__init__.py" = ["F401"] "lib/mpl_toolkits/axisartist/angle_helper.py" = ["E221"] "lib/mpl_toolkits/mplot3d/proj3d.py" = ["E201"] -"lib/pylab.py" = ["F401", "F403"] +"lib/pylab.py" = ["F403"] "galleries/users_explain/artists/paths.py" = ["E402"] "galleries/users_explain/artists/patheffects_guide.py" = ["E402"] From 14b80470719d45a4f2d65c7fb2c5aa866dc5d47a Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 19 Jul 2024 10:27:54 +0200 Subject: [PATCH 0047/1230] MNT: Raise on GeoAxes limits manipulation GeoAxes does not support changing limits. We already raised on `set_x/ylim()`. This now also raises on `set_x/ybounds()` and `invert_x/yaxis()`. Closes #28590. --- lib/matplotlib/projections/geo.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/matplotlib/projections/geo.py b/lib/matplotlib/projections/geo.py index 498b2f72ebb4..89a9de7618be 100644 --- a/lib/matplotlib/projections/geo.py +++ b/lib/matplotlib/projections/geo.py @@ -151,6 +151,15 @@ def set_xlim(self, *args, **kwargs): "not supported. Please consider using Cartopy.") set_ylim = set_xlim + set_xbound = set_xlim + set_ybound = set_ylim + + def invert_xaxis(self): + """Not supported. Please consider using Cartopy.""" + raise TypeError("Changing axes limits of a geographic projection is " + "not supported. Please consider using Cartopy.") + + invert_yaxis = invert_xaxis def format_coord(self, lon, lat): """Return a format string formatting the coordinate.""" From 9d6e2165966bbf3413634abf13b2d0c92b83c331 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 03:31:34 -0400 Subject: [PATCH 0048/1230] MNT: Clean up unused noqa comments --- lib/matplotlib/__init__.pyi | 4 ++-- lib/matplotlib/backends/qt_compat.py | 2 +- lib/matplotlib/pyplot.py | 2 +- pyproject.toml | 6 ------ tools/boilerplate.py | 5 ++++- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/__init__.pyi b/lib/matplotlib/__init__.pyi index 54b28a8318ef..e7208a17c99f 100644 --- a/lib/matplotlib/__init__.pyi +++ b/lib/matplotlib/__init__.pyi @@ -111,5 +111,5 @@ def _preprocess_data( label_namer: str | None = ... ) -> Callable: ... -from matplotlib.cm import _colormaps as colormaps -from matplotlib.colors import _color_sequences as color_sequences +from matplotlib.cm import _colormaps as colormaps # noqa: E402 +from matplotlib.colors import _color_sequences as color_sequences # noqa: E402 diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index d91f7c14cb22..b57a98b1138a 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -49,7 +49,7 @@ if QT_API_ENV in ["pyqt5", "pyside2"]: QT_API = _ETS[QT_API_ENV] else: - _QT_FORCE_QT5_BINDING = True # noqa + _QT_FORCE_QT5_BINDING = True # noqa: F811 QT_API = None # A non-Qt backend was selected but we still got there (possible, e.g., when # fully manually embedding Matplotlib in a Qt app without using pyplot). diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 1ea391b16b64..8ca35373328d 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -4018,7 +4018,7 @@ def spy( **kwargs, ) if isinstance(__ret, cm.ScalarMappable): - sci(__ret) # noqa + sci(__ret) return __ret diff --git a/pyproject.toml b/pyproject.toml index 48259895051a..0f181ccb629e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -192,15 +192,10 @@ convention = "numpy" "galleries/examples/user_interfaces/pylab_with_gtk4_sgskip.py" = ["E402"] "galleries/examples/userdemo/pgf_preamble_sgskip.py" = ["E402"] -"lib/matplotlib/__init__.py" = ["E402"] -"lib/matplotlib/_animation_data.py" = ["E501"] "lib/matplotlib/_cm.py" = ["E202", "E203"] "lib/matplotlib/_mathtext.py" = ["E221"] "lib/matplotlib/_mathtext_data.py" = ["E203"] -"lib/matplotlib/axes/__init__.py" = ["F403"] "lib/matplotlib/backends/backend_template.py" = ["F401"] -"lib/matplotlib/font_manager.py" = ["E501"] -"lib/matplotlib/image.py" = ["F403"] "lib/matplotlib/pylab.py" = ["F401", "F403"] "lib/matplotlib/pyplot.py" = ["F811"] "lib/matplotlib/tests/test_mathtext.py" = ["E501"] @@ -209,7 +204,6 @@ convention = "numpy" "lib/mpl_toolkits/axes_grid1/axes_size.py" = ["E272"] "lib/mpl_toolkits/axisartist/angle_helper.py" = ["E221"] "lib/mpl_toolkits/mplot3d/proj3d.py" = ["E201"] -"lib/pylab.py" = ["F403"] "galleries/users_explain/artists/paths.py" = ["E402"] "galleries/users_explain/artists/patheffects_guide.py" = ["E402"] diff --git a/tools/boilerplate.py b/tools/boilerplate.py index db93b102fce9..d4f8a01d0493 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -304,7 +304,10 @@ def boilerplate_gen(): 'pcolormesh': 'sci(__ret)', 'hist2d': 'sci(__ret[-1])', 'imshow': 'sci(__ret)', - 'spy': 'if isinstance(__ret, cm.ScalarMappable): sci(__ret) # noqa', + 'spy': ( + 'if isinstance(__ret, cm.ScalarMappable):\n' + ' sci(__ret)' + ), 'quiver': 'sci(__ret)', 'specgram': 'sci(__ret[-1])', 'streamplot': 'sci(__ret.lines)', From c37449cf46d2da62c42cf3657c8913d176e6d076 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 19 Jul 2024 22:50:31 +0200 Subject: [PATCH 0049/1230] Backport PR #28518: [TYP] Fix overload of `pyplot.subplots` --- lib/matplotlib/figure.pyi | 2 +- lib/matplotlib/pyplot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi index b079312695c1..c31f90b4b2a8 100644 --- a/lib/matplotlib/figure.pyi +++ b/lib/matplotlib/figure.pyi @@ -132,7 +132,7 @@ class FigureBase(Artist): height_ratios: Sequence[float] | None = ..., subplot_kw: dict[str, Any] | None = ..., gridspec_kw: dict[str, Any] | None = ..., - ) -> Axes | np.ndarray: ... + ) -> Any: ... def delaxes(self, ax: Axes) -> None: ... def clear(self, keep_observers: bool = ...) -> None: ... def clf(self, keep_observers: bool = ...) -> None: ... diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 8b4769342c7d..442013f7d21a 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -1597,7 +1597,7 @@ def subplots( subplot_kw: dict[str, Any] | None = ..., gridspec_kw: dict[str, Any] | None = ..., **fig_kw -) -> tuple[Figure, Axes | np.ndarray]: +) -> tuple[Figure, Any]: ... From a99ffa9f557ec224550af393b5c95fa2e5fc0a56 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 18:50:34 -0400 Subject: [PATCH 0050/1230] Pin PyQt6 back on Ubuntu 20.04 The 6.7.1 wheels on PyPI do not confirm to manylinux 2.28 due to requiring glibc 2.35 symbols, and cannot be loaded on Ubuntu 20.04, which has glibc 2.31. So we need to pin that back to avoid the failures. --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 230c42c136d5..8c27b09f1ad5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -69,7 +69,7 @@ jobs: CFLAGS: "-fno-lto" # Ensure that disabling LTO works. # https://github.com/matplotlib/matplotlib/pull/26052#issuecomment-1574595954 # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html - pyqt6-ver: '!=6.5.1,!=6.6.0' + pyqt6-ver: '!=6.5.1,!=6.6.0,!=6.7.1' # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 pyside6-ver: '!=6.5.1' - os: ubuntu-22.04 From 99eaf725ec585ebe3d950492ce141c87b1d3d5c5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 19:18:51 -0400 Subject: [PATCH 0051/1230] Pin PyQt6 back on Ubuntu 20.04 The 6.7.1 wheels on PyPI do not conform to manylinux 2.28 due to requiring glibc 2.35 symbols, and cannot be loaded on Ubuntu 20.04, which has glibc 2.31. So we need to pin that back to avoid test failures. --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index daa07e62b2e5..634c83fa57fd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,7 +64,7 @@ jobs: CFLAGS: "-fno-lto" # Ensure that disabling LTO works. # https://github.com/matplotlib/matplotlib/pull/26052#issuecomment-1574595954 # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html - pyqt6-ver: '!=6.5.1,!=6.6.0' + pyqt6-ver: '!=6.5.1,!=6.6.0,!=6.7.1' # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 pyside6-ver: '!=6.5.1' - os: ubuntu-20.04 @@ -72,7 +72,7 @@ jobs: extra-requirements: '-r requirements/testing/extra.txt' # https://github.com/matplotlib/matplotlib/pull/26052#issuecomment-1574595954 # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html - pyqt6-ver: '!=6.5.1,!=6.6.0' + pyqt6-ver: '!=6.5.1,!=6.6.0,!=6.7.1' # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 pyside6-ver: '!=6.5.1' - os: ubuntu-22.04 From 279cd8f3257ef269da2270d4b7360de8fde02503 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Wed, 15 May 2024 21:09:20 +0100 Subject: [PATCH 0052/1230] Remove internal use of Artist.figure --- lib/matplotlib/_constrained_layout.py | 4 +- lib/matplotlib/artist.py | 17 +++--- lib/matplotlib/artist.pyi | 7 ++- lib/matplotlib/axes/_axes.py | 7 ++- lib/matplotlib/axes/_base.py | 56 +++++++++++-------- lib/matplotlib/axes/_secondary_axes.py | 5 +- lib/matplotlib/axis.py | 43 +++++++------- lib/matplotlib/backend_bases.py | 14 ++--- lib/matplotlib/collections.py | 10 ++-- lib/matplotlib/contour.py | 7 ++- lib/matplotlib/figure.py | 25 ++++----- lib/matplotlib/figure.pyi | 6 ++ lib/matplotlib/gridspec.py | 4 +- lib/matplotlib/image.py | 9 +-- lib/matplotlib/legend.py | 12 ++-- lib/matplotlib/legend_handler.py | 2 +- lib/matplotlib/lines.py | 9 +-- lib/matplotlib/offsetbox.py | 30 +++++----- lib/matplotlib/patches.py | 15 ++--- lib/matplotlib/projections/polar.py | 6 +- lib/matplotlib/pyplot.py | 7 ++- lib/matplotlib/quiver.py | 19 ++++--- lib/matplotlib/spines.py | 9 +-- lib/matplotlib/table.py | 12 ++-- lib/matplotlib/testing/widgets.py | 4 +- lib/matplotlib/tests/test_agg.py | 4 +- lib/matplotlib/tests/test_artist.py | 2 +- lib/matplotlib/tests/test_axes.py | 6 +- lib/matplotlib/tests/test_backend_bases.py | 5 +- lib/matplotlib/tests/test_collections.py | 2 +- lib/matplotlib/tests/test_colorbar.py | 8 +-- lib/matplotlib/tests/test_image.py | 2 +- lib/matplotlib/tests/test_legend.py | 2 +- lib/matplotlib/tests/test_widgets.py | 11 ++-- lib/matplotlib/text.py | 36 +++++++----- lib/matplotlib/widgets.py | 28 +++++----- lib/mpl_toolkits/axes_grid1/axes_grid.py | 4 +- lib/mpl_toolkits/axes_grid1/inset_locator.py | 13 +++-- lib/mpl_toolkits/axes_grid1/parasite_axes.py | 3 +- .../axes_grid1/tests/test_axes_grid1.py | 2 +- lib/mpl_toolkits/axisartist/axis_artist.py | 16 +++--- lib/mpl_toolkits/axisartist/floating_axes.py | 2 +- lib/mpl_toolkits/mplot3d/axes3d.py | 15 ++--- lib/mpl_toolkits/mplot3d/axis3d.py | 2 +- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 17 +++--- 45 files changed, 282 insertions(+), 237 deletions(-) diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index b960f363e9d4..1689f68c2815 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -627,7 +627,7 @@ def get_pos_and_bbox(ax, renderer): bbox : `~matplotlib.transforms.Bbox` Tight bounding box in figure coordinates. """ - fig = ax.figure + fig = ax.get_figure(root=False) pos = ax.get_position(original=True) # pos is in panel co-ords, but we need in figure for the layout pos = pos.transformed(fig.transSubfigure - fig.transFigure) @@ -699,7 +699,7 @@ def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None): parents = cbax._colorbar_info['parents'] gs = parents[0].get_gridspec() - fig = cbax.figure + fig = cbax.get_figure(root=False) trans_fig_to_subfig = fig.transFigure - fig.transSubfigure cb_rspans, cb_cspans = get_cb_parent_spans(cbax) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 345a61bfc16a..b1fd1074e8ad 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -75,8 +75,8 @@ def draw_wrapper(artist, renderer): renderer.stop_filter(artist.get_agg_filter()) if artist.get_rasterized(): renderer._raster_depth -= 1 - if (renderer._rasterizing and artist.figure and - artist.figure.suppressComposite): + if (renderer._rasterizing and (fig := artist.get_figure(root=True)) and + fig.suppressComposite): # restart rasterizing to prevent merging renderer.stop_rasterizing() renderer.start_rasterizing() @@ -248,9 +248,9 @@ def remove(self): self.axes = None # decouple the artist from the Axes _ax_flag = True - if self.figure: + if (fig := self.get_figure(root=False)) is not None: if not _ax_flag: - self.figure.stale = True + fig.stale = True self._parent_figure = None else: @@ -473,8 +473,9 @@ def _different_canvas(self, event): return False, {} # subclass-specific implementation follows """ - return (getattr(event, "canvas", None) is not None and self.figure is not None - and event.canvas is not self.figure.canvas) + return (getattr(event, "canvas", None) is not None + and (fig := self.get_figure(root=False)) is not None + and event.canvas is not fig.canvas) def contains(self, mouseevent): """ @@ -504,7 +505,7 @@ def pickable(self): -------- .Artist.set_picker, .Artist.get_picker, .Artist.pick """ - return self.figure is not None and self._picker is not None + return self.get_figure(root=False) is not None and self._picker is not None def pick(self, mouseevent): """ @@ -526,7 +527,7 @@ def pick(self, mouseevent): else: inside, prop = self.contains(mouseevent) if inside: - PickEvent("pick_event", self.figure.canvas, + PickEvent("pick_event", self.get_figure(root=False).canvas, mouseevent, self, **prop)._process() # Pick children diff --git a/lib/matplotlib/artist.pyi b/lib/matplotlib/artist.pyi index 3059600e488c..be23f69d44a6 100644 --- a/lib/matplotlib/artist.pyi +++ b/lib/matplotlib/artist.pyi @@ -15,7 +15,7 @@ from .transforms import ( import numpy as np from collections.abc import Callable, Iterable -from typing import Any, NamedTuple, TextIO, overload, TypeVar +from typing import Any, Literal, NamedTuple, TextIO, overload, TypeVar from numpy.typing import ArrayLike _T_Artist = TypeVar("_T_Artist", bound=Artist) @@ -88,6 +88,11 @@ class Artist: ) -> None: ... def set_path_effects(self, path_effects: list[AbstractPathEffect]) -> None: ... def get_path_effects(self) -> list[AbstractPathEffect]: ... + @overload + def get_figure(self, root: Literal[True]) -> Figure | None: ... + @overload + def get_figure(self, root: Literal[False]) -> Figure | SubFigure | None: ... + @overload def get_figure(self, root: bool = ...) -> Figure | SubFigure | None: ... def set_figure(self, fig: Figure | SubFigure) -> None: ... def set_clip_box(self, clipbox: BboxBase | None) -> None: ... diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 5c236efbe429..9d8bb81abbfc 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -406,8 +406,9 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): # This puts the rectangle into figure-relative coordinates. inset_locator = _TransformedBoundsLocator(bounds, transform) bounds = inset_locator(self, None).bounds - projection_class, pkw = self.figure._process_projection_requirements(**kwargs) - inset_ax = projection_class(self.figure, bounds, zorder=zorder, **pkw) + fig = self.get_figure(root=False) + projection_class, pkw = fig._process_projection_requirements(**kwargs) + inset_ax = projection_class(fig, bounds, zorder=zorder, **pkw) # this locator lets the axes move if in data coordinates. # it gets called in `ax.apply_aspect() (of all places) @@ -515,7 +516,7 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, # decide which two of the lines to keep visible.... pos = inset_ax.get_position() - bboxins = pos.transformed(self.figure.transSubfigure) + bboxins = pos.transformed(self.get_figure(root=False).transSubfigure) rectbbox = mtransforms.Bbox.from_bounds( *bounds ).transformed(transform) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index a29583668a17..eeac93bdd4e3 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -115,7 +115,7 @@ def __call__(self, ax, renderer): # time as transSubfigure may otherwise change after this is evaluated. return mtransforms.TransformedBbox( mtransforms.Bbox.from_bounds(*self._bounds), - self._transform - ax.figure.transSubfigure) + self._transform - ax.get_figure(root=False).transSubfigure) def _process_plot_format(fmt, *, ambiguous_fmt_datakey=False): @@ -788,7 +788,7 @@ def get_subplotspec(self): def set_subplotspec(self, subplotspec): """Set the `.SubplotSpec`. associated with the subplot.""" self._subplotspec = subplotspec - self._set_position(subplotspec.get_position(self.figure)) + self._set_position(subplotspec.get_position(self.get_figure(root=False))) def get_gridspec(self): """Return the `.GridSpec` associated with the subplot, or None.""" @@ -959,8 +959,9 @@ def get_xaxis_text1_transform(self, pad_points): """ labels_align = mpl.rcParams["xtick.alignment"] return (self.get_xaxis_transform(which='tick1') + - mtransforms.ScaledTranslation(0, -1 * pad_points / 72, - self.figure.dpi_scale_trans), + mtransforms.ScaledTranslation( + 0, -1 * pad_points / 72, + self.get_figure(root=False).dpi_scale_trans), "top", labels_align) def get_xaxis_text2_transform(self, pad_points): @@ -985,8 +986,9 @@ def get_xaxis_text2_transform(self, pad_points): """ labels_align = mpl.rcParams["xtick.alignment"] return (self.get_xaxis_transform(which='tick2') + - mtransforms.ScaledTranslation(0, pad_points / 72, - self.figure.dpi_scale_trans), + mtransforms.ScaledTranslation( + 0, pad_points / 72, + self.get_figure(root=False).dpi_scale_trans), "bottom", labels_align) def get_yaxis_transform(self, which='grid'): @@ -1039,8 +1041,9 @@ def get_yaxis_text1_transform(self, pad_points): """ labels_align = mpl.rcParams["ytick.alignment"] return (self.get_yaxis_transform(which='tick1') + - mtransforms.ScaledTranslation(-1 * pad_points / 72, 0, - self.figure.dpi_scale_trans), + mtransforms.ScaledTranslation( + -1 * pad_points / 72, 0, + self.get_figure(root=False).dpi_scale_trans), labels_align, "right") def get_yaxis_text2_transform(self, pad_points): @@ -1065,8 +1068,9 @@ def get_yaxis_text2_transform(self, pad_points): """ labels_align = mpl.rcParams["ytick.alignment"] return (self.get_yaxis_transform(which='tick2') + - mtransforms.ScaledTranslation(pad_points / 72, 0, - self.figure.dpi_scale_trans), + mtransforms.ScaledTranslation( + pad_points / 72, 0, + self.get_figure(root=False).dpi_scale_trans), labels_align, "left") def _update_transScale(self): @@ -1173,7 +1177,7 @@ def get_axes_locator(self): def _set_artist_props(self, a): """Set the boilerplate props for artists added to Axes.""" - a.set_figure(self.figure) + a.set_figure(self.get_figure(root=False)) if not a.is_transform_set(): a.set_transform(self.transData) @@ -1347,7 +1351,7 @@ def __clear(self): # the other artists. We use the frame to draw the edges so we are # setting the edgecolor to None. self.patch = self._gen_axes_patch() - self.patch.set_figure(self.figure) + self.patch.set_figure(self.get_figure(root=False)) self.patch.set_facecolor(self._facecolor) self.patch.set_edgecolor('none') self.patch.set_linewidth(0) @@ -1522,7 +1526,7 @@ def _set_title_offset_trans(self, title_offset_points): """ self.titleOffsetTrans = mtransforms.ScaledTranslation( 0.0, title_offset_points / 72, - self.figure.dpi_scale_trans) + self.get_figure(root=False).dpi_scale_trans) for _title in (self.title, self._left_title, self._right_title): _title.set_transform(self.transAxes + self.titleOffsetTrans) _title.set_clip_box(None) @@ -1937,7 +1941,7 @@ def apply_aspect(self, position=None): self._set_position(position, which='active') return - trans = self.get_figure().transSubfigure + trans = self.get_figure(root=False).transSubfigure bb = mtransforms.Bbox.unit().transformed(trans) # this is the physical aspect of the panel (or figure): fig_aspect = bb.height / bb.width @@ -2274,7 +2278,7 @@ def add_child_axes(self, ax): self.child_axes.append(ax) ax._remove_method = functools.partial( - self.figure._remove_axes, owners=[self.child_axes]) + self.get_figure(root=False)._remove_axes, owners=[self.child_axes]) self.stale = True return ax @@ -3022,7 +3026,8 @@ def _update_title_position(self, renderer): axs = set() axs.update(self.child_axes) axs.update(self._twinned_axes.get_siblings(self)) - axs.update(self.figure._align_label_groups['title'].get_siblings(self)) + axs.update( + self.get_figure(root=False)._align_label_groups['title'].get_siblings(self)) for ax in self.child_axes: # Child positions must be updated first. locator = ax.get_axes_locator() @@ -3108,7 +3113,7 @@ def draw(self, renderer): for _axis in self._axis_map.values(): artists.remove(_axis) - if not self.figure.canvas.is_saving(): + if not self.get_figure(root=False).canvas.is_saving(): artists = [ a for a in artists if not a.get_animated() or isinstance(a, mimage.AxesImage)] @@ -3136,10 +3141,10 @@ def draw(self, renderer): artists = [self.patch] + artists if artists_rasterized: - _draw_rasterized(self.figure, artists_rasterized, renderer) + _draw_rasterized(self.get_figure(root=True), artists_rasterized, renderer) mimage._draw_list_compositing_images( - renderer, self, artists, self.figure.suppressComposite) + renderer, self, artists, self.get_figure(root=True).suppressComposite) renderer.close_group('axes') self.stale = False @@ -3148,7 +3153,7 @@ def draw_artist(self, a): """ Efficiently redraw a single artist. """ - a.draw(self.figure.canvas.get_renderer()) + a.draw(self.get_figure(root=False).canvas.get_renderer()) def redraw_in_frame(self): """ @@ -3158,7 +3163,7 @@ def redraw_in_frame(self): for artist in [*self._axis_map.values(), self.title, self._left_title, self._right_title]: stack.enter_context(artist._cm_set(visible=False)) - self.draw(self.figure.canvas.get_renderer()) + self.draw(self.get_figure(root=False).canvas.get_renderer()) # Axes rectangle characteristics @@ -4466,7 +4471,7 @@ def get_tightbbox(self, renderer=None, call_axes_locator=True, bb = [] if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() if not self.get_visible(): return None @@ -4517,9 +4522,9 @@ def _make_twin_axes(self, *args, **kwargs): raise ValueError("Twinned Axes may share only one axis") ss = self.get_subplotspec() if ss: - twin = self.figure.add_subplot(ss, *args, **kwargs) + twin = self.get_figure(root=False).add_subplot(ss, *args, **kwargs) else: - twin = self.figure.add_axes( + twin = self.get_figure(root=False).add_axes( self.get_position(True), *args, **kwargs, axes_locator=_TransformedBoundsLocator( [0, 0, 1, 1], self.transAxes)) @@ -4748,6 +4753,9 @@ def __init__(self, figure, artists): self.figure = figure self.artists = artists + def get_figure(self, root=False): + return self.figure + @martist.allow_rasterization def draw(self, renderer): for a in self.artists: diff --git a/lib/matplotlib/axes/_secondary_axes.py b/lib/matplotlib/axes/_secondary_axes.py index 3fabf49ebb38..b01acc4b127d 100644 --- a/lib/matplotlib/axes/_secondary_axes.py +++ b/lib/matplotlib/axes/_secondary_axes.py @@ -27,13 +27,14 @@ def __init__(self, parent, orientation, location, functions, transform=None, self._orientation = orientation self._ticks_set = False + fig = self._parent.get_figure(root=False) if self._orientation == 'x': - super().__init__(self._parent.figure, [0, 1., 1, 0.0001], **kwargs) + super().__init__(fig, [0, 1., 1, 0.0001], **kwargs) self._axis = self.xaxis self._locstrings = ['top', 'bottom'] self._otherstrings = ['left', 'right'] else: # 'y' - super().__init__(self._parent.figure, [0, 1., 0.0001, 1], **kwargs) + super().__init__(fig, [0, 1., 0.0001, 1], **kwargs) self._axis = self.yaxis self._locstrings = ['right', 'left'] self._otherstrings = ['top', 'bottom'] diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 1eb1b2331db3..ab10dbeb4a0a 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -102,7 +102,7 @@ def __init__( else: gridOn = False - self.set_figure(axes.figure) + self.set_figure(axes.get_figure(root=False)) self.axes = axes self._loc = loc @@ -321,7 +321,7 @@ def set_url(self, url): self.stale = True def _set_artist_props(self, a): - a.set_figure(self.figure) + a.set_figure(self.get_figure(root=False)) def get_view_interval(self): """ @@ -647,7 +647,7 @@ def __init__(self, axes, *, pickradius=15, clear=True): super().__init__() self._remove_overlapping_locs = True - self.set_figure(axes.figure) + self.set_figure(axes.get_figure(root=False)) self.isDefault_label = True @@ -1280,8 +1280,9 @@ def _set_lim(self, v0, v1, *, emit=True, auto): other._axis_map[name]._set_lim(v0, v1, emit=False, auto=auto) if emit: other.callbacks.process(f"{name}lim_changed", other) - if other.figure != self.figure: - other.figure.canvas.draw_idle() + if ((other_fig := other.get_figure(root=False)) != + self.get_figure(root=False)): + other_fig.canvas.draw_idle() self.stale = True return v0, v1 @@ -1289,7 +1290,7 @@ def _set_lim(self, v0, v1, *, emit=True, auto): def _set_artist_props(self, a): if a is None: return - a.set_figure(self.figure) + a.set_figure(self.get_figure(root=False)) def _update_ticks(self): """ @@ -1346,7 +1347,7 @@ def _update_ticks(self): def _get_ticklabel_bboxes(self, ticks, renderer=None): """Return lists of bboxes for ticks' label1's and label2's.""" if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() return ([tick.label1.get_window_extent(renderer) for tick in ticks if tick.label1.get_visible()], [tick.label2.get_window_extent(renderer) @@ -1365,7 +1366,7 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): if not self.get_visible(): return if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() ticks_to_draw = self._update_ticks() self._update_label_position(renderer) @@ -2185,9 +2186,9 @@ def _get_tick_boxes_siblings(self, renderer): """ # Get the Grouper keeping track of x or y label groups for this figure. name = self._get_axis_name() - if name not in self.figure._align_label_groups: + if name not in self.get_figure(root=False)._align_label_groups: return [], [] - grouper = self.figure._align_label_groups[name] + grouper = self.get_figure(root=False)._align_label_groups[name] bboxes = [] bboxes2 = [] # If we want to align labels from other Axes: @@ -2408,12 +2409,14 @@ def _update_label_position(self, renderer): # Union with extents of the bottom spine if present, of the axes otherwise. bbox = mtransforms.Bbox.union([ *bboxes, self.axes.spines.get("bottom", self.axes).get_window_extent()]) - self.label.set_position((x, bbox.y0 - self.labelpad * self.figure.dpi / 72)) + self.label.set_position( + (x, bbox.y0 - self.labelpad * self.get_figure(root=False).dpi / 72)) else: # Union with extents of the top spine if present, of the axes otherwise. bbox = mtransforms.Bbox.union([ *bboxes2, self.axes.spines.get("top", self.axes).get_window_extent()]) - self.label.set_position((x, bbox.y1 + self.labelpad * self.figure.dpi / 72)) + self.label.set_position( + (x, bbox.y1 + self.labelpad * self.get_figure(root=False).dpi / 72)) def _update_offset_text_position(self, bboxes, bboxes2): """ @@ -2429,14 +2432,14 @@ def _update_offset_text_position(self, bboxes, bboxes2): else: bbox = mtransforms.Bbox.union(bboxes) bottom = bbox.y0 - y = bottom - self.OFFSETTEXTPAD * self.figure.dpi / 72 + y = bottom - self.OFFSETTEXTPAD * self.get_figure(root=False).dpi / 72 else: if not len(bboxes2): top = self.axes.bbox.ymax else: bbox = mtransforms.Bbox.union(bboxes2) top = bbox.y1 - y = top + self.OFFSETTEXTPAD * self.figure.dpi / 72 + y = top + self.OFFSETTEXTPAD * self.get_figure(root=False).dpi / 72 self.offsetText.set_position((x, y)) def set_ticks_position(self, position): @@ -2533,7 +2536,7 @@ def set_default_intervals(self): def get_tick_space(self): ends = mtransforms.Bbox.unit().transformed( - self.axes.transAxes - self.figure.dpi_scale_trans) + self.axes.transAxes - self.get_figure(root=False).dpi_scale_trans) length = ends.width * 72 # There is a heuristic here that the aspect ratio of tick text # is no more than 3:1 @@ -2633,12 +2636,14 @@ def _update_label_position(self, renderer): # Union with extents of the left spine if present, of the axes otherwise. bbox = mtransforms.Bbox.union([ *bboxes, self.axes.spines.get("left", self.axes).get_window_extent()]) - self.label.set_position((bbox.x0 - self.labelpad * self.figure.dpi / 72, y)) + self.label.set_position( + (bbox.x0 - self.labelpad * self.get_figure(root=False).dpi / 72, y)) else: # Union with extents of the right spine if present, of the axes otherwise. bbox = mtransforms.Bbox.union([ *bboxes2, self.axes.spines.get("right", self.axes).get_window_extent()]) - self.label.set_position((bbox.x1 + self.labelpad * self.figure.dpi / 72, y)) + self.label.set_position( + (bbox.x1 + self.labelpad * self.get_figure(root=False).dpi / 72, y)) def _update_offset_text_position(self, bboxes, bboxes2): """ @@ -2653,7 +2658,7 @@ def _update_offset_text_position(self, bboxes, bboxes2): bbox = self.axes.bbox top = bbox.ymax self.offsetText.set_position( - (x, top + self.OFFSETTEXTPAD * self.figure.dpi / 72) + (x, top + self.OFFSETTEXTPAD * self.get_figure(root=False).dpi / 72) ) def set_offset_position(self, position): @@ -2761,7 +2766,7 @@ def set_default_intervals(self): def get_tick_space(self): ends = mtransforms.Bbox.unit().transformed( - self.axes.transAxes - self.figure.dpi_scale_trans) + self.axes.transAxes - self.get_figure(root=False).dpi_scale_trans) length = ends.height * 72 # Having a spacing of at least 2 just looks good. size = self._get_tick_label_size('y') * 2 diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 2c9f6188a97c..f3ad54a31a6d 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1514,13 +1514,13 @@ def _mouse_handler(event): # done with the internal _set_inaxes method which ensures that # the xdata and ydata attributes are also correct. try: + canvas = last_axes.get_figure(root=False).canvas leave_event = LocationEvent( - "axes_leave_event", last_axes.figure.canvas, + "axes_leave_event", canvas, event.x, event.y, event.guiEvent, modifiers=event.modifiers) leave_event._set_inaxes(last_axes) - last_axes.figure.canvas.callbacks.process( - "axes_leave_event", leave_event) + canvas.callbacks.process("axes_leave_event", leave_event) except Exception: pass # The last canvas may already have been torn down. if event.inaxes is not None: @@ -2496,27 +2496,27 @@ def _get_uniform_gridstate(ticks): scale = ax.get_yscale() if scale == 'log': ax.set_yscale('linear') - ax.figure.canvas.draw_idle() + ax.get_figure(root=False).canvas.draw_idle() elif scale == 'linear': try: ax.set_yscale('log') except ValueError as exc: _log.warning(str(exc)) ax.set_yscale('linear') - ax.figure.canvas.draw_idle() + ax.get_figure(root=False).canvas.draw_idle() # toggle scaling of x-axes between 'log and 'linear' (default key 'k') elif event.key in rcParams['keymap.xscale']: scalex = ax.get_xscale() if scalex == 'log': ax.set_xscale('linear') - ax.figure.canvas.draw_idle() + ax.get_figure(root=False).canvas.draw_idle() elif scalex == 'linear': try: ax.set_xscale('log') except ValueError as exc: _log.warning(str(exc)) ax.set_xscale('linear') - ax.figure.canvas.draw_idle() + ax.get_figure(root=False).canvas.draw_idle() def button_press_handler(event, canvas=None, toolbar=None): diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 00146cec3cb0..2e1a455883b0 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -392,8 +392,8 @@ def draw(self, renderer): else: combined_transform = transform extents = paths[0].get_extents(combined_transform) - if (extents.width < self.figure.bbox.width - and extents.height < self.figure.bbox.height): + if (extents.width < self.get_figure(root=True).bbox.width + and extents.height < self.get_figure(root=True).bbox.height): do_single_path_optimization = True if self._joinstyle: @@ -1001,7 +1001,7 @@ def set_sizes(self, sizes, dpi=72.0): @artist.allow_rasterization def draw(self, renderer): - self.set_sizes(self._sizes, self.figure.dpi) + self.set_sizes(self._sizes, self.get_figure(root=False).dpi) super().draw(renderer) @@ -1310,7 +1310,7 @@ def get_rotation(self): @artist.allow_rasterization def draw(self, renderer): - self.set_sizes(self._sizes, self.figure.dpi) + self.set_sizes(self._sizes, self.get_figure(root=False).dpi) self._transforms = [ transforms.Affine2D(x).rotate(-self._rotation).get_matrix() for x in self._transforms @@ -1757,7 +1757,7 @@ def _set_transforms(self): """Calculate transforms immediately before drawing.""" ax = self.axes - fig = self.figure + fig = self.get_figure(root=False) if self._units == 'xy': sc = 1 diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 0e6068c64b62..9d7c21e54d48 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -27,7 +27,7 @@ def _contour_labeler_event_handler(cs, inline, inline_spacing, event): - canvas = cs.axes.figure.canvas + canvas = cs.axes.get_figure(root=False).canvas is_button = event.name == "button_press_event" is_key = event.name == "key_press_event" # Quit (even if not in infinite mode; this is consistent with @@ -199,7 +199,8 @@ def clabel(self, levels=None, *, if not inline: print('Remove last label by clicking third mouse button.') mpl._blocking_input.blocking_input_loop( - self.axes.figure, ["button_press_event", "key_press_event"], + self.axes.get_figure(root=True), + ["button_press_event", "key_press_event"], timeout=-1, handler=functools.partial( _contour_labeler_event_handler, self, inline, inline_spacing)) @@ -222,7 +223,7 @@ def too_close(self, x, y, lw): def _get_nth_label_width(self, nth): """Return the width of the *nth* label, in pixels.""" - fig = self.axes.figure + fig = self.axes.get_figure(root=False) renderer = fig._get_renderer() return (Text(0, 0, self.get_text(self.labelLevelList[nth], self.labelFmt), diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 41d4b6078223..582a588e0983 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -625,7 +625,7 @@ def add_axes(self, *args, **kwargs): if isinstance(args[0], Axes): a, *extra_args = args key = a._projection_init - if a.get_figure() is not self: + if a.get_figure(root=False) is not self: raise ValueError( "The Axes must have been created in the present figure") else: @@ -756,7 +756,7 @@ def add_subplot(self, *args, **kwargs): and args[0].get_subplotspec()): ax = args[0] key = ax._projection_init - if ax.get_figure() is not self: + if ax.get_figure(root=False) is not self: raise ValueError("The Axes must have been created in " "the present figure") else: @@ -1282,7 +1282,7 @@ def colorbar( fig = ( # Figure of first Axes; logic copied from make_axes. [*ax.flat] if isinstance(ax, np.ndarray) else [*ax] if np.iterable(ax) - else [ax])[0].figure + else [ax])[0].get_figure(root=False) current_ax = fig.gca() if (fig.get_layout_engine() is not None and not fig.get_layout_engine().colorbar_gridspec): @@ -1297,24 +1297,21 @@ def colorbar( fig.sca(current_ax) cax.grid(visible=False, which='both', axis='both') - if hasattr(mappable, "figure") and mappable.figure is not None: - # Get top level artists - mappable_host_fig = mappable.figure - if isinstance(mappable_host_fig, mpl.figure.SubFigure): - mappable_host_fig = mappable_host_fig.figure + if (hasattr(mappable, "get_figure") and + (mappable_host_fig := mappable.get_figure(root=True)) is not None): # Warn in case of mismatch - if mappable_host_fig is not self.figure: + if mappable_host_fig is not self._root_figure: _api.warn_external( f'Adding colorbar to a different Figure ' - f'{repr(mappable.figure)} than ' - f'{repr(self.figure)} which ' + f'{repr(mappable_host_fig)} than ' + f'{repr(self._root_figure)} which ' f'fig.colorbar is called on.') NON_COLORBAR_KEYS = [ # remove kws that cannot be passed to Colorbar 'fraction', 'pad', 'shrink', 'aspect', 'anchor', 'panchor'] cb = cbar.Colorbar(cax, mappable, **{ k: v for k, v in kwargs.items() if k not in NON_COLORBAR_KEYS}) - cax.figure.stale = True + cax.get_figure(root=False).stale = True return cb def subplots_adjust(self, left=None, bottom=None, right=None, top=None, @@ -1829,7 +1826,7 @@ def get_tightbbox(self, renderer=None, bbox_extra_artists=None): """ if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() bb = [] if bbox_extra_artists is None: @@ -2421,7 +2418,7 @@ def draw(self, renderer): renderer.open_group('subfigure', gid=self.get_gid()) self.patch.draw(renderer) mimage._draw_list_compositing_images( - renderer, self, artists, self.figure.suppressComposite) + renderer, self, artists, self.get_figure(root=True).suppressComposite) renderer.close_group('subfigure') finally: diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi index 3c6876b3441b..f1363e06e55f 100644 --- a/lib/matplotlib/figure.pyi +++ b/lib/matplotlib/figure.pyi @@ -61,6 +61,12 @@ class FigureBase(Artist): def get_linewidth(self) -> float: ... def set_edgecolor(self, color: ColorType) -> None: ... def set_facecolor(self, color: ColorType) -> None: ... + @overload + def get_figure(self, root: Literal[True]) -> Figure: ... + @overload + def get_figure(self, root: Literal[False]) -> Figure | SubFigure: ... + @overload + def get_figure(self, root: bool = ...) -> Figure | SubFigure: ... def set_frameon(self, b: bool) -> None: ... @property def frameon(self) -> bool: ... diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index c6b363d36efa..06f0b2f7f781 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -391,8 +391,8 @@ def update(self, **kwargs): if ax.get_subplotspec() is not None: ss = ax.get_subplotspec().get_topmost_subplotspec() if ss.get_gridspec() == self: - ax._set_position( - ax.get_subplotspec().get_position(ax.figure)) + fig = ax.get_figure(root=False) + ax._set_position(ax.get_subplotspec().get_position(fig)) def get_subplot_params(self, figure=None): """ diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 3b4dd4c75b5d..4e7eb3e55a9f 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -978,7 +978,7 @@ def make_image(self, renderer, magnification=1.0, unsampled=False): bbox = Bbox(np.array([[x1, y1], [x2, y2]])) transformed_bbox = TransformedBbox(bbox, trans) clip = ((self.get_clip_box() or self.axes.bbox) if self.get_clip_on() - else self.figure.bbox) + else self.get_figure(root=True).bbox) return self._make_image(self._A, bbox, transformed_bbox, clip, magnification, unsampled=unsampled) @@ -1403,7 +1403,7 @@ def __init__(self, fig, cmap=cmap, origin=origin ) - self.figure = fig + self.set_figure(fig) self.ox = offsetx self.oy = offsety self._internal_update(kwargs) @@ -1417,14 +1417,15 @@ def get_extent(self): def make_image(self, renderer, magnification=1.0, unsampled=False): # docstring inherited - fac = renderer.dpi/self.figure.dpi + fig = self.get_figure(root=True) + fac = renderer.dpi/fig.dpi # fac here is to account for pdf, eps, svg backends where # figure.dpi is set to 72. This means we need to scale the # image (using magnification) and offset it appropriately. bbox = Bbox([[self.ox/fac, self.oy/fac], [(self.ox/fac + self._A.shape[1]), (self.oy/fac + self._A.shape[0])]]) - width, height = self.figure.get_size_inches() + width, height = fig.get_size_inches() width *= renderer.dpi height *= renderer.dpi clip = Bbox([[0, 0], [width, height]]) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 9033fc23c1a1..b2a544bdb1fb 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -98,8 +98,8 @@ def _update_bbox_to_anchor(self, loc_in_canvas): _legend_kw_doc_base = """ bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats Box that is used to position the legend in conjunction with *loc*. - Defaults to `axes.bbox` (if called as a method to `.Axes.legend`) or - `figure.bbox` (if `.Figure.legend`). This argument allows arbitrary + Defaults to ``axes.bbox`` (if called as a method to `.Axes.legend`) or + ``figure.bbox`` (if ``figure.legend``). This argument allows arbitrary placement of the legend. Bbox coordinates are interpreted in the coordinate system given by @@ -497,7 +497,7 @@ def __init__( if isinstance(parent, Axes): self.isaxes = True self.axes = parent - self.set_figure(parent.figure) + self.set_figure(parent.get_figure(root=False)) elif isinstance(parent, FigureBase): self.isaxes = False self.set_figure(parent) @@ -637,7 +637,7 @@ def _set_artist_props(self, a): """ Set the boilerplate props for artists added to Axes. """ - a.set_figure(self.figure) + a.set_figure(self.get_figure(root=False)) if self.isaxes: a.axes = self.axes @@ -943,7 +943,7 @@ def _init_legend_box(self, handles, labels, markerfirst=True): align=self._alignment, children=[self._legend_title_box, self._legend_handle_box]) - self._legend_box.set_figure(self.figure) + self._legend_box.set_figure(self.get_figure(root=False)) self._legend_box.axes = self.axes self.texts = text_list self.legend_handles = handle_list @@ -1065,7 +1065,7 @@ def get_title(self): def get_window_extent(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() return self._legend_box.get_window_extent(renderer=renderer) def get_tightbbox(self, renderer=None): diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 5a929070e32d..97076ad09cb8 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -466,7 +466,7 @@ def update_prop(self, legend_handle, orig_handle, legend): self._update_prop(legend_handle, orig_handle) - legend_handle.set_figure(legend.figure) + legend_handle.set_figure(legend.get_figure(root=False)) # legend._set_artist_props(legend_handle) legend_handle.set_clip_box(None) legend_handle.set_clip_path(None) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 72e74f4eb9c5..129348d51026 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -467,11 +467,12 @@ def contains(self, mouseevent): yt = xy[:, 1] # Convert pick radius from points to pixels - if self.figure is None: + fig = self.get_figure(root=False) + if fig is None: _log.warning('no figure set when check if mouse is on line') pixels = self._pickradius else: - pixels = self.figure.dpi / 72. * self._pickradius + pixels = fig.dpi / 72. * self._pickradius # The math involved in checking for containment (here and inside of # segment_hits) assumes that it is OK to overflow, so temporarily set @@ -640,7 +641,7 @@ def get_window_extent(self, renderer=None): ignore=True) # correct for marker size, if any if self._marker: - ms = (self._markersize / 72.0 * self.figure.dpi) * 0.5 + ms = (self._markersize / 72.0 * self.get_figure(root=False).dpi) * 0.5 bbox = bbox.padded(ms) return bbox @@ -1648,7 +1649,7 @@ def __init__(self, line): 'pick_event', self.onpick) self.ind = set() - canvas = property(lambda self: self.axes.figure.canvas) + canvas = property(lambda self: self.axes.get_figure(root=False).canvas) def process_selected(self, ind, xs, ys): """ diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 32c5bafcde1d..bde7e5f37c85 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -363,7 +363,7 @@ def get_bbox(self, renderer): def get_window_extent(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() bbox = self.get_bbox(renderer) try: # Some subclasses redefine get_offset to take no args. px, py = self.get_offset(bbox, renderer) @@ -644,7 +644,7 @@ def add_artist(self, a): a.set_transform(self.get_transform()) if self.axes is not None: a.axes = self.axes - fig = self.figure + fig = self.get_figure(root=False) if fig is not None: a.set_figure(fig) @@ -1356,7 +1356,7 @@ def get_fontsize(self): def get_window_extent(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() self.update_positions(renderer) return Bbox.union([child.get_window_extent(renderer) for child in self.get_children()]) @@ -1364,7 +1364,7 @@ def get_window_extent(self, renderer=None): def get_tightbbox(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() self.update_positions(renderer) return Bbox.union([child.get_tightbbox(renderer) for child in self.get_children()]) @@ -1412,8 +1412,9 @@ def draw(self, renderer): renderer.open_group(self.__class__.__name__, gid=self.get_gid()) self.update_positions(renderer) if self.arrow_patch is not None: - if self.arrow_patch.figure is None and self.figure is not None: - self.arrow_patch.figure = self.figure + if (self.arrow_patch.get_figure(root=False) is None and + (fig := self.get_figure(root=False)) is not None): + self.arrow_patch.set_figure(fig) self.arrow_patch.draw(renderer) self.patch.draw(renderer) self.offsetbox.draw(renderer) @@ -1468,7 +1469,7 @@ def __init__(self, ref_artist, use_blit=False): ] # A property, not an attribute, to maintain picklability. - canvas = property(lambda self: self.ref_artist.figure.canvas) + canvas = property(lambda self: self.ref_artist.get_figure(root=False).canvas) cids = property(lambda self: [ disconnect.args[0] for disconnect in self._disconnectors[:2]]) @@ -1480,7 +1481,7 @@ def on_motion(self, evt): if self._use_blit: self.canvas.restore_region(self.background) self.ref_artist.draw( - self.ref_artist.figure._get_renderer()) + self.ref_artist.get_figure(root=False)._get_renderer()) self.canvas.blit() else: self.canvas.draw() @@ -1493,10 +1494,9 @@ def on_pick(self, evt): if self._use_blit: self.ref_artist.set_animated(True) self.canvas.draw() - self.background = \ - self.canvas.copy_from_bbox(self.ref_artist.figure.bbox) - self.ref_artist.draw( - self.ref_artist.figure._get_renderer()) + fig = self.ref_artist.get_figure(root=False) + self.background = self.canvas.copy_from_bbox(fig.bbox) + self.ref_artist.draw(fig._get_renderer()) self.canvas.blit() self.save_offset() @@ -1508,7 +1508,7 @@ def on_release(self, event): self.ref_artist.set_animated(False) def _check_still_parented(self): - if self.ref_artist.figure is None: + if self.ref_artist.get_figure(root=False) is None: self.disconnect() return False else: @@ -1536,7 +1536,7 @@ def __init__(self, ref_artist, offsetbox, use_blit=False): def save_offset(self): offsetbox = self.offsetbox - renderer = offsetbox.figure._get_renderer() + renderer = offsetbox.get_figure(root=False)._get_renderer() offset = offsetbox.get_offset(offsetbox.get_bbox(renderer), renderer) self.offsetbox_x, self.offsetbox_y = offset self.offsetbox.set_offset(offset) @@ -1547,7 +1547,7 @@ def update_offset(self, dx, dy): def get_loc_in_canvas(self): offsetbox = self.offsetbox - renderer = offsetbox.figure._get_renderer() + renderer = offsetbox.get_figure(root=False)._get_renderer() bbox = offsetbox.get_bbox(renderer) ox, oy = offsetbox._offset loc_in_canvas = (ox + bbox.x0, oy + bbox.y0) diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 2899952634a9..3de227fd17f9 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -2161,7 +2161,7 @@ def segment_circle_intersect(x0, y0, x1, y1): # the unit circle in the same way that it is relative to the desired # ellipse. box_path_transform = ( - transforms.BboxTransformTo((self.axes or self.figure).bbox) + transforms.BboxTransformTo((self.axes or self.get_figure(root=False)).bbox) - self.get_transform()) box_path = Path.unit_rectangle().transformed(box_path_transform) @@ -4574,13 +4574,14 @@ def _get_xy(self, xy, s, axes=None): if axes is None: axes = self.axes xy = np.array(xy) + fig = self.get_figure(root=False) if s in ["figure points", "axes points"]: - xy *= self.figure.dpi / 72 + xy *= fig.dpi / 72 s = s.replace("points", "pixels") elif s == "figure fraction": - s = self.figure.transFigure + s = fig.transFigure elif s == "subfigure fraction": - s = self.figure.transSubfigure + s = fig.transSubfigure elif s == "axes fraction": s = axes.transAxes x, y = xy @@ -4595,7 +4596,7 @@ def _get_xy(self, xy, s, axes=None): return self._get_xy(self.xy, 'data') return ( self._get_xy(self.xy, self.xycoords) # converted data point - + xy * self.figure.dpi / 72) # converted offset + + xy * self.get_figure(root=False).dpi / 72) # converted offset elif s == 'polar': theta, r = x, y x = r * np.cos(theta) @@ -4604,13 +4605,13 @@ def _get_xy(self, xy, s, axes=None): return trans.transform((x, y)) elif s == 'figure pixels': # pixels from the lower left corner of the figure - bb = self.figure.figbbox + bb = self.get_figure(root=False).figbbox x = bb.x0 + x if x >= 0 else bb.x1 + x y = bb.y0 + y if y >= 0 else bb.y1 + y return x, y elif s == 'subfigure pixels': # pixels from the lower left corner of the figure - bb = self.figure.bbox + bb = self.get_figure(root=False).bbox x = bb.x0 + x if x >= 0 else bb.x1 + x y = bb.y0 + y if y >= 0 else bb.y1 + y return x, y diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 025155351f88..d30163db7743 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -341,9 +341,9 @@ class ThetaTick(maxis.XTick): def __init__(self, axes, *args, **kwargs): self._text1_translate = mtransforms.ScaledTranslation( - 0, 0, axes.figure.dpi_scale_trans) + 0, 0, axes.get_figure(root=False).dpi_scale_trans) self._text2_translate = mtransforms.ScaledTranslation( - 0, 0, axes.figure.dpi_scale_trans) + 0, 0, axes.get_figure(root=False).dpi_scale_trans) super().__init__(axes, *args, **kwargs) self.label1.set( rotation_mode='anchor', @@ -530,7 +530,7 @@ class _ThetaShift(mtransforms.ScaledTranslation): of the axes, or using the rlabel position (``'rlabel'``). """ def __init__(self, axes, pad, mode): - super().__init__(pad, pad, axes.figure.dpi_scale_trans) + super().__init__(pad, pad, axes.get_figure(root=False).dpi_scale_trans) self.set_children(axes._realViewLim) self.axes = axes self.mode = mode diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 629092d9517b..8ea85c289ac8 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -997,7 +997,7 @@ def figure( "Ignoring specified arguments in this call " f"because figure with num: {num.canvas.manager.num} already exists") _pylab_helpers.Gcf.set_active(num.canvas.manager) - return num.figure + return num.get_figure(root=True) next_num = max(allnums) + 1 if allnums else 1 fig_label = '' @@ -1362,8 +1362,9 @@ def sca(ax: Axes) -> None: # Mypy sees ax.figure as potentially None, # but if you are calling this, it won't be None # Additionally the slight difference between `Figure` and `FigureBase` mypy catches - figure(ax.figure) # type: ignore[arg-type] - ax.figure.sca(ax) # type: ignore[union-attr] + fig = ax.get_figure(root=False) + figure(fig) # type: ignore[arg-type] + fig.sca(ax) # type: ignore[union-attr] def cla() -> None: diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index 240d7737b516..589ed70e92a6 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -316,11 +316,11 @@ def __init__(self, Q, X, Y, U, label, @property def labelsep(self): - return self._labelsep_inches * self.Q.axes.figure.dpi + return self._labelsep_inches * self.Q.axes.get_figure(root=False).dpi def _init(self): - if True: # self._dpi_at_last_init != self.axes.figure.dpi - if self.Q._dpi_at_last_init != self.Q.axes.figure.dpi: + if True: # self._dpi_at_last_init != self.axes.get_figure().dpi + if self.Q._dpi_at_last_init != self.Q.axes.get_figure(root=False).dpi: self.Q._init() self._set_transform() with cbook._setattr_cm(self.Q, pivot=self.pivot[self.labelpos], @@ -341,7 +341,7 @@ def _init(self): self.vector.set_color(self.color) self.vector.set_transform(self.Q.get_transform()) self.vector.set_figure(self.get_figure()) - self._dpi_at_last_init = self.Q.axes.figure.dpi + self._dpi_at_last_init = self.Q.axes.get_figure(root=False).dpi def _text_shift(self): return { @@ -361,11 +361,12 @@ def draw(self, renderer): self.stale = False def _set_transform(self): + fig = self.Q.axes.get_figure(root=False) self.set_transform(_api.check_getitem({ "data": self.Q.axes.transData, "axes": self.Q.axes.transAxes, - "figure": self.Q.axes.figure.transFigure, - "inches": self.Q.axes.figure.dpi_scale_trans, + "figure": fig.transFigure, + "inches": fig.dpi_scale_trans, }, coordinates=self.coord)) def set_figure(self, fig): @@ -518,11 +519,11 @@ def _init(self): self.width = 0.06 * self.span / sn # _make_verts sets self.scale if not already specified - if (self._dpi_at_last_init != self.axes.figure.dpi + if (self._dpi_at_last_init != self.axes.get_figure(root=False).dpi and self.scale is None): self._make_verts(self.XY, self.U, self.V, self.angles) - self._dpi_at_last_init = self.axes.figure.dpi + self._dpi_at_last_init = self.axes.get_figure(root=False).dpi def get_datalim(self, transData): trans = self.get_transform() @@ -579,7 +580,7 @@ def _dots_per_unit(self, units): 'width': bb.width, 'height': bb.height, 'dots': 1., - 'inches': self.axes.figure.dpi, + 'inches': self.axes.get_figure(root=False).dpi, }, units=units) def _set_transform(self): diff --git a/lib/matplotlib/spines.py b/lib/matplotlib/spines.py index 39cb99c53d72..956c78c473c2 100644 --- a/lib/matplotlib/spines.py +++ b/lib/matplotlib/spines.py @@ -53,7 +53,7 @@ def __init__(self, axes, spine_type, path, **kwargs): """ super().__init__(**kwargs) self.axes = axes - self.set_figure(self.axes.figure) + self.set_figure(self.axes.get_figure(root=False)) self.spine_type = spine_type self.set_facecolor('none') self.set_edgecolor(mpl.rcParams['axes.edgecolor']) @@ -174,8 +174,9 @@ def get_window_extent(self, renderer=None): else: padout = 0.5 padin = 0.5 - padout = padout * tickl / 72 * self.figure.dpi - padin = padin * tickl / 72 * self.figure.dpi + dpi = self.get_figure(root=False).dpi + padout = padout * tickl / 72 * dpi + padin = padin * tickl / 72 * dpi if tick.tick1line.get_visible(): if self.spine_type == 'left': @@ -368,7 +369,7 @@ def get_spine_transform(self): offset_dots = amount * np.array(offset_vec) / 72 return (base_transform + mtransforms.ScaledTranslation( - *offset_dots, self.figure.dpi_scale_trans)) + *offset_dots, self.get_figure(root=False).dpi_scale_trans)) elif position_type == 'axes': if self.spine_type in ['left', 'right']: # keep y unchanged, fix x at amount diff --git a/lib/matplotlib/table.py b/lib/matplotlib/table.py index 7d8c8ec4c3f4..96c144406466 100644 --- a/lib/matplotlib/table.py +++ b/lib/matplotlib/table.py @@ -303,7 +303,7 @@ def __init__(self, ax, loc=None, bbox=None, **kwargs): "Unrecognized location {!r}. Valid locations are\n\t{}" .format(loc, '\n\t'.join(self.codes))) loc = self.codes[loc] - self.set_figure(ax.figure) + self.set_figure(ax.get_figure(root=False)) self._axes = ax self._loc = loc self._bbox = bbox @@ -354,7 +354,7 @@ def __setitem__(self, position, cell): except Exception as err: raise KeyError('Only tuples length 2 are accepted as ' 'coordinates') from err - cell.set_figure(self.figure) + cell.set_figure(self.get_figure(root=False)) cell.set_transform(self.get_transform()) cell.set_clip_on(False) self._cells[row, col] = cell @@ -389,7 +389,7 @@ def edges(self, value): self.stale = True def _approx_text_height(self): - return (self.FONTSIZE / 72.0 * self.figure.dpi / + return (self.FONTSIZE / 72.0 * self.get_figure(root=False).dpi / self._axes.bbox.height * 1.2) @allow_rasterization @@ -399,7 +399,7 @@ def draw(self, renderer): # Need a renderer to do hit tests on mouseevent; assume the last one # will do if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() if renderer is None: raise RuntimeError('No renderer defined') @@ -432,7 +432,7 @@ def contains(self, mouseevent): return False, {} # TODO: Return index of the cell containing the cursor so that the user # doesn't have to bind to each one individually. - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() if renderer is not None: boxes = [cell.get_window_extent(renderer) for (row, col), cell in self._cells.items() @@ -449,7 +449,7 @@ def get_children(self): def get_window_extent(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() self._update_positions(renderer) boxes = [cell.get_window_extent(renderer) for cell in self._cells.values()] diff --git a/lib/matplotlib/testing/widgets.py b/lib/matplotlib/testing/widgets.py index 748cdaccc7e9..fdec2eaa3db9 100644 --- a/lib/matplotlib/testing/widgets.py +++ b/lib/matplotlib/testing/widgets.py @@ -16,7 +16,7 @@ def get_ax(): fig, ax = plt.subplots(1, 1) ax.plot([0, 200], [0, 200]) ax.set_aspect(1.0) - ax.figure.canvas.draw() + fig.canvas.draw() return ax @@ -57,7 +57,7 @@ def mock_event(ax, button=1, xdata=0, ydata=0, key=None, step=1): (xdata, ydata)])[0] event.xdata, event.ydata = xdata, ydata event.inaxes = ax - event.canvas = ax.figure.canvas + event.canvas = ax.get_figure(root=False).canvas event.key = key event.step = step event.guiEvent = None diff --git a/lib/matplotlib/tests/test_agg.py b/lib/matplotlib/tests/test_agg.py index 6ca74ed400b1..d68ba6447068 100644 --- a/lib/matplotlib/tests/test_agg.py +++ b/lib/matplotlib/tests/test_agg.py @@ -181,8 +181,8 @@ def process_image(self, padded_src, dpi): shadow.update_from(line) # offset transform - transform = mtransforms.offset_copy(line.get_transform(), ax.figure, - x=4.0, y=-6.0, units='points') + transform = mtransforms.offset_copy( + line.get_transform(), fig, x=4.0, y=-6.0, units='points') shadow.set_transform(transform) # adjust zorder of the shadow lines so that it is drawn below the diff --git a/lib/matplotlib/tests/test_artist.py b/lib/matplotlib/tests/test_artist.py index edba2c179781..e75572d776eb 100644 --- a/lib/matplotlib/tests/test_artist.py +++ b/lib/matplotlib/tests/test_artist.py @@ -208,7 +208,7 @@ def test_remove(): for art in [im, ln]: assert art.axes is None - assert art.figure is None + assert art.get_figure() is None assert im not in ax._mouseover_set assert fig.stale diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 69a580fe515b..52496d0ef152 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6293,7 +6293,7 @@ def formatter_func(x, pos): ax.set_xticks([-1, 0, 1, 2, 3]) ax.set_xlim(-0.5, 2.5) - ax.figure.canvas.draw() + fig.canvas.draw() tick_texts = [tick.get_text() for tick in ax.xaxis.get_ticklabels()] assert tick_texts == ["", "", "unit value", "", ""] @@ -8923,11 +8923,11 @@ def test_cla_clears_children_axes_and_fig(): img = ax.imshow([[1]]) for art in lines + [img]: assert art.axes is ax - assert art.figure is fig + assert art.get_figure() is fig ax.clear() for art in lines + [img]: assert art.axes is None - assert art.figure is None + assert art.get_figure() is None def test_child_axes_removal(): diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py index 3a49f0ec08ec..3e1f524ed1c9 100644 --- a/lib/matplotlib/tests/test_backend_bases.py +++ b/lib/matplotlib/tests/test_backend_bases.py @@ -283,10 +283,11 @@ def test_toolbar_zoompan(): with pytest.warns(UserWarning, match=_EXPECTED_WARNING_TOOLMANAGER): plt.rcParams['toolbar'] = 'toolmanager' ax = plt.gca() + fig = ax.get_figure() assert ax.get_navigate_mode() is None - ax.figure.canvas.manager.toolmanager.trigger_tool('zoom') + fig.canvas.manager.toolmanager.trigger_tool('zoom') assert ax.get_navigate_mode() == "ZOOM" - ax.figure.canvas.manager.toolmanager.trigger_tool('pan') + fig.canvas.manager.toolmanager.trigger_tool('pan') assert ax.get_navigate_mode() == "PAN" diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 5e7937053496..9d7c161c4136 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -518,7 +518,7 @@ def get_transform(self): """Return transform scaling circle areas to data space.""" ax = self.axes - pts2pixels = 72.0 / ax.figure.dpi + pts2pixels = 72.0 / ax.get_figure(root=False).dpi scale_x = pts2pixels * ax.bbox.width / ax.viewLim.width scale_y = pts2pixels * ax.bbox.height / ax.viewLim.height diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index 35911afc7952..68ac920b813a 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -1180,12 +1180,12 @@ def test_title_text_loc(): def test_passing_location(fig_ref, fig_test): ax_ref = fig_ref.add_subplot() im = ax_ref.imshow([[0, 1], [2, 3]]) - ax_ref.figure.colorbar(im, cax=ax_ref.inset_axes([0, 1.05, 1, 0.05]), - orientation="horizontal", ticklocation="top") + ax_ref.get_figure().colorbar(im, cax=ax_ref.inset_axes([0, 1.05, 1, 0.05]), + orientation="horizontal", ticklocation="top") ax_test = fig_test.add_subplot() im = ax_test.imshow([[0, 1], [2, 3]]) - ax_test.figure.colorbar(im, cax=ax_test.inset_axes([0, 1.05, 1, 0.05]), - location="top") + ax_test.get_figure().colorbar(im, cax=ax_test.inset_axes([0, 1.05, 1, 0.05]), + location="top") @pytest.mark.parametrize("kwargs,error,message", [ diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 4340be96a38b..c73e712e5505 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -601,7 +601,7 @@ def test_bbox_image_inverted(): image = np.identity(10) bbox_im = BboxImage(TransformedBbox(Bbox([[0.1, 0.2], [0.3, 0.25]]), - ax.figure.transFigure), + ax.get_figure().transFigure), interpolation='nearest') bbox_im.set_data(image) bbox_im.set_clip_on(False) diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 0353f1408b73..f083c8374619 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -1259,7 +1259,7 @@ def test_subfigure_legend(): ax = subfig.subplots() ax.plot([0, 1], [0, 1], label="line") leg = subfig.legend() - assert leg.figure is subfig + assert leg.get_figure(root=False) is subfig def test_setting_alpha_keeps_polycollection_color(): diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 0f2cc411dbdf..9c4dafe7f5fa 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -862,7 +862,7 @@ def test_tool_line_handle(ax): def test_span_selector_bound(direction): fig, ax = plt.subplots(1, 1) ax.plot([10, 20], [10, 30]) - ax.figure.canvas.draw() + fig.canvas.draw() x_bound = ax.get_xbound() y_bound = ax.get_ybound() @@ -1109,7 +1109,7 @@ def test_RadioButtons(ax): @image_comparison(['check_radio_buttons.png'], style='mpl20', remove_text=True) def test_check_radio_buttons_image(): ax = get_ax() - fig = ax.figure + fig = ax.get_figure(root=False) fig.subplots_adjust(left=0.3) rax1 = fig.add_axes((0.05, 0.7, 0.2, 0.15)) @@ -1660,7 +1660,7 @@ def test_polygon_selector_box(ax): # In order to trigger the correct callbacks, trigger events on the canvas # instead of the individual tools t = ax.transData - canvas = ax.figure.canvas + canvas = ax.get_figure(root=False).canvas # Scale to half size using the top right corner of the bounding box MouseEvent( @@ -1722,7 +1722,8 @@ def test_polygon_selector_clear_method(ax): @pytest.mark.parametrize("horizOn", [False, True]) @pytest.mark.parametrize("vertOn", [False, True]) def test_MultiCursor(horizOn, vertOn): - (ax1, ax3) = plt.figure().subplots(2, sharex=True) + fig = plt.figure() + (ax1, ax3) = fig.subplots(2, sharex=True) ax2 = plt.figure().subplots() # useblit=false to avoid having to draw the figure to cache the renderer @@ -1740,7 +1741,7 @@ def test_MultiCursor(horizOn, vertOn): event = mock_event(ax1, xdata=.5, ydata=.25) multi.onmove(event) # force a draw + draw event to exercise clear - ax1.figure.canvas.draw() + fig.canvas.draw() # the lines in the first two ax should both move for l in multi.vlines: diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index af990ec1bf9f..d4a772d375be 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -372,7 +372,8 @@ def _get_layout(self, renderer): # Full vertical extent of font, including ascenders and descenders: _, lp_h, lp_d = _get_text_metrics_with_cache( renderer, "lp", self._fontproperties, - ismath="TeX" if self.get_usetex() else False, dpi=self.figure.dpi) + ismath="TeX" if self.get_usetex() else False, + dpi=self.get_figure(root=False).dpi) min_dy = (lp_h - lp_d) * self._linespacing for i, line in enumerate(lines): @@ -380,7 +381,7 @@ def _get_layout(self, renderer): if clean_line: w, h, d = _get_text_metrics_with_cache( renderer, clean_line, self._fontproperties, - ismath=ismath, dpi=self.figure.dpi) + ismath=ismath, dpi=self.get_figure(root=False).dpi) else: w = h = d = 0 @@ -934,28 +935,30 @@ def get_window_extent(self, renderer=None, dpi=None): dpi : float, optional The dpi value for computing the bbox, defaults to - ``self.figure.dpi`` (*not* the renderer dpi); should be set e.g. if + ``self.get_figure().dpi`` (*not* the renderer dpi); should be set e.g. if to match regions with a figure saved with a custom dpi value. """ if not self.get_visible(): return Bbox.unit() + + fig = self.get_figure(root=True) if dpi is None: - dpi = self.figure.dpi + dpi = fig.dpi if self.get_text() == '': - with cbook._setattr_cm(self.figure, dpi=dpi): + with cbook._setattr_cm(fig, dpi=dpi): tx, ty = self._get_xy_display() return Bbox.from_bounds(tx, ty, 0, 0) if renderer is not None: self._renderer = renderer if self._renderer is None: - self._renderer = self.figure._get_renderer() + self._renderer = fig._get_renderer() if self._renderer is None: raise RuntimeError( "Cannot get window extent of text w/o renderer. You likely " "want to call 'figure.draw_without_rendering()' first.") - with cbook._setattr_cm(self.figure, dpi=dpi): + with cbook._setattr_cm(fig, dpi=dpi): bbox, info, descent = self._get_layout(self._renderer) x, y = self.get_unitless_position() x, y = self.get_transform().transform((x, y)) @@ -1514,9 +1517,9 @@ def _get_xy_transform(self, renderer, coords): # if unit is offset-like if bbox_name == "figure": - bbox0 = self.figure.figbbox + bbox0 = self.get_figure(root=False).figbbox elif bbox_name == "subfigure": - bbox0 = self.figure.bbox + bbox0 = self.get_figure(root=False).bbox elif bbox_name == "axes": bbox0 = self.axes.bbox @@ -1529,11 +1532,13 @@ def _get_xy_transform(self, renderer, coords): raise ValueError(f"{coords!r} is not a valid coordinate") if unit == "points": - tr = Affine2D().scale(self.figure.dpi / 72) # dpi/72 dots per point + tr = Affine2D().scale( + self.get_figure(root=False).dpi / 72) # dpi/72 dots per point elif unit == "pixels": tr = Affine2D() elif unit == "fontsize": - tr = Affine2D().scale(self.get_size() * self.figure.dpi / 72) + tr = Affine2D().scale( + self.get_size() * self.get_figure(root=False).dpi / 72) elif unit == "fraction": tr = Affine2D().scale(*bbox0.size) else: @@ -1571,7 +1576,7 @@ def _get_position_xy(self, renderer): def _check_xy(self, renderer=None): """Check whether the annotation at *xy_pixel* should be drawn.""" if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() b = self.get_annotation_clip() if b or (b is None and self.xycoords == "data"): # check if self.xy is inside the Axes. @@ -1987,8 +1992,9 @@ def draw(self, renderer): self.update_positions(renderer) self.update_bbox_position_size(renderer) if self.arrow_patch is not None: # FancyArrowPatch - if self.arrow_patch.figure is None and self.figure is not None: - self.arrow_patch.figure = self.figure + if (self.arrow_patch.get_figure(root=False) is None and + (fig := self.get_figure(root=False)) is not None): + self.arrow_patch.set_figure(fig) self.arrow_patch.draw(renderer) # Draw text, including FancyBboxPatch, after FancyArrowPatch. # Otherwise, a wedge arrowstyle can land partly on top of the Bbox. @@ -2003,7 +2009,7 @@ def get_window_extent(self, renderer=None): if renderer is not None: self._renderer = renderer if self._renderer is None: - self._renderer = self.figure._get_renderer() + self._renderer = self.get_figure(root=False)._get_renderer() if self._renderer is None: raise RuntimeError('Cannot get window extent without renderer') diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index a298f3ae3d6a..a9792011d9d6 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -117,7 +117,7 @@ def __init__(self, ax): self.ax = ax self._cids = [] - canvas = property(lambda self: self.ax.figure.canvas) + canvas = property(lambda self: self.ax.get_figure(root=False).canvas) def connect_event(self, event, callback): """ @@ -569,7 +569,7 @@ def set_val(self, val): self._handle.set_xdata([val]) self.valtext.set_text(self._format(val)) if self.drawon: - self.ax.figure.canvas.draw_idle() + self.ax.get_figure(root=False).canvas.draw_idle() self.val = val if self.eventson: self._observers.process('changed', val) @@ -945,7 +945,7 @@ def set_val(self, val): self.valtext.set_text(self._format((vmin, vmax))) if self.drawon: - self.ax.figure.canvas.draw_idle() + self.ax.get_figure(root=False).canvas.draw_idle() self.val = (vmin, vmax) if self.eventson: self._observers.process("changed", (vmin, vmax)) @@ -1370,8 +1370,9 @@ def _rendercursor(self): # This causes a single extra draw if the figure has never been rendered # yet, which should be fine as we're going to repeatedly re-render the # figure later anyways. - if self.ax.figure._get_renderer() is None: - self.ax.figure.canvas.draw() + fig = self.ax.get_figure(root=False) + if fig._get_renderer() is None: + fig.canvas.draw() text = self.text_disp.get_text() # Save value before overwriting it. widthtext = text[:self.cursor_index] @@ -1393,7 +1394,7 @@ def _rendercursor(self): visible=True) self.text_disp.set_text(text) - self.ax.figure.canvas.draw() + fig.canvas.draw() def _release(self, event): if self.ignore(event): @@ -1456,7 +1457,7 @@ def begin_typing(self): stack = ExitStack() # Register cleanup actions when user stops typing. self._on_stop_typing = stack.close toolmanager = getattr( - self.ax.figure.canvas.manager, "toolmanager", None) + self.ax.get_figure(root=False).canvas.manager, "toolmanager", None) if toolmanager is not None: # If using toolmanager, lock keypresses, and plan to release the # lock when typing stops. @@ -1478,7 +1479,7 @@ def stop_typing(self): notifysubmit = False self.capturekeystrokes = False self.cursor.set_visible(False) - self.ax.figure.canvas.draw() + self.ax.get_figure(root=False).canvas.draw() if notifysubmit and self.eventson: # Because process() might throw an error in the user's code, only # call it once we've already done our cleanup. @@ -1509,7 +1510,7 @@ def _motion(self, event): if not colors.same_color(c, self.ax.get_facecolor()): self.ax.set_facecolor(c) if self.drawon: - self.ax.figure.canvas.draw() + self.ax.get_figure(root=False).canvas.draw() def on_text_change(self, func): """ @@ -2003,7 +2004,8 @@ def __init__(self, canvas, axes, *, useblit=True, horizOn=False, vertOn=True, self.vertOn = vertOn self._canvas_infos = { - ax.figure.canvas: {"cids": [], "background": None} for ax in axes} + ax.get_figure(root=False).canvas: + {"cids": [], "background": None} for ax in axes} xmin, xmax = axes[-1].get_xlim() ymin, ymax = axes[-1].get_ylim() @@ -2201,7 +2203,7 @@ def ignore(self, event): def update(self): """Draw using blit() or draw_idle(), depending on ``self.useblit``.""" if (not self.ax.get_visible() or - self.ax.figure._get_renderer() is None): + self.ax.get_figure(root=False)._get_renderer() is None): return if self.useblit: if self.background is not None: @@ -2574,7 +2576,7 @@ def __init__(self, ax, onselect, direction, *, minspan=0, useblit=False, def new_axes(self, ax, *, _props=None, _init=False): """Set SpanSelector to operate on a new Axes.""" reconnect = False - if _init or self.canvas is not ax.figure.canvas: + if _init or self.canvas is not ax.get_figure(root=False).canvas: if self.canvas is not None: self.disconnect_events() reconnect = True @@ -2627,7 +2629,7 @@ def _set_cursor(self, enabled): else: cursor = backend_tools.Cursors.POINTER - self.ax.figure.canvas.set_cursor(cursor) + self.ax.get_figure(root=False).canvas.set_cursor(cursor) def connect_default_events(self): # docstring inherited diff --git a/lib/mpl_toolkits/axes_grid1/axes_grid.py b/lib/mpl_toolkits/axes_grid1/axes_grid.py index 63888b1932ff..b5663364481e 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_grid.py +++ b/lib/mpl_toolkits/axes_grid1/axes_grid.py @@ -17,7 +17,7 @@ def __init__(self, *args, orientation, **kwargs): super().__init__(*args, **kwargs) def colorbar(self, mappable, **kwargs): - return self.figure.colorbar( + return self.get_figure(root=False).colorbar( mappable, cax=self, location=self.orientation, **kwargs) @_api.deprecated("3.8", alternative="ax.tick_params and colorbar.set_label") @@ -415,7 +415,7 @@ def _init_locators(self): self._colorbar_pad = self._vert_pad_size.fixed_size self.cbar_axes = [ _cbaraxes_class_factory(self._defaultAxesClass)( - self.axes_all[0].figure, self._divider.get_position(), + self.axes_all[0].get_figure(root=False), self._divider.get_position(), orientation=self._colorbar_location) for _ in range(self.ngrids)] diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index 6d591a45311b..c4fbd660fe4c 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -70,13 +70,14 @@ def draw(self, renderer): raise RuntimeError("No draw method should be called") def __call__(self, ax, renderer): + fig = ax.get_figure(root=False) if renderer is None: - renderer = ax.figure._get_renderer() + renderer = fig._get_renderer() self.axes = ax bbox = self.get_window_extent(renderer) px, py = self.get_offset(bbox.width, bbox.height, 0, 0, renderer) bbox_canvas = Bbox.from_bounds(px, py, bbox.width, bbox.height) - tr = ax.figure.transSubfigure.inverted() + tr = fig.transSubfigure.inverted() return TransformedBbox(bbox_canvas, tr) @@ -287,10 +288,11 @@ def _add_inset_axes(parent_axes, axes_class, axes_kwargs, axes_locator): axes_class = HostAxes if axes_kwargs is None: axes_kwargs = {} + fig = parent_axes.get_figure(root=False) inset_axes = axes_class( - parent_axes.figure, parent_axes.get_position(), + fig, parent_axes.get_position(), **{"navigate": False, **axes_kwargs, "axes_locator": axes_locator}) - return parent_axes.figure.add_axes(inset_axes) + return fig.add_axes(inset_axes) @_docstring.dedent_interpd @@ -395,7 +397,8 @@ def inset_axes(parent_axes, width, height, loc='upper right', Inset axes object created. """ - if (bbox_transform in [parent_axes.transAxes, parent_axes.figure.transFigure] + if (bbox_transform in [parent_axes.transAxes, + parent_axes.get_figure(root=False).transFigure] and bbox_to_anchor is None): _api.warn_external("Using the axes or figure transform requires a " "bounding box in the respective coordinates. " diff --git a/lib/mpl_toolkits/axes_grid1/parasite_axes.py b/lib/mpl_toolkits/axes_grid1/parasite_axes.py index 2a2b5957e844..b526cf4e628c 100644 --- a/lib/mpl_toolkits/axes_grid1/parasite_axes.py +++ b/lib/mpl_toolkits/axes_grid1/parasite_axes.py @@ -13,7 +13,8 @@ def __init__(self, parent_axes, aux_transform=None, self.transAux = aux_transform self.set_viewlim_mode(viewlim_mode) kwargs["frameon"] = False - super().__init__(parent_axes.figure, parent_axes._position, **kwargs) + super().__init__(parent_axes.get_figure(root=False), + parent_axes._position, **kwargs) def clear(self): super().clear() diff --git a/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py b/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py index d5a79a21c000..2bf37e1f6589 100644 --- a/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py +++ b/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py @@ -515,7 +515,7 @@ def on_pick(event): if click_axes is axes["parasite"]: click_axes = axes["host"] (x, y) = click_axes.transAxes.transform(axes_coords) - m = MouseEvent("button_press_event", click_axes.figure.canvas, x, y, + m = MouseEvent("button_press_event", click_axes.get_figure(root=False).canvas, x, y, button=1) click_axes.pick(m) # Checks diff --git a/lib/mpl_toolkits/axisartist/axis_artist.py b/lib/mpl_toolkits/axisartist/axis_artist.py index 407ad07a3dc2..244af8133765 100644 --- a/lib/mpl_toolkits/axisartist/axis_artist.py +++ b/lib/mpl_toolkits/axisartist/axis_artist.py @@ -253,7 +253,7 @@ def draw(self, renderer): def get_window_extent(self, renderer=None): if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() # save original and adjust some properties tr = self.get_transform() @@ -391,7 +391,7 @@ def draw(self, renderer): def get_window_extent(self, renderer=None): if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() if not self.get_visible(): return @@ -550,7 +550,7 @@ def set_locs_angles_labels(self, locs_angles_labels): def get_window_extents(self, renderer=None): if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() if not self.get_visible(): self._axislabel_pad = self._external_pad @@ -691,7 +691,7 @@ def __init__(self, axes, self.offset_transform = ScaledTranslation( *offset, Affine2D().scale(1 / 72) # points to inches. - + self.axes.figure.dpi_scale_trans) + + self.axes.get_figure(root=False).dpi_scale_trans) if axis_direction in ["left", "right"]: self.axis = axes.yaxis @@ -879,7 +879,7 @@ def _init_ticks(self, **kwargs): self.major_ticklabels = TickLabels( axis=self.axis, axis_direction=self._axis_direction, - figure=self.axes.figure, + figure=self.axes.get_figure(root=False), transform=trans, fontsize=size, pad=kwargs.get( @@ -888,7 +888,7 @@ def _init_ticks(self, **kwargs): self.minor_ticklabels = TickLabels( axis=self.axis, axis_direction=self._axis_direction, - figure=self.axes.figure, + figure=self.axes.get_figure(root=False), transform=trans, fontsize=size, pad=kwargs.get( @@ -922,7 +922,7 @@ def _update_ticks(self, renderer=None): # majorticks even for minor ticks. not clear what is best. if renderer is None: - renderer = self.figure._get_renderer() + renderer = self.get_figure(root=False)._get_renderer() dpi_cor = renderer.points_to_pixels(1.) if self.major_ticks.get_visible() and self.major_ticks.get_tick_out(): @@ -997,7 +997,7 @@ def _init_label(self, **kwargs): transform=tr, axis_direction=self._axis_direction, ) - self.label.set_figure(self.axes.figure) + self.label.set_figure(self.axes.get_figure(root=False)) labelpad = kwargs.get("labelpad", 5) self.label.set_pad(labelpad) diff --git a/lib/mpl_toolkits/axisartist/floating_axes.py b/lib/mpl_toolkits/axisartist/floating_axes.py index 24c9ce61afa7..ecdcca5122bf 100644 --- a/lib/mpl_toolkits/axisartist/floating_axes.py +++ b/lib/mpl_toolkits/axisartist/floating_axes.py @@ -266,7 +266,7 @@ def clear(self): # The original patch is not in the draw tree; it is only used for # clipping purposes. orig_patch = super()._gen_axes_patch() - orig_patch.set_figure(self.figure) + orig_patch.set_figure(self.get_figure(root=False)) orig_patch.set_transform(self.transAxes) self.patch.set_clip_path(orig_patch) self.gridlines.set_clip_path(orig_patch) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 12f3682ae5e9..8a951a4bd1b3 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -173,11 +173,12 @@ def __init__( self.fmt_zdata = None self.mouse_init() - self.figure.canvas.callbacks._connect_picklable( + fig = self.get_figure(root=False) + fig.canvas.callbacks._connect_picklable( 'motion_notify_event', self._on_move) - self.figure.canvas.callbacks._connect_picklable( + fig.canvas.callbacks._connect_picklable( 'button_press_event', self._button_press) - self.figure.canvas.callbacks._connect_picklable( + fig.canvas.callbacks._connect_picklable( 'button_release_event', self._button_release) self.set_top_view() @@ -1364,7 +1365,7 @@ def _button_press(self, event): if event.inaxes == self: self.button_pressed = event.button self._sx, self._sy = event.xdata, event.ydata - toolbar = self.figure.canvas.toolbar + toolbar = self.get_figure(root=False).canvas.toolbar if toolbar and toolbar._nav_stack() is None: toolbar.push_current() if toolbar: @@ -1372,7 +1373,7 @@ def _button_press(self, event): def _button_release(self, event): self.button_pressed = None - toolbar = self.figure.canvas.toolbar + toolbar = self.get_figure(root=False).canvas.toolbar # backend_bases.release_zoom and backend_bases.release_pan call # push_current, so check the navigation mode so we don't call it twice if toolbar and self.get_navigate_mode() is None: @@ -1605,7 +1606,7 @@ def _on_move(self, event): # Store the event coordinates for the next time through. self._sx, self._sy = x, y # Always request a draw update at the end of interaction - self.figure.canvas.draw_idle() + self.get_figure(root=False).canvas.draw_idle() def drag_pan(self, button, key, x, y): # docstring inherited @@ -3656,7 +3657,7 @@ def _extract_errs(err, data, lomask, himask): # them directly in planar form. quiversize = eb_cap_style.get('markersize', mpl.rcParams['lines.markersize']) ** 2 - quiversize *= self.figure.dpi / 72 + quiversize *= self.get_figure(root=False).dpi / 72 quiversize = self.transAxes.inverted().transform([ (0, 0), (quiversize, quiversize)]) quiversize = np.mean(np.diff(quiversize, axis=0)) diff --git a/lib/mpl_toolkits/mplot3d/axis3d.py b/lib/mpl_toolkits/mplot3d/axis3d.py index 79b78657bdb9..0562b421e22c 100644 --- a/lib/mpl_toolkits/mplot3d/axis3d.py +++ b/lib/mpl_toolkits/mplot3d/axis3d.py @@ -586,7 +586,7 @@ def draw(self, renderer): # Calculate offset distances # A rough estimate; points are ambiguous since 3D plots rotate - reltoinches = self.figure.dpi_scale_trans.inverted() + reltoinches = self.get_figure(root=False).dpi_scale_trans.inverted() ax_inches = reltoinches.transform(self.axes.bbox.size) ax_points_estimate = sum(72. * ax_inches) deltas_per_point = 48 / ax_points_estimate diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index f519b42098e5..8eadc727f757 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -667,8 +667,6 @@ def test_surface3d_label_offset_tick_position(): ax.set_ylabel("Y label") ax.set_zlabel("Z label") - ax.figure.canvas.draw() - @mpl3d_image_comparison(['surface3d_shaded.png'], style='mpl20') def test_surface3d_shaded(): @@ -1944,7 +1942,7 @@ def test_rotate(): fig = plt.figure() ax = fig.add_subplot(1, 1, 1, projection='3d') ax.view_init(0, 0, roll) - ax.figure.canvas.draw() + fig.canvas.draw() # drag mouse to change orientation ax._button_press( @@ -1952,7 +1950,7 @@ def test_rotate(): ax._on_move( mock_event(ax, button=MouseButton.LEFT, xdata=dx*ax._pseudo_w, ydata=dy*ax._pseudo_h)) - ax.figure.canvas.draw() + fig.canvas.draw() assert np.isclose(ax.elev, new_elev) assert np.isclose(ax.azim, new_azim) @@ -1968,9 +1966,10 @@ def convert_lim(dmin, dmax): range_ = dmax - dmin return center, range_ - ax = plt.figure().add_subplot(projection='3d') + fig = plt.figure() + ax = fig.add_subplot(projection='3d') ax.scatter(0, 0, 0) - ax.figure.canvas.draw() + fig.canvas.draw() x_center0, x_range0 = convert_lim(*ax.get_xlim3d()) y_center0, y_range0 = convert_lim(*ax.get_ylim3d()) @@ -2425,7 +2424,7 @@ def test_view_init_vertical_axis( rtol = 2e-06 ax = plt.subplot(1, 1, 1, projection="3d") ax.view_init(elev=0, azim=0, roll=0, vertical_axis=vertical_axis) - ax.figure.canvas.draw() + ax.get_figure().canvas.draw() # Assert the projection matrix: proj_actual = ax.get_proj() @@ -2451,7 +2450,7 @@ def test_on_move_vertical_axis(vertical_axis: str) -> None: """ ax = plt.subplot(1, 1, 1, projection="3d") ax.view_init(elev=0, azim=0, roll=0, vertical_axis=vertical_axis) - ax.figure.canvas.draw() + ax.get_figure().canvas.draw() proj_before = ax.get_proj() event_click = mock_event(ax, button=MouseButton.LEFT, xdata=0, ydata=1) @@ -2480,7 +2479,7 @@ def test_on_move_vertical_axis(vertical_axis: str) -> None: def test_set_box_aspect_vertical_axis(vertical_axis, aspect_expected): ax = plt.subplot(1, 1, 1, projection="3d") ax.view_init(elev=0, azim=0, roll=0, vertical_axis=vertical_axis) - ax.figure.canvas.draw() + ax.get_figure().canvas.draw() ax.set_box_aspect(None) From 9253f3da586d5806262c92894a846f060147ce65 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 20 Jul 2024 08:45:08 +0100 Subject: [PATCH 0053/1230] make _MinimalArtist.get_figure consistent with Artist.get_figure Co-authored-by: Thomas A Caswell --- lib/matplotlib/axes/_base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index eeac93bdd4e3..87c79659fcd1 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -4754,7 +4754,10 @@ def __init__(self, figure, artists): self.artists = artists def get_figure(self, root=False): - return self.figure + if root: + return self.figure.get_figure(root=True) + else: + return self.figure @martist.allow_rasterization def draw(self, renderer): From 0c911b38a8bcd8a9e4a55bf17f5fca9aaaaae09a Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 20 Jul 2024 09:15:52 +0100 Subject: [PATCH 0054/1230] Get dpi, canvas and renderer directly from the root Figure Co-authored-by: Thomas A Caswell --- lib/matplotlib/artist.py | 4 ++-- lib/matplotlib/axes/_base.py | 8 +++---- lib/matplotlib/axis.py | 18 +++++++-------- lib/matplotlib/backend_bases.py | 10 ++++----- lib/matplotlib/collections.py | 4 ++-- lib/matplotlib/contour.py | 4 ++-- lib/matplotlib/figure.py | 2 +- lib/matplotlib/legend.py | 2 +- lib/matplotlib/lines.py | 6 ++--- lib/matplotlib/offsetbox.py | 14 ++++++------ lib/matplotlib/patches.py | 2 +- lib/matplotlib/pyplot.py | 13 ++++++----- lib/matplotlib/quiver.py | 12 +++++----- lib/matplotlib/spines.py | 2 +- lib/matplotlib/table.py | 8 +++---- lib/matplotlib/testing/widgets.py | 2 +- lib/matplotlib/tests/test_collections.py | 2 +- lib/matplotlib/tests/test_widgets.py | 2 +- lib/matplotlib/text.py | 16 +++++++------- lib/matplotlib/widgets.py | 22 +++++++++---------- .../axes_grid1/tests/test_axes_grid1.py | 2 +- lib/mpl_toolkits/axisartist/axis_artist.py | 8 +++---- lib/mpl_toolkits/mplot3d/axes3d.py | 10 ++++----- 23 files changed, 87 insertions(+), 86 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index b1fd1074e8ad..a3bd882966df 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -474,7 +474,7 @@ def _different_canvas(self, event): # subclass-specific implementation follows """ return (getattr(event, "canvas", None) is not None - and (fig := self.get_figure(root=False)) is not None + and (fig := self.get_figure(root=True)) is not None and event.canvas is not fig.canvas) def contains(self, mouseevent): @@ -527,7 +527,7 @@ def pick(self, mouseevent): else: inside, prop = self.contains(mouseevent) if inside: - PickEvent("pick_event", self.get_figure(root=False).canvas, + PickEvent("pick_event", self.get_figure(root=True).canvas, mouseevent, self, **prop)._process() # Pick children diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 87c79659fcd1..fe0c0cfd755e 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -3113,7 +3113,7 @@ def draw(self, renderer): for _axis in self._axis_map.values(): artists.remove(_axis) - if not self.get_figure(root=False).canvas.is_saving(): + if not self.get_figure(root=True).canvas.is_saving(): artists = [ a for a in artists if not a.get_animated() or isinstance(a, mimage.AxesImage)] @@ -3153,7 +3153,7 @@ def draw_artist(self, a): """ Efficiently redraw a single artist. """ - a.draw(self.get_figure(root=False).canvas.get_renderer()) + a.draw(self.get_figure(root=True).canvas.get_renderer()) def redraw_in_frame(self): """ @@ -3163,7 +3163,7 @@ def redraw_in_frame(self): for artist in [*self._axis_map.values(), self.title, self._left_title, self._right_title]: stack.enter_context(artist._cm_set(visible=False)) - self.draw(self.get_figure(root=False).canvas.get_renderer()) + self.draw(self.get_figure(root=True).canvas.get_renderer()) # Axes rectangle characteristics @@ -4471,7 +4471,7 @@ def get_tightbbox(self, renderer=None, call_axes_locator=True, bb = [] if renderer is None: - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() if not self.get_visible(): return None diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index ab10dbeb4a0a..799a5e071eca 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1347,7 +1347,7 @@ def _update_ticks(self): def _get_ticklabel_bboxes(self, ticks, renderer=None): """Return lists of bboxes for ticks' label1's and label2's.""" if renderer is None: - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() return ([tick.label1.get_window_extent(renderer) for tick in ticks if tick.label1.get_visible()], [tick.label2.get_window_extent(renderer) @@ -1366,7 +1366,7 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): if not self.get_visible(): return if renderer is None: - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() ticks_to_draw = self._update_ticks() self._update_label_position(renderer) @@ -2410,13 +2410,13 @@ def _update_label_position(self, renderer): bbox = mtransforms.Bbox.union([ *bboxes, self.axes.spines.get("bottom", self.axes).get_window_extent()]) self.label.set_position( - (x, bbox.y0 - self.labelpad * self.get_figure(root=False).dpi / 72)) + (x, bbox.y0 - self.labelpad * self.get_figure(root=True).dpi / 72)) else: # Union with extents of the top spine if present, of the axes otherwise. bbox = mtransforms.Bbox.union([ *bboxes2, self.axes.spines.get("top", self.axes).get_window_extent()]) self.label.set_position( - (x, bbox.y1 + self.labelpad * self.get_figure(root=False).dpi / 72)) + (x, bbox.y1 + self.labelpad * self.get_figure(root=True).dpi / 72)) def _update_offset_text_position(self, bboxes, bboxes2): """ @@ -2432,14 +2432,14 @@ def _update_offset_text_position(self, bboxes, bboxes2): else: bbox = mtransforms.Bbox.union(bboxes) bottom = bbox.y0 - y = bottom - self.OFFSETTEXTPAD * self.get_figure(root=False).dpi / 72 + y = bottom - self.OFFSETTEXTPAD * self.get_figure(root=True).dpi / 72 else: if not len(bboxes2): top = self.axes.bbox.ymax else: bbox = mtransforms.Bbox.union(bboxes2) top = bbox.y1 - y = top + self.OFFSETTEXTPAD * self.get_figure(root=False).dpi / 72 + y = top + self.OFFSETTEXTPAD * self.get_figure(root=True).dpi / 72 self.offsetText.set_position((x, y)) def set_ticks_position(self, position): @@ -2637,13 +2637,13 @@ def _update_label_position(self, renderer): bbox = mtransforms.Bbox.union([ *bboxes, self.axes.spines.get("left", self.axes).get_window_extent()]) self.label.set_position( - (bbox.x0 - self.labelpad * self.get_figure(root=False).dpi / 72, y)) + (bbox.x0 - self.labelpad * self.get_figure(root=True).dpi / 72, y)) else: # Union with extents of the right spine if present, of the axes otherwise. bbox = mtransforms.Bbox.union([ *bboxes2, self.axes.spines.get("right", self.axes).get_window_extent()]) self.label.set_position( - (bbox.x1 + self.labelpad * self.get_figure(root=False).dpi / 72, y)) + (bbox.x1 + self.labelpad * self.get_figure(root=True).dpi / 72, y)) def _update_offset_text_position(self, bboxes, bboxes2): """ @@ -2658,7 +2658,7 @@ def _update_offset_text_position(self, bboxes, bboxes2): bbox = self.axes.bbox top = bbox.ymax self.offsetText.set_position( - (x, top + self.OFFSETTEXTPAD * self.get_figure(root=False).dpi / 72) + (x, top + self.OFFSETTEXTPAD * self.get_figure(root=True).dpi / 72) ) def set_offset_position(self, position): diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index f3ad54a31a6d..d5eeea3dd070 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1514,7 +1514,7 @@ def _mouse_handler(event): # done with the internal _set_inaxes method which ensures that # the xdata and ydata attributes are also correct. try: - canvas = last_axes.get_figure(root=False).canvas + canvas = last_axes.get_figure(root=True).canvas leave_event = LocationEvent( "axes_leave_event", canvas, event.x, event.y, event.guiEvent, @@ -2496,27 +2496,27 @@ def _get_uniform_gridstate(ticks): scale = ax.get_yscale() if scale == 'log': ax.set_yscale('linear') - ax.get_figure(root=False).canvas.draw_idle() + ax.get_figure(root=True).canvas.draw_idle() elif scale == 'linear': try: ax.set_yscale('log') except ValueError as exc: _log.warning(str(exc)) ax.set_yscale('linear') - ax.get_figure(root=False).canvas.draw_idle() + ax.get_figure(root=True).canvas.draw_idle() # toggle scaling of x-axes between 'log and 'linear' (default key 'k') elif event.key in rcParams['keymap.xscale']: scalex = ax.get_xscale() if scalex == 'log': ax.set_xscale('linear') - ax.get_figure(root=False).canvas.draw_idle() + ax.get_figure(root=True).canvas.draw_idle() elif scalex == 'linear': try: ax.set_xscale('log') except ValueError as exc: _log.warning(str(exc)) ax.set_xscale('linear') - ax.get_figure(root=False).canvas.draw_idle() + ax.get_figure(root=True).canvas.draw_idle() def button_press_handler(event, canvas=None, toolbar=None): diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 2e1a455883b0..ef333d396101 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -1001,7 +1001,7 @@ def set_sizes(self, sizes, dpi=72.0): @artist.allow_rasterization def draw(self, renderer): - self.set_sizes(self._sizes, self.get_figure(root=False).dpi) + self.set_sizes(self._sizes, self.get_figure(root=True).dpi) super().draw(renderer) @@ -1310,7 +1310,7 @@ def get_rotation(self): @artist.allow_rasterization def draw(self, renderer): - self.set_sizes(self._sizes, self.get_figure(root=False).dpi) + self.set_sizes(self._sizes, self.get_figure(root=True).dpi) self._transforms = [ transforms.Affine2D(x).rotate(-self._rotation).get_matrix() for x in self._transforms diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 9d7c21e54d48..bb668064b257 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -27,7 +27,7 @@ def _contour_labeler_event_handler(cs, inline, inline_spacing, event): - canvas = cs.axes.get_figure(root=False).canvas + canvas = cs.axes.get_figure(root=True).canvas is_button = event.name == "button_press_event" is_key = event.name == "key_press_event" # Quit (even if not in infinite mode; this is consistent with @@ -224,7 +224,7 @@ def too_close(self, x, y, lw): def _get_nth_label_width(self, nth): """Return the width of the *nth* label, in pixels.""" fig = self.axes.get_figure(root=False) - renderer = fig._get_renderer() + renderer = fig.get_figure(root=True)._get_renderer() return (Text(0, 0, self.get_text(self.labelLevelList[nth], self.labelFmt), figure=fig, fontproperties=self._label_font_props) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 582a588e0983..c712e0308bac 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -947,7 +947,7 @@ def _remove_axes(self, ax, owners): self._axobservers.process("_axes_change_event", self) self.stale = True - self.canvas.release_mouse(ax) + self._root_figure.canvas.release_mouse(ax) for name in ax._axis_names: # Break link between any shared Axes grouper = ax._shared_axes[name] diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index b2a544bdb1fb..7ef328e2007c 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -1065,7 +1065,7 @@ def get_title(self): def get_window_extent(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() return self._legend_box.get_window_extent(renderer=renderer) def get_tightbbox(self, renderer=None): diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 129348d51026..42b459d12f05 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -467,7 +467,7 @@ def contains(self, mouseevent): yt = xy[:, 1] # Convert pick radius from points to pixels - fig = self.get_figure(root=False) + fig = self.get_figure(root=True) if fig is None: _log.warning('no figure set when check if mouse is on line') pixels = self._pickradius @@ -641,7 +641,7 @@ def get_window_extent(self, renderer=None): ignore=True) # correct for marker size, if any if self._marker: - ms = (self._markersize / 72.0 * self.get_figure(root=False).dpi) * 0.5 + ms = (self._markersize / 72.0 * self.get_figure(root=True).dpi) * 0.5 bbox = bbox.padded(ms) return bbox @@ -1649,7 +1649,7 @@ def __init__(self, line): 'pick_event', self.onpick) self.ind = set() - canvas = property(lambda self: self.axes.get_figure(root=False).canvas) + canvas = property(lambda self: self.axes.get_figure(root=True).canvas) def process_selected(self, ind, xs, ys): """ diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index bde7e5f37c85..09904f582c4a 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -363,7 +363,7 @@ def get_bbox(self, renderer): def get_window_extent(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() bbox = self.get_bbox(renderer) try: # Some subclasses redefine get_offset to take no args. px, py = self.get_offset(bbox, renderer) @@ -1356,7 +1356,7 @@ def get_fontsize(self): def get_window_extent(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() self.update_positions(renderer) return Bbox.union([child.get_window_extent(renderer) for child in self.get_children()]) @@ -1364,7 +1364,7 @@ def get_window_extent(self, renderer=None): def get_tightbbox(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() self.update_positions(renderer) return Bbox.union([child.get_tightbbox(renderer) for child in self.get_children()]) @@ -1469,7 +1469,7 @@ def __init__(self, ref_artist, use_blit=False): ] # A property, not an attribute, to maintain picklability. - canvas = property(lambda self: self.ref_artist.get_figure(root=False).canvas) + canvas = property(lambda self: self.ref_artist.get_figure(root=True).canvas) cids = property(lambda self: [ disconnect.args[0] for disconnect in self._disconnectors[:2]]) @@ -1481,7 +1481,7 @@ def on_motion(self, evt): if self._use_blit: self.canvas.restore_region(self.background) self.ref_artist.draw( - self.ref_artist.get_figure(root=False)._get_renderer()) + self.ref_artist.get_figure(root=True)._get_renderer()) self.canvas.blit() else: self.canvas.draw() @@ -1536,7 +1536,7 @@ def __init__(self, ref_artist, offsetbox, use_blit=False): def save_offset(self): offsetbox = self.offsetbox - renderer = offsetbox.get_figure(root=False)._get_renderer() + renderer = offsetbox.get_figure(root=True)._get_renderer() offset = offsetbox.get_offset(offsetbox.get_bbox(renderer), renderer) self.offsetbox_x, self.offsetbox_y = offset self.offsetbox.set_offset(offset) @@ -1547,7 +1547,7 @@ def update_offset(self, dx, dy): def get_loc_in_canvas(self): offsetbox = self.offsetbox - renderer = offsetbox.get_figure(root=False)._get_renderer() + renderer = offsetbox.get_figure(root=True)._get_renderer() bbox = offsetbox.get_bbox(renderer) ox, oy = offsetbox._offset loc_in_canvas = (ox + bbox.x0, oy + bbox.y0) diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 3de227fd17f9..0064c0b70086 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -4596,7 +4596,7 @@ def _get_xy(self, xy, s, axes=None): return self._get_xy(self.xy, 'data') return ( self._get_xy(self.xy, self.xycoords) # converted data point - + xy * self.get_figure(root=False).dpi / 72) # converted offset + + xy * self.get_figure(root=True).dpi / 72) # converted offset elif s == 'polar': theta, r = x, y x = r * np.cos(theta) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 8ea85c289ac8..abf80ef95c0c 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -989,15 +989,16 @@ def figure( if isinstance(num, FigureBase): # type narrowed to `Figure | SubFigure` by combination of input and isinstance - if num.canvas.manager is None: + root_fig = num.get_figure(root=True) + if root_fig.canvas.manager is None: raise ValueError("The passed figure is not managed by pyplot") elif any([figsize, dpi, facecolor, edgecolor, not frameon, - kwargs]) and num.canvas.manager.num in allnums: + kwargs]) and root_fig.canvas.manager.num in allnums: _api.warn_external( - "Ignoring specified arguments in this call " - f"because figure with num: {num.canvas.manager.num} already exists") - _pylab_helpers.Gcf.set_active(num.canvas.manager) - return num.get_figure(root=True) + "Ignoring specified arguments in this call because figure " + f"with num: {root_fig.canvas.manager.num} already exists") + _pylab_helpers.Gcf.set_active(root_fig.canvas.manager) + return root_fig next_num = max(allnums) + 1 if allnums else 1 fig_label = '' diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index 589ed70e92a6..859118ef5c6c 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -316,11 +316,11 @@ def __init__(self, Q, X, Y, U, label, @property def labelsep(self): - return self._labelsep_inches * self.Q.axes.get_figure(root=False).dpi + return self._labelsep_inches * self.Q.axes.get_figure(root=True).dpi def _init(self): if True: # self._dpi_at_last_init != self.axes.get_figure().dpi - if self.Q._dpi_at_last_init != self.Q.axes.get_figure(root=False).dpi: + if self.Q._dpi_at_last_init != self.Q.axes.get_figure(root=True).dpi: self.Q._init() self._set_transform() with cbook._setattr_cm(self.Q, pivot=self.pivot[self.labelpos], @@ -341,7 +341,7 @@ def _init(self): self.vector.set_color(self.color) self.vector.set_transform(self.Q.get_transform()) self.vector.set_figure(self.get_figure()) - self._dpi_at_last_init = self.Q.axes.get_figure(root=False).dpi + self._dpi_at_last_init = self.Q.axes.get_figure(root=True).dpi def _text_shift(self): return { @@ -519,11 +519,11 @@ def _init(self): self.width = 0.06 * self.span / sn # _make_verts sets self.scale if not already specified - if (self._dpi_at_last_init != self.axes.get_figure(root=False).dpi + if (self._dpi_at_last_init != self.axes.get_figure(root=True).dpi and self.scale is None): self._make_verts(self.XY, self.U, self.V, self.angles) - self._dpi_at_last_init = self.axes.get_figure(root=False).dpi + self._dpi_at_last_init = self.axes.get_figure(root=True).dpi def get_datalim(self, transData): trans = self.get_transform() @@ -580,7 +580,7 @@ def _dots_per_unit(self, units): 'width': bb.width, 'height': bb.height, 'dots': 1., - 'inches': self.axes.get_figure(root=False).dpi, + 'inches': self.axes.get_figure(root=True).dpi, }, units=units) def _set_transform(self): diff --git a/lib/matplotlib/spines.py b/lib/matplotlib/spines.py index 956c78c473c2..1cec93b31db3 100644 --- a/lib/matplotlib/spines.py +++ b/lib/matplotlib/spines.py @@ -174,7 +174,7 @@ def get_window_extent(self, renderer=None): else: padout = 0.5 padin = 0.5 - dpi = self.get_figure(root=False).dpi + dpi = self.get_figure(root=True).dpi padout = padout * tickl / 72 * dpi padin = padin * tickl / 72 * dpi diff --git a/lib/matplotlib/table.py b/lib/matplotlib/table.py index 96c144406466..2656d9aeb89e 100644 --- a/lib/matplotlib/table.py +++ b/lib/matplotlib/table.py @@ -389,7 +389,7 @@ def edges(self, value): self.stale = True def _approx_text_height(self): - return (self.FONTSIZE / 72.0 * self.get_figure(root=False).dpi / + return (self.FONTSIZE / 72.0 * self.get_figure(root=True).dpi / self._axes.bbox.height * 1.2) @allow_rasterization @@ -399,7 +399,7 @@ def draw(self, renderer): # Need a renderer to do hit tests on mouseevent; assume the last one # will do if renderer is None: - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() if renderer is None: raise RuntimeError('No renderer defined') @@ -432,7 +432,7 @@ def contains(self, mouseevent): return False, {} # TODO: Return index of the cell containing the cursor so that the user # doesn't have to bind to each one individually. - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() if renderer is not None: boxes = [cell.get_window_extent(renderer) for (row, col), cell in self._cells.items() @@ -449,7 +449,7 @@ def get_children(self): def get_window_extent(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() self._update_positions(renderer) boxes = [cell.get_window_extent(renderer) for cell in self._cells.values()] diff --git a/lib/matplotlib/testing/widgets.py b/lib/matplotlib/testing/widgets.py index fdec2eaa3db9..3962567aa7c0 100644 --- a/lib/matplotlib/testing/widgets.py +++ b/lib/matplotlib/testing/widgets.py @@ -57,7 +57,7 @@ def mock_event(ax, button=1, xdata=0, ydata=0, key=None, step=1): (xdata, ydata)])[0] event.xdata, event.ydata = xdata, ydata event.inaxes = ax - event.canvas = ax.get_figure(root=False).canvas + event.canvas = ax.get_figure(root=True).canvas event.key = key event.step = step event.guiEvent = None diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 9d7c161c4136..f36ff801bcea 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -518,7 +518,7 @@ def get_transform(self): """Return transform scaling circle areas to data space.""" ax = self.axes - pts2pixels = 72.0 / ax.get_figure(root=False).dpi + pts2pixels = 72.0 / ax.get_figure(root=True).dpi scale_x = pts2pixels * ax.bbox.width / ax.viewLim.width scale_y = pts2pixels * ax.bbox.height / ax.viewLim.height diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 9c4dafe7f5fa..8fee80f8f52c 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1660,7 +1660,7 @@ def test_polygon_selector_box(ax): # In order to trigger the correct callbacks, trigger events on the canvas # instead of the individual tools t = ax.transData - canvas = ax.get_figure(root=False).canvas + canvas = ax.get_figure(root=True).canvas # Scale to half size using the top right corner of the bounding box MouseEvent( diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index d4a772d375be..6f59ca669d21 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -373,7 +373,7 @@ def _get_layout(self, renderer): _, lp_h, lp_d = _get_text_metrics_with_cache( renderer, "lp", self._fontproperties, ismath="TeX" if self.get_usetex() else False, - dpi=self.get_figure(root=False).dpi) + dpi=self.get_figure(root=True).dpi) min_dy = (lp_h - lp_d) * self._linespacing for i, line in enumerate(lines): @@ -381,7 +381,7 @@ def _get_layout(self, renderer): if clean_line: w, h, d = _get_text_metrics_with_cache( renderer, clean_line, self._fontproperties, - ismath=ismath, dpi=self.get_figure(root=False).dpi) + ismath=ismath, dpi=self.get_figure(root=True).dpi) else: w = h = d = 0 @@ -935,8 +935,8 @@ def get_window_extent(self, renderer=None, dpi=None): dpi : float, optional The dpi value for computing the bbox, defaults to - ``self.get_figure().dpi`` (*not* the renderer dpi); should be set e.g. if - to match regions with a figure saved with a custom dpi value. + ``self.get_figure(root=True).dpi`` (*not* the renderer dpi); should be set + e.g. if to match regions with a figure saved with a custom dpi value. """ if not self.get_visible(): return Bbox.unit() @@ -1533,12 +1533,12 @@ def _get_xy_transform(self, renderer, coords): if unit == "points": tr = Affine2D().scale( - self.get_figure(root=False).dpi / 72) # dpi/72 dots per point + self.get_figure(root=True).dpi / 72) # dpi/72 dots per point elif unit == "pixels": tr = Affine2D() elif unit == "fontsize": tr = Affine2D().scale( - self.get_size() * self.get_figure(root=False).dpi / 72) + self.get_size() * self.get_figure(root=True).dpi / 72) elif unit == "fraction": tr = Affine2D().scale(*bbox0.size) else: @@ -1576,7 +1576,7 @@ def _get_position_xy(self, renderer): def _check_xy(self, renderer=None): """Check whether the annotation at *xy_pixel* should be drawn.""" if renderer is None: - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() b = self.get_annotation_clip() if b or (b is None and self.xycoords == "data"): # check if self.xy is inside the Axes. @@ -2009,7 +2009,7 @@ def get_window_extent(self, renderer=None): if renderer is not None: self._renderer = renderer if self._renderer is None: - self._renderer = self.get_figure(root=False)._get_renderer() + self._renderer = self.get_figure(root=True)._get_renderer() if self._renderer is None: raise RuntimeError('Cannot get window extent without renderer') diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index a9792011d9d6..cb60925fb074 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -117,7 +117,7 @@ def __init__(self, ax): self.ax = ax self._cids = [] - canvas = property(lambda self: self.ax.get_figure(root=False).canvas) + canvas = property(lambda self: self.ax.get_figure(root=True).canvas) def connect_event(self, event, callback): """ @@ -569,7 +569,7 @@ def set_val(self, val): self._handle.set_xdata([val]) self.valtext.set_text(self._format(val)) if self.drawon: - self.ax.get_figure(root=False).canvas.draw_idle() + self.ax.get_figure(root=True).canvas.draw_idle() self.val = val if self.eventson: self._observers.process('changed', val) @@ -945,7 +945,7 @@ def set_val(self, val): self.valtext.set_text(self._format((vmin, vmax))) if self.drawon: - self.ax.get_figure(root=False).canvas.draw_idle() + self.ax.get_figure(root=True).canvas.draw_idle() self.val = (vmin, vmax) if self.eventson: self._observers.process("changed", (vmin, vmax)) @@ -1370,7 +1370,7 @@ def _rendercursor(self): # This causes a single extra draw if the figure has never been rendered # yet, which should be fine as we're going to repeatedly re-render the # figure later anyways. - fig = self.ax.get_figure(root=False) + fig = self.ax.get_figure(root=True) if fig._get_renderer() is None: fig.canvas.draw() @@ -1457,7 +1457,7 @@ def begin_typing(self): stack = ExitStack() # Register cleanup actions when user stops typing. self._on_stop_typing = stack.close toolmanager = getattr( - self.ax.get_figure(root=False).canvas.manager, "toolmanager", None) + self.ax.get_figure(root=True).canvas.manager, "toolmanager", None) if toolmanager is not None: # If using toolmanager, lock keypresses, and plan to release the # lock when typing stops. @@ -1479,7 +1479,7 @@ def stop_typing(self): notifysubmit = False self.capturekeystrokes = False self.cursor.set_visible(False) - self.ax.get_figure(root=False).canvas.draw() + self.ax.get_figure(root=True).canvas.draw() if notifysubmit and self.eventson: # Because process() might throw an error in the user's code, only # call it once we've already done our cleanup. @@ -1510,7 +1510,7 @@ def _motion(self, event): if not colors.same_color(c, self.ax.get_facecolor()): self.ax.set_facecolor(c) if self.drawon: - self.ax.get_figure(root=False).canvas.draw() + self.ax.get_figure(root=True).canvas.draw() def on_text_change(self, func): """ @@ -2004,7 +2004,7 @@ def __init__(self, canvas, axes, *, useblit=True, horizOn=False, vertOn=True, self.vertOn = vertOn self._canvas_infos = { - ax.get_figure(root=False).canvas: + ax.get_figure(root=True).canvas: {"cids": [], "background": None} for ax in axes} xmin, xmax = axes[-1].get_xlim() @@ -2203,7 +2203,7 @@ def ignore(self, event): def update(self): """Draw using blit() or draw_idle(), depending on ``self.useblit``.""" if (not self.ax.get_visible() or - self.ax.get_figure(root=False)._get_renderer() is None): + self.ax.get_figure(root=True)._get_renderer() is None): return if self.useblit: if self.background is not None: @@ -2576,7 +2576,7 @@ def __init__(self, ax, onselect, direction, *, minspan=0, useblit=False, def new_axes(self, ax, *, _props=None, _init=False): """Set SpanSelector to operate on a new Axes.""" reconnect = False - if _init or self.canvas is not ax.get_figure(root=False).canvas: + if _init or self.canvas is not ax.get_figure(root=True).canvas: if self.canvas is not None: self.disconnect_events() reconnect = True @@ -2629,7 +2629,7 @@ def _set_cursor(self, enabled): else: cursor = backend_tools.Cursors.POINTER - self.ax.get_figure(root=False).canvas.set_cursor(cursor) + self.ax.get_figure(root=True).canvas.set_cursor(cursor) def connect_default_events(self): # docstring inherited diff --git a/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py b/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py index 2bf37e1f6589..346fcc1d8f02 100644 --- a/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py +++ b/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py @@ -515,7 +515,7 @@ def on_pick(event): if click_axes is axes["parasite"]: click_axes = axes["host"] (x, y) = click_axes.transAxes.transform(axes_coords) - m = MouseEvent("button_press_event", click_axes.get_figure(root=False).canvas, x, y, + m = MouseEvent("button_press_event", click_axes.get_figure(root=True).canvas, x, y, button=1) click_axes.pick(m) # Checks diff --git a/lib/mpl_toolkits/axisartist/axis_artist.py b/lib/mpl_toolkits/axisartist/axis_artist.py index 244af8133765..d58313bd99ef 100644 --- a/lib/mpl_toolkits/axisartist/axis_artist.py +++ b/lib/mpl_toolkits/axisartist/axis_artist.py @@ -253,7 +253,7 @@ def draw(self, renderer): def get_window_extent(self, renderer=None): if renderer is None: - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() # save original and adjust some properties tr = self.get_transform() @@ -391,7 +391,7 @@ def draw(self, renderer): def get_window_extent(self, renderer=None): if renderer is None: - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() if not self.get_visible(): return @@ -550,7 +550,7 @@ def set_locs_angles_labels(self, locs_angles_labels): def get_window_extents(self, renderer=None): if renderer is None: - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() if not self.get_visible(): self._axislabel_pad = self._external_pad @@ -922,7 +922,7 @@ def _update_ticks(self, renderer=None): # majorticks even for minor ticks. not clear what is best. if renderer is None: - renderer = self.get_figure(root=False)._get_renderer() + renderer = self.get_figure(root=True)._get_renderer() dpi_cor = renderer.points_to_pixels(1.) if self.major_ticks.get_visible() and self.major_ticks.get_tick_out(): diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 8a951a4bd1b3..9343360782f3 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -173,7 +173,7 @@ def __init__( self.fmt_zdata = None self.mouse_init() - fig = self.get_figure(root=False) + fig = self.get_figure(root=True) fig.canvas.callbacks._connect_picklable( 'motion_notify_event', self._on_move) fig.canvas.callbacks._connect_picklable( @@ -1365,7 +1365,7 @@ def _button_press(self, event): if event.inaxes == self: self.button_pressed = event.button self._sx, self._sy = event.xdata, event.ydata - toolbar = self.get_figure(root=False).canvas.toolbar + toolbar = self.get_figure(root=True).canvas.toolbar if toolbar and toolbar._nav_stack() is None: toolbar.push_current() if toolbar: @@ -1373,7 +1373,7 @@ def _button_press(self, event): def _button_release(self, event): self.button_pressed = None - toolbar = self.get_figure(root=False).canvas.toolbar + toolbar = self.get_figure(root=True).canvas.toolbar # backend_bases.release_zoom and backend_bases.release_pan call # push_current, so check the navigation mode so we don't call it twice if toolbar and self.get_navigate_mode() is None: @@ -1606,7 +1606,7 @@ def _on_move(self, event): # Store the event coordinates for the next time through. self._sx, self._sy = x, y # Always request a draw update at the end of interaction - self.get_figure(root=False).canvas.draw_idle() + self.get_figure(root=True).canvas.draw_idle() def drag_pan(self, button, key, x, y): # docstring inherited @@ -3657,7 +3657,7 @@ def _extract_errs(err, data, lomask, himask): # them directly in planar form. quiversize = eb_cap_style.get('markersize', mpl.rcParams['lines.markersize']) ** 2 - quiversize *= self.get_figure(root=False).dpi / 72 + quiversize *= self.get_figure(root=True).dpi / 72 quiversize = self.transAxes.inverted().transform([ (0, 0), (quiversize, quiversize)]) quiversize = np.mean(np.diff(quiversize, axis=0)) From 45cd3714ed9650815131fd43af8319f7c6583ed0 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Wed, 3 Jul 2024 16:25:48 +0200 Subject: [PATCH 0055/1230] backend_svg: separate font attributes && use translate(x y) in RendedSVG._draw_text_as_path fixed linting (flake8 test failing) Fixed checks in testing for svg.fonttype = none fixed flake8 && failing tests linting modified tspans && fix to svg tests Updated baseline images for svg tests Update lib/matplotlib/testing/compare.py nicer regex to check if font syling in svg is specified Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Apply suggestions from code review More concise font testing in test_backend_svg.py Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Update lib/matplotlib/tests/test_mathtext.py Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Update backend_svg.py inlining `style` restored test images update to baseline images Fix to regex for testing/compare.py::compare forgot one baseline image --- lib/matplotlib/backends/backend_svg.py | 95 +- lib/matplotlib/testing/compare.py | 11 +- .../test_axes/axvspan_epoch.svg | 956 +++---- .../test_axes/errorbar_basic.svg | 1564 ++++++----- .../test_axes/errorbar_limits.svg | 2433 +++++++++-------- .../test_axes/errorbar_mixed.svg | 466 ++-- .../test_axes/nonfinite_limits.svg | 483 ++-- .../test_axes/single_point.svg | 1045 ++++--- .../twin_axis_locators_formatters.svg | 1519 +++++----- ...EventCollection_plot__append_positions.svg | 1219 +++++---- .../EventCollection_plot__default.svg | 945 ++++--- ...EventCollection_plot__extend_positions.svg | 1188 ++++---- .../EventCollection_plot__set_color.svg | 861 +++--- .../EventCollection_plot__set_linelength.svg | 965 +++---- .../EventCollection_plot__set_lineoffset.svg | 994 +++---- .../EventCollection_plot__set_linewidth.svg | 954 +++---- .../EventCollection_plot__set_positions.svg | 1107 ++++---- ...entCollection_plot__switch_orientation.svg | 1003 +++---- ...ollection_plot__switch_orientation__2x.svg | 1040 +++---- .../test_patches/clip_to_bbox.svg | 365 +-- .../clipping_with_nans.svg | 546 ++-- .../test_spines/spines_axes_positions.svg | 959 +++---- .../test_subplots/subplots_offset_text.svg | 1138 ++++---- .../baseline_images/test_text/font_styles.svg | 1582 ++++++----- .../baseline_images/test_text/multiline.svg | 862 +++--- .../test_tightlayout/tight_layout2.svg | 1015 +++---- .../test_tightlayout/tight_layout3.svg | 899 +++--- .../test_tightlayout/tight_layout4.svg | 1135 ++++---- lib/matplotlib/tests/test_backend_svg.py | 7 +- lib/matplotlib/tests/test_mathtext.py | 15 +- 30 files changed, 14072 insertions(+), 13299 deletions(-) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 9d27861e9a9c..84e4f96ad4a7 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1066,12 +1066,13 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): self._update_glyph_map_defs(glyph_map_new) for glyph_id, xposition, yposition, scale in glyph_info: - attrib = {'xlink:href': f'#{glyph_id}'} - if xposition != 0.0: - attrib['x'] = _short_float_fmt(xposition) - if yposition != 0.0: - attrib['y'] = _short_float_fmt(yposition) - writer.element('use', attrib=attrib) + writer.element( + 'use', + transform=_generate_transform([ + ('translate', (xposition, yposition)), + ('scale', (scale,)), + ]), + attrib={'xlink:href': f'#{glyph_id}'}) else: if ismath == "TeX": @@ -1109,25 +1110,26 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): writer = self.writer color = rgb2hex(gc.get_rgb()) - style = {} + font_style = {} + color_style = {} if color != '#000000': - style['fill'] = color + color_style['fill'] = color alpha = gc.get_alpha() if gc.get_forced_alpha() else gc.get_rgb()[3] if alpha != 1: - style['opacity'] = _short_float_fmt(alpha) + color_style['opacity'] = _short_float_fmt(alpha) if not ismath: attrib = {} - font_parts = [] + # Separate font style in their separate attributes if prop.get_style() != 'normal': - font_parts.append(prop.get_style()) + font_style['font-style'] = prop.get_style() if prop.get_variant() != 'normal': - font_parts.append(prop.get_variant()) + font_style['font-variant'] = prop.get_variant() weight = fm.weight_dict[prop.get_weight()] if weight != 400: - font_parts.append(f'{weight}') + font_style['font-weight'] = f'{weight}' def _normalize_sans(name): return 'sans-serif' if name in ['sans', 'sans serif'] else name @@ -1150,15 +1152,15 @@ def _get_all_quoted_names(prop): for entry in prop.get_family() for name in _expand_family_entry(entry)] - font_parts.extend([ - f'{_short_float_fmt(prop.get_size())}px', - # ensure expansion, quoting, and dedupe of font names - ", ".join(dict.fromkeys(_get_all_quoted_names(prop))) - ]) - style['font'] = ' '.join(font_parts) + font_style['font-size'] = f'{_short_float_fmt(prop.get_size())}px' + # ensure expansion, quoting, and dedupe of font names + font_style['font-family'] = ", ".join( + dict.fromkeys(_get_all_quoted_names(prop)) + ) + if prop.get_stretch() != 'normal': - style['font-stretch'] = prop.get_stretch() - attrib['style'] = _generate_css(style) + font_style['font-stretch'] = prop.get_stretch() + attrib['style'] = _generate_css({**font_style, **color_style}) if mtext and (angle == 0 or mtext.get_rotation_mode() == "anchor"): # If text anchoring can be supported, get the original @@ -1180,11 +1182,11 @@ def _get_all_quoted_names(prop): ha_mpl_to_svg = {'left': 'start', 'right': 'end', 'center': 'middle'} - style['text-anchor'] = ha_mpl_to_svg[mtext.get_ha()] + font_style['text-anchor'] = ha_mpl_to_svg[mtext.get_ha()] attrib['x'] = _short_float_fmt(ax) attrib['y'] = _short_float_fmt(ay) - attrib['style'] = _generate_css(style) + attrib['style'] = _generate_css({**font_style, **color_style}) attrib['transform'] = _generate_transform([ ("rotate", (-angle, ax, ay))]) @@ -1204,7 +1206,7 @@ def _get_all_quoted_names(prop): # Apply attributes to 'g', not 'text', because we likely have some # rectangles as well with the same style and transformation. writer.start('g', - style=_generate_css(style), + style=_generate_css({**font_style, **color_style}), transform=_generate_transform([ ('translate', (x, y)), ('rotate', (-angle,))]), @@ -1216,43 +1218,32 @@ def _get_all_quoted_names(prop): spans = {} for font, fontsize, thetext, new_x, new_y in glyphs: entry = fm.ttfFontProperty(font) - font_parts = [] + font_style = {} + # Separate font style in its separate attributes if entry.style != 'normal': - font_parts.append(entry.style) + font_style['font-style'] = entry.style if entry.variant != 'normal': - font_parts.append(entry.variant) + font_style['font-variant'] = entry.variant if entry.weight != 400: - font_parts.append(f'{entry.weight}') - font_parts.extend([ - f'{_short_float_fmt(fontsize)}px', - f'{entry.name!r}', # ensure quoting - ]) - style = {'font': ' '.join(font_parts)} + font_style['font-weight'] = f'{entry.weight}' + font_style['font-size'] = f'{_short_float_fmt(fontsize)}px' + font_style['font-family'] = f'{entry.name!r}' # ensure quoting if entry.stretch != 'normal': - style['font-stretch'] = entry.stretch - style = _generate_css(style) + font_style['font-stretch'] = entry.stretch + style = _generate_css({**font_style, **color_style}) if thetext == 32: thetext = 0xa0 # non-breaking space spans.setdefault(style, []).append((new_x, -new_y, thetext)) for style, chars in spans.items(): - chars.sort() - - if len({y for x, y, t in chars}) == 1: # Are all y's the same? - ys = str(chars[0][1]) - else: - ys = ' '.join(str(c[1]) for c in chars) - - attrib = { - 'style': style, - 'x': ' '.join(_short_float_fmt(c[0]) for c in chars), - 'y': ys - } - - writer.element( - 'tspan', - ''.join(chr(c[2]) for c in chars), - attrib=attrib) + chars.sort() # Sort by increasing x position + for x, y, t in chars: # Output one tspan for each character + writer.element( + 'tspan', + chr(t), + x=_short_float_fmt(x), + y=_short_float_fmt(y), + style=style) writer.end('text') diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index ee93061480e7..0f252bc1da8e 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -13,6 +13,7 @@ import sys from tempfile import TemporaryDirectory, TemporaryFile import weakref +import re import numpy as np from PIL import Image @@ -305,7 +306,15 @@ def convert(filename, cache): # re-generate any SVG test files using this mode, or else such tests will # fail to use the converter for the expected images (but will for the # results), and the tests will fail strangely. - if 'style="font:' in contents: + if re.search( + # searches for attributes : + # style=[font|font-size|font-weight| + # font-family|font-variant|font-style] + # taking care of the possibility of multiple style attributes + # before the font styling (i.e. opacity) + r'style="[^"]*font(|-size|-weight|-family|-variant|-style):', + contents # raw contents of the svg file + ): # for svg.fonttype = none, we explicitly patch the font search # path so that fonts shipped by Matplotlib are found. convert = _svg_with_matplotlib_fonts_converter diff --git a/lib/matplotlib/tests/baseline_images/test_axes/axvspan_epoch.svg b/lib/matplotlib/tests/baseline_images/test_axes/axvspan_epoch.svg index 54ce8e9308f5..d3260ae11ee6 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/axvspan_epoch.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/axvspan_epoch.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:36:36.965429 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,461 +35,476 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#pe2e2378c9e)" style="fill: #0000ff; opacity: 0.25; stroke: #000000; stroke-linejoin: miter"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - + - - - - + - - - - - +" transform="scale(0.015625)"/> + + + + + + + - - - - - - - - - - + + + + + + + + + + - + - + - + - - - - - - - - - - + + + + + + + + + + - + - + - + - - - - - - - - - - + + + + + + + + + + - + - + - - - - + + + + - - - - - - - - - - + + + + + + + + + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - + + + + + + + + + + - + - + - + - - - - - - - - - - + + + + + + + + + + - - + + - + - - +" transform="scale(0.015625)"/> + - + @@ -486,225 +512,233 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - - - + + + + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + @@ -712,8 +746,8 @@ Q 18.3125 60.0625 18.3125 54.390625 - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_basic.svg b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_basic.svg index e3940abeca5b..361aa8826a73 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_basic.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_basic.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:36:59.370109 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,120 +35,120 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff"/> - +" style="stroke: #0000ff; stroke-width: 0.5"/> - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - +" style="stroke: #0000ff; stroke-width: 0.5"/> - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - + - - - +" transform="scale(0.015625)"/> + + + - - - + + + - + - + - + - - + + - + - + - + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + @@ -533,654 +554,681 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - - - + + + - + - + - + - - - + + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - - - + + + - - - - + - - + - - - - + - + - - - + - + - - +" transform="scale(0.015625)"/> + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_limits.svg b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_limits.svg index 8ea6f573d94b..d7f2f4a27f92 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_limits.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_limits.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:37:00.187406 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,412 +35,698 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff"/> - - + - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #008000"/> - - + + - + - + - + + - + - + - - - - - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #008000"/> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000"/> - - + - + - + + - + + - + - + - + + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000"/> - - - - + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff00ff"/> - - + - + + + - + + + - + - - - - - + - - - - - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff00ff"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + - + + + - + + + - + - - - - - + - - - - - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #00ffff"/> - - + - + - + - + + - + - + + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #00ffff"/> - - - - - - - + - +" style="stroke: #00ffff; stroke-width: 0.5; stroke-linejoin: miter"/> - - - - - - - - - - - + + + + - - - - - - - - - - - - + + + + + + + + - + - +" style="stroke: #00ffff; stroke-width: 0.5; stroke-linejoin: miter"/> - - - - - - - - - - - + + - - - - - - - - - - - - + + + + + + - - + - - - - - - - - - - - - - - - - - +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #0000ff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #008000"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #ff0000"/> - - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #ff00ff"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + - +" style="stroke: #0000ff; stroke-width: 0.5"/> - - - - - - - - - - - + + + + + + + + + + + +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -907,32 +904,33 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -940,42 +938,43 @@ z - + - + - - - - + + + + @@ -983,50 +982,51 @@ Q 31.109375 20.453125 19.1875 8.296875 - + - + - - - - + + + + @@ -1034,36 +1034,38 @@ Q 46.96875 40.921875 40.578125 39.3125 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -1071,43 +1073,44 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -1117,557 +1120,583 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - + - - +" transform="scale(0.015625)"/> + - - - + + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - - + + - + - - + - - - + - - - - - + - - + + - - + - - - +M 3116 1747 +Q 3116 2381 2855 2742 +Q 2594 3103 2138 3103 +Q 1681 3103 1420 2742 +Q 1159 2381 1159 1747 +Q 1159 1113 1420 752 +Q 1681 391 2138 391 +Q 2594 391 2855 752 +Q 3116 1113 3116 1747 +z +" transform="scale(0.015625)"/> + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.svg b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.svg index b4d97d7d0e9f..d28b10e07376 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.svg @@ -6,11 +6,11 @@ - 2022-01-07T01:42:44.033823 + 2024-07-17T16:09:13.200518 image/svg+xml - Matplotlib v3.6.0.dev1138+gd48fca95df.d20220107, https://matplotlib.org/ + Matplotlib v0.1.0.dev50528+ga1cfe8b, https://matplotlib.org/ @@ -40,28 +40,28 @@ z +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff"/> @@ -69,7 +69,7 @@ L 214.036364 121.211578 L -3 -0 " style="stroke: #0000ff; stroke-width: 0.5"/> - + @@ -81,7 +81,7 @@ L -3 -0 - + @@ -106,7 +106,7 @@ C -1.55874 2.683901 -0.795609 3 0 3 z " style="stroke: #000000; stroke-width: 0.5"/> - + @@ -233,7 +233,7 @@ L -4 0 - + - - - + + + @@ -316,10 +316,10 @@ z - + - - + + @@ -336,10 +336,10 @@ z - + - - + + @@ -356,7 +356,7 @@ z - + - - + + @@ -392,17 +392,17 @@ z - + - - + + - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -621,28 +621,28 @@ z +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> @@ -650,7 +650,7 @@ L 472.224823 195.998601 L 0 -3 " style="stroke: #0000ff; stroke-opacity: 0.4; stroke-width: 0.5"/> - + @@ -662,7 +662,7 @@ L 0 -3 - + @@ -687,7 +687,7 @@ C -1.55874 2.683901 -0.795609 3 0 3 z " style="stroke: #000000; stroke-opacity: 0.4; stroke-width: 0.5"/> - + @@ -794,10 +794,10 @@ L 518.4 43.2 - + - - + + @@ -814,7 +814,7 @@ L 518.4 43.2 - + - - + + @@ -860,7 +860,7 @@ z - + - - + + @@ -901,7 +901,7 @@ z - + - - + + @@ -953,7 +953,7 @@ z - + - - + + @@ -1014,17 +1014,17 @@ z - + - - + + - + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -1209,54 +1209,54 @@ z +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff"/> @@ -1264,7 +1264,7 @@ L 214.036364 272.060219 L 0 -3 " style="stroke: #0000ff; stroke-width: 0.5"/> - + @@ -1276,7 +1276,7 @@ L 0 -3 - + @@ -1288,7 +1288,7 @@ L 0 -3 - + @@ -1300,7 +1300,7 @@ L 0 -3 - + @@ -1320,8 +1320,8 @@ L 175.990909 339.908877 L 188.672727 343.693421 L 201.354545 345.988863 L 214.036364 347.381119 -" clip-path="url(#p2c9259b7f3)" style="fill: none; stroke-dasharray: 6,6; stroke-dashoffset: 0; stroke: #0000ff"/> - +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke-dasharray: 6,6; stroke-dashoffset: 0; stroke: #0000ff"/> + @@ -1366,9 +1366,9 @@ L 274.909091 231.709091 - + - + @@ -1385,7 +1385,7 @@ L 274.909091 231.709091 - + @@ -1403,7 +1403,7 @@ L 274.909091 231.709091 - + @@ -1421,7 +1421,7 @@ L 274.909091 231.709091 - + @@ -1439,7 +1439,7 @@ L 274.909091 231.709091 - + @@ -1459,11 +1459,11 @@ L 274.909091 231.709091 - + - - - + + + @@ -1480,10 +1480,10 @@ L 274.909091 231.709091 - + - - + + @@ -1500,10 +1500,10 @@ L 274.909091 231.709091 - + - - + + @@ -1520,10 +1520,10 @@ L 274.909091 231.709091 - + - - + + @@ -1540,17 +1540,17 @@ L 274.909091 231.709091 - + - - + + - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -1592,54 +1592,54 @@ z +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #008000"/> @@ -1647,7 +1647,7 @@ L 457.527273 284.387119 L 0 -3 " style="stroke: #008000; stroke-width: 2"/> - + @@ -1659,7 +1659,7 @@ L 0 -3 - + @@ -1676,7 +1676,7 @@ L 0 -3 L -3 -0 " style="stroke: #008000; stroke-width: 2"/> - + @@ -1688,7 +1688,7 @@ L -3 -0 - + @@ -1700,7 +1700,7 @@ L -3 -0 - + @@ -1745,9 +1745,9 @@ L 518.4 231.709091 - + - + @@ -1764,7 +1764,7 @@ L 518.4 231.709091 - + @@ -1782,7 +1782,7 @@ L 518.4 231.709091 - + @@ -1800,7 +1800,7 @@ L 518.4 231.709091 - + @@ -1818,7 +1818,7 @@ L 518.4 231.709091 - + @@ -1837,12 +1837,12 @@ L 518.4 231.709091 - - + + - - + + @@ -1858,12 +1858,12 @@ L 518.4 231.709091 - - + + - - + + @@ -1880,10 +1880,10 @@ L 518.4 231.709091 - + - + @@ -1900,10 +1900,10 @@ L 518.4 231.709091 - + - + @@ -2208,7 +2208,7 @@ L -2 0 - + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - + - + - + - + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/nonfinite_limits.svg b/lib/matplotlib/tests/baseline_images/test_axes/nonfinite_limits.svg index c1a886b6ff5f..77cfb8afaffa 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/nonfinite_limits.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/nonfinite_limits.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:36:38.117527 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,10 +35,10 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - + + - - +M 2034 4750 +Q 2819 4750 3233 4129 +Q 3647 3509 3647 2328 +Q 3647 1150 3233 529 +Q 2819 -91 2034 -91 +Q 1250 -91 836 529 +Q 422 1150 422 2328 +Q 422 3509 836 4129 +Q 1250 4750 2034 4750 +z +" transform="scale(0.015625)"/> + + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + @@ -387,149 +405,152 @@ Q 46.96875 40.921875 40.578125 39.3125 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -537,17 +558,17 @@ z - + - + - + @@ -556,8 +577,8 @@ z - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/single_point.svg b/lib/matplotlib/tests/baseline_images/test_axes/single_point.svg index 5f940bb5a83c..de3b541c4f8a 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/single_point.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/single_point.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:36:36.453826 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,11 +35,11 @@ L 518.4 200.290909 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - - + + +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - - - - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - + - - - +M 2034 4750 +Q 2819 4750 3233 4129 +Q 3647 3509 3647 2328 +Q 3647 1150 3233 529 +Q 2819 -91 2034 -91 +Q 1250 -91 836 529 +Q 422 1150 422 2328 +Q 422 3509 836 4129 +Q 1250 4750 2034 4750 +z +" transform="scale(0.015625)"/> + + + - - - - + + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - - + + - - +" transform="scale(0.015625)"/> + - - - - + + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - - - - + + + + - - - - + + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + @@ -410,224 +389,196 @@ L 518.4 43.2 - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - - - - + + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - - + + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - - + + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pbfdf9b07b4)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + @@ -640,324 +591,302 @@ L 518.4 388.8 L 518.4 231.709091 L 72 231.709091 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - - - - + + + + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - - - - + + + + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - - + + - - +" transform="scale(0.015625)"/> + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + @@ -965,211 +894,183 @@ L 518.4 231.709091 - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + - - - - - +" clip-path="url(#pc45172f364)" style="fill: none; stroke-dasharray: 1,3; stroke-dashoffset: 0; stroke: #000000; stroke-width: 0.5"/> - + - + - + - - - + + + @@ -1177,11 +1078,11 @@ L 518.4 231.709091 - - + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/twin_axis_locators_formatters.svg b/lib/matplotlib/tests/baseline_images/test_axes/twin_axis_locators_formatters.svg index 37a6b88f3e73..12763588c0d5 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/twin_axis_locators_formatters.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/twin_axis_locators_formatters.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:36:34.083981 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,517 +35,535 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - + + - - +M 2034 4750 +Q 2819 4750 3233 4129 +Q 3647 3509 3647 2328 +Q 3647 1150 3233 529 +Q 2819 -91 2034 -91 +Q 1250 -91 836 529 +Q 422 1150 422 2328 +Q 422 3509 836 4129 +Q 1250 4750 2034 4750 +z +" transform="scale(0.015625)"/> + + - - - - + + + + - + - - + + - - +" transform="scale(0.015625)"/> + - - - - + + + + - + - - - - + + + + - - - - + + + + - + - - - - + + + + - - - - + + + + - + - - + + - - +" transform="scale(0.015625)"/> + - - - - + + + + - + - - + + - - +" transform="scale(0.015625)"/> + - - - - + + + + - + - - - - + + + + - - - - + + + + - + - - + + - - +" transform="scale(0.015625)"/> + - - - - + + + + - + - - - - + + + + - - - - + + + + - + - - - - + + + + - - - - + + + + - + - + - - - - - + + + + + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -542,12 +571,12 @@ Q 45.0625 54.296875 48.78125 52.59375 - + - + @@ -555,38 +584,40 @@ Q 45.0625 54.296875 48.78125 52.59375 - + - - - - + + + + @@ -594,33 +625,35 @@ Q 48.6875 17.390625 48.6875 27.296875 - + - - - - + + + + @@ -630,367 +663,381 @@ Q 18.84375 56 30.609375 56 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - - - - - - - + + + + + + + - + - + - - - - - - - + + + + + + + - + - + - - - - - - - + + + + + + + - + - + - - - - - - - + + + + + + + - + - + - - - - - - - + + + + + + + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - + - + - + - - - +" transform="scale(0.015625)"/> + + - - - - - + + + + + - + - - - - - - + + + + + + - - - + + + - + - - + + - + - - +M 3022 2063 +Q 3016 2534 2758 2815 +Q 2500 3097 2075 3097 +Q 1594 3097 1305 2825 +Q 1016 2553 972 2059 +L 3022 2063 +z +" transform="scale(0.015625)"/> + - - - + + + @@ -1000,116 +1047,116 @@ z +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + @@ -1119,116 +1166,116 @@ L 0 4 +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + @@ -1236,8 +1283,8 @@ L -4 0 - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__append_positions.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__append_positions.svg index dea1649a020e..9ceeb930cef2 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__append_positions.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__append_positions.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:26.264474 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,103 +35,105 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -128,188 +141,196 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - - - + + + + - + - + - + - - - - + + + + - + - + - + - - - - + + + + - + - + - + - - - - + + + + - + @@ -318,495 +339,521 @@ Q 18.3125 60.0625 18.3125 54.390625 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - - - + + + + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + - + - + - + - - + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__default.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__default.svg index ed927a817852..aac64d958b31 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__default.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__default.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:25.382570 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,100 +35,102 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -125,43 +138,44 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -169,97 +183,99 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - + - + - + - + - - - - + + + + - + @@ -268,425 +284,448 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - - + + + + + + + + + + - + - - - + - - - - - + + - + + - - - - + - - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__extend_positions.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__extend_positions.svg index 02e2f614bd11..c4b5c08c50c0 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__extend_positions.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__extend_positions.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:26.434024 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,106 +35,108 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -131,188 +144,196 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - - - + + + + - + - + - + - - - - + + + + - + - + - + - - - - + + + + - + - + - + - - - - + + + + - + @@ -321,478 +342,503 @@ Q 18.3125 60.0625 18.3125 54.390625 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - - - + + + + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + - + - + - + - - + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_color.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_color.svg index 1be6be46f002..29a9ad0368b4 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_color.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_color.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:27.893155 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,100 +35,102 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #00ffff; stroke-width: 2"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -125,43 +138,44 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -169,97 +183,99 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - + - + - + - + - - - - + + + + - + @@ -268,384 +284,403 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - - + + + + + + + + - - + - - + - - - + - - + - + + - - - + - + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linelength.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linelength.svg index c6864997c697..90b5fab01765 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linelength.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linelength.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:27.095376 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,100 +35,102 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -125,43 +138,44 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -169,97 +183,99 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - + - + - + - + - - - - + + + + - + @@ -268,114 +284,115 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - + - + - + - + @@ -383,17 +400,17 @@ z - + - + - + @@ -401,362 +418,382 @@ z - + - + - + - + - + - + - + - + - + - + - + - + - - + + + + + + + + + - + - + - - - - - + - - + + - + + - - - - + - - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_lineoffset.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_lineoffset.svg index ebb92a50afb4..b8dbf48bce65 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_lineoffset.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_lineoffset.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:27.255825 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,100 +35,102 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -125,43 +138,44 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -169,97 +183,99 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - + - + - + - + - - - - + + + + - + @@ -268,451 +284,475 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - + - - +M 3366 4563 +L 3366 3988 +Q 3128 4100 2886 4159 +Q 2644 4219 2406 4219 +Q 1781 4219 1451 3797 +Q 1122 3375 1075 2522 +Q 1259 2794 1537 2939 +Q 1816 3084 2150 3084 +Q 2853 3084 3261 2657 +Q 3669 2231 3669 1497 +Q 3669 778 3244 343 +Q 2819 -91 2113 -91 +Q 1303 -91 875 529 +Q 447 1150 447 2328 +Q 447 3434 972 4092 +Q 1497 4750 2381 4750 +Q 2619 4750 2861 4703 +Q 3103 4656 3366 4563 +z +" transform="scale(0.015625)"/> + + - - - + + + - + - + - + - - - + + + - + - + - + - - - + + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - - + + + - + - + - + - - - + + + - - + + + + + + + + - - + - - + - - - + - - + - + + - - - + - + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linewidth.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linewidth.svg index a4e63b10de3b..d9b33747f360 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linewidth.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_linewidth.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:27.736237 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,100 +35,102 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 5"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -125,43 +138,44 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -169,97 +183,99 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - + - + - + - + - - - - + + + + - + @@ -268,429 +284,451 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - - + + + + + + + + + + - + - - - + - - - - - + + + - + + - - - - + - - - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_positions.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_positions.svg index c6fb8e815924..e7ba87cd63c7 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_positions.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__set_positions.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:25.870012 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,109 +35,111 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -134,188 +147,196 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - - - + + + + - + - + - + - - - - + + + + - + - + - + - - - - + + + + - + - + - + - - - - + + + + - + @@ -324,437 +345,459 @@ Q 18.3125 60.0625 18.3125 54.390625 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - - - + + + + - - + + - + - + - - - - + + + + - - + + - + - + - + - - + + - + - + - + - - + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation.svg index 8d804f5b85fe..a467edef0196 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:26.604187 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,255 +35,261 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - + + - - +M 2034 4750 +Q 2819 4750 3233 4129 +Q 3647 3509 3647 2328 +Q 3647 1150 3233 529 +Q 2819 -91 2034 -91 +Q 1250 -91 836 529 +Q 422 1150 422 2328 +Q 422 3509 836 4129 +Q 1250 4750 2034 4750 +z +" transform="scale(0.015625)"/> + + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + @@ -281,27 +298,27 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + @@ -309,17 +326,17 @@ L -4 0 - + - + - + @@ -327,396 +344,418 @@ L -4 0 - + - + - + - + - + - + - + - + - + - + - + - + - - + + + + + + + + + - + - + - - - - - + - - + + - + + - - - + - - + - + - - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation__2x.svg b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation__2x.svg index 856034c6ffbb..0f7bde1e09d8 100644 --- a/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation__2x.svg +++ b/lib/matplotlib/tests/baseline_images/test_collections/EventCollection_plot__switch_orientation__2x.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:41:26.771522 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,100 +35,102 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #ff0000; stroke-width: 2"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -125,43 +138,44 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -169,97 +183,99 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + - + - + - + - + - + - + - + - - - - + + + + - + @@ -268,474 +284,498 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - - + + + + + + + + + - + - + - - - - - + - - + + - + + - - - + - - + - + - - + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_patches/clip_to_bbox.svg b/lib/matplotlib/tests/baseline_images/test_patches/clip_to_bbox.svg index c5f152be9748..9b7644a861c5 100644 --- a/lib/matplotlib/tests/baseline_images/test_patches/clip_to_bbox.svg +++ b/lib/matplotlib/tests/baseline_images/test_patches/clip_to_bbox.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:42:10.383159 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,10 +35,10 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#pe2e2378c9e)" style="fill: #ff7f50; opacity: 0.5"/> - +" clip-path="url(#pe2e2378c9e)" style="fill: #008000; opacity: 0.5; stroke: #000000; stroke-width: 4; stroke-linejoin: miter"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - + - + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - - - + + + + - - + + - + - + - + - + - + - + - + @@ -259,17 +275,17 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - + @@ -277,82 +293,83 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - + - + - + - + - + - + - + - + - - - - + + + + - + @@ -361,89 +378,89 @@ Q 31.109375 20.453125 19.1875 8.296875 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - - - + + + - + - + - + - - - + + + - + - + - + - - + + - + - + - + @@ -451,39 +468,39 @@ L -4 0 - + - + - + - + - + - + - + - - + + @@ -491,8 +508,8 @@ L -4 0 - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_simplification/clipping_with_nans.svg b/lib/matplotlib/tests/baseline_images/test_simplification/clipping_with_nans.svg index fbe862667424..9970c51bf07a 100644 --- a/lib/matplotlib/tests/baseline_images/test_simplification/clipping_with_nans.svg +++ b/lib/matplotlib/tests/baseline_images/test_simplification/clipping_with_nans.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:43:38.220504 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,10 +35,10 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -121,32 +134,33 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -154,42 +168,43 @@ z - + - + - - - - + + + + @@ -197,50 +212,51 @@ Q 31.109375 20.453125 19.1875 8.296875 - + - + - - - - + + + + @@ -248,36 +264,38 @@ Q 46.96875 40.921875 40.578125 39.3125 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -285,43 +303,44 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -329,47 +348,49 @@ z - + - + - - - - + + + + @@ -377,28 +398,29 @@ Q 48.484375 72.75 52.59375 71.296875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -408,126 +430,128 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - + - - +" transform="scale(0.015625)"/> + - - - + + + - + - + - + - - - + + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + @@ -535,8 +559,8 @@ z - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_spines/spines_axes_positions.svg b/lib/matplotlib/tests/baseline_images/test_spines/spines_axes_positions.svg index 3893e172bf78..4d0bb1aefc81 100644 --- a/lib/matplotlib/tests/baseline_images/test_spines/spines_axes_positions.svg +++ b/lib/matplotlib/tests/baseline_images/test_spines/spines_axes_positions.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:44:09.761597 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,10 +35,10 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#pe2e2378c9e)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -192,27 +205,28 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - - + + - - +" transform="scale(0.015625)"/> + @@ -220,37 +234,38 @@ z - + - - - - + + + + @@ -258,45 +273,46 @@ Q 31.109375 20.453125 19.1875 8.296875 - + - - - - + + + + @@ -304,31 +320,33 @@ Q 46.96875 40.921875 40.578125 39.3125 - + - - + + - - +" transform="scale(0.015625)"/> + @@ -336,38 +354,39 @@ z - + - - + + - - +" transform="scale(0.015625)"/> + @@ -375,42 +394,44 @@ z - + - - - - + + + + @@ -418,23 +439,24 @@ Q 48.484375 72.75 52.59375 71.296875 - + - - + + - - +" transform="scale(0.015625)"/> + @@ -444,376 +466,391 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - + - - +" transform="scale(0.015625)"/> + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + - - - + + + + + + + + + - - + - - - - - - + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_subplots/subplots_offset_text.svg b/lib/matplotlib/tests/baseline_images/test_subplots/subplots_offset_text.svg index 0c231c4d5c25..6f2498ca0912 100644 --- a/lib/matplotlib/tests/baseline_images/test_subplots/subplots_offset_text.svg +++ b/lib/matplotlib/tests/baseline_images/test_subplots/subplots_offset_text.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:44:47.680020 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,10 +35,10 @@ L 274.909091 200.290909 L 274.909091 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p6e0de7efff)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -195,48 +206,50 @@ L 0 4 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -244,32 +257,33 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -277,42 +291,43 @@ z - + - + - - - - + + + + @@ -320,50 +335,51 @@ Q 31.109375 20.453125 19.1875 8.296875 - + - + - - - - + + + + @@ -371,36 +387,38 @@ Q 46.96875 40.921875 40.578125 39.3125 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -408,43 +426,44 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -452,47 +471,49 @@ z - + - + - - - - + + + + @@ -500,28 +521,29 @@ Q 48.484375 72.75 52.59375 71.296875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -529,55 +551,58 @@ z - + - + - - - - + + + + @@ -585,82 +610,86 @@ Q 18.3125 60.0625 18.3125 54.390625 - + - + - - - - + + + + - - + + - - +M 3022 2063 +Q 3016 2534 2758 2815 +Q 2500 3097 2075 3097 +Q 1594 3097 1305 2825 +Q 1016 2553 972 2059 +L 3022 2063 +z +" transform="scale(0.015625)"/> + - - + + @@ -672,10 +701,10 @@ L 518.4 200.290909 L 518.4 43.2 L 315.490909 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p9022d9e00d)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -833,120 +862,120 @@ L 518.4 43.2 - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -959,10 +988,10 @@ L 274.909091 388.8 L 274.909091 231.709091 L 72 231.709091 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#pb81cb1308c)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + @@ -1016,17 +1045,17 @@ L 274.909091 231.709091 - + - + - + @@ -1034,17 +1063,17 @@ L 274.909091 231.709091 - + - + - + @@ -1052,17 +1081,17 @@ L 274.909091 231.709091 - + - + - + @@ -1070,17 +1099,17 @@ L 274.909091 231.709091 - + - + - + @@ -1088,17 +1117,17 @@ L 274.909091 231.709091 - + - + - + @@ -1106,17 +1135,17 @@ L 274.909091 231.709091 - + - + - + @@ -1124,17 +1153,17 @@ L 274.909091 231.709091 - + - + - + @@ -1142,17 +1171,17 @@ L 274.909091 231.709091 - + - + - + @@ -1160,27 +1189,27 @@ L 274.909091 231.709091 - + - + - + - + - - + + @@ -1188,17 +1217,17 @@ L 274.909091 231.709091 - + - + - + @@ -1206,17 +1235,17 @@ L 274.909091 231.709091 - + - + - + @@ -1224,17 +1253,17 @@ L 274.909091 231.709091 - + - + - + @@ -1242,17 +1271,17 @@ L 274.909091 231.709091 - + - + - + @@ -1260,17 +1289,17 @@ L 274.909091 231.709091 - + - + - + @@ -1278,17 +1307,17 @@ L 274.909091 231.709091 - + - + - + @@ -1296,17 +1325,17 @@ L 274.909091 231.709091 - + - + - + @@ -1314,17 +1343,17 @@ L 274.909091 231.709091 - + - + - + @@ -1332,17 +1361,17 @@ L 274.909091 231.709091 - + - + - + @@ -1350,27 +1379,27 @@ L 274.909091 231.709091 - + - + - + - + - - + + @@ -1382,10 +1411,10 @@ L 518.4 388.8 L 518.4 231.709091 L 315.490909 231.709091 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p964f42dabe)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + @@ -1439,197 +1468,198 @@ L 518.4 231.709091 - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - - + + + @@ -1637,120 +1667,120 @@ z - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -1758,17 +1788,17 @@ z - - + + - - + + - - + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_text/font_styles.svg b/lib/matplotlib/tests/baseline_images/test_text/font_styles.svg index e62797eb4c23..343b5c0bd3d7 100644 --- a/lib/matplotlib/tests/baseline_images/test_text/font_styles.svg +++ b/lib/matplotlib/tests/baseline_images/test_text/font_styles.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-09T01:10:40.151601 + image/svg+xml + + + Matplotlib v0.1.0.dev50524+g1791319.d20240709, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,802 +35,853 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_text/multiline.svg b/lib/matplotlib/tests/baseline_images/test_text/multiline.svg index 15bfc30bebdc..598c1d92d1c1 100644 --- a/lib/matplotlib/tests/baseline_images/test_text/multiline.svg +++ b/lib/matplotlib/tests/baseline_images/test_text/multiline.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-09T01:10:40.770401 + image/svg+xml + + + Matplotlib v0.1.0.dev50524+g1791319.d20240709, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,481 +35,502 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - - + + - - - +" transform="scale(0.015625)"/> + + - - - - - + + + + + - - + + - - - +" transform="scale(0.015625)"/> + + - + - - - - - + + + + + - + - - - - - + + + + + - - - - - + + + + + - + - - - - - + + + + + - + - - - - - + + + + + - - - - - - - - + + + + + + + + - + - - - - - + + + + + - - - + + + - - + - + + - - - - +" transform="scale(0.015625)"/> + + + - - - - - - - - + + + + + + + + - - + + + + - - + - - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout2.svg b/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout2.svg index c68cafc5e9c7..2075eca4868f 100644 --- a/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout2.svg +++ b/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout2.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:48:17.981141 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,288 +35,302 @@ L 271.943437 177.879375 L 271.943437 26.8475 L 52.433438 26.8475 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p471c10e632)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - + + - - +M 2034 4750 +Q 2819 4750 3233 4129 +Q 3647 3509 3647 2328 +Q 3647 1150 3233 529 +Q 2819 -91 2034 -91 +Q 1250 -91 836 529 +Q 422 1150 422 2328 +Q 422 3509 836 4129 +Q 1250 4750 2034 4750 +z +" transform="scale(0.015625)"/> + + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - - + + - + - + - + - - + - - +M 1159 2969 +Q 1341 3281 1617 3432 +Q 1894 3584 2278 3584 +Q 2916 3584 3314 3078 +Q 3713 2572 3713 1747 +Q 3713 922 3314 415 +Q 2916 -91 2278 -91 +Q 1894 -91 1617 61 +Q 1341 213 1159 525 +L 1159 0 +L 581 0 +L 581 4863 +L 1159 4863 +L 1159 2969 +z +" transform="scale(0.015625)"/> + + - - - - - - + + + + + + @@ -313,180 +338,186 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + - - + + - - +" transform="scale(0.015625)"/> + - - - - - - + + + + + + - - + + - + - + - - +" transform="scale(0.015625)"/> + - - - - + + + + @@ -497,104 +528,104 @@ L 553.463437 177.879375 L 553.463437 26.8475 L 333.953437 26.8475 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p4ddb23cc8e)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + @@ -602,84 +633,84 @@ L 553.463437 26.8475 - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + - + - - - - + + + + @@ -690,104 +721,104 @@ L 271.943437 387.399375 L 271.943437 236.3675 L 52.433438 236.3675 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p7d460e1d1c)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + @@ -795,84 +826,84 @@ L 271.943437 236.3675 - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + - + - - - - + + + + @@ -883,104 +914,104 @@ L 553.463437 387.399375 L 553.463437 236.3675 L 333.953437 236.3675 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p6f8bf01143)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + @@ -988,100 +1019,100 @@ L 553.463437 236.3675 - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + - + - - - - + + + + - - + + - - + + - - + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout3.svg b/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout3.svg index 88e6a404ac25..d7d66b644771 100644 --- a/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout3.svg +++ b/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout3.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:48:18.249876 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,288 +35,302 @@ L 271.943437 177.879375 L 271.943437 26.8475 L 52.433438 26.8475 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p471c10e632)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - + + - - +M 2034 4750 +Q 2819 4750 3233 4129 +Q 3647 3509 3647 2328 +Q 3647 1150 3233 529 +Q 2819 -91 2034 -91 +Q 1250 -91 836 529 +Q 422 1150 422 2328 +Q 422 3509 836 4129 +Q 1250 4750 2034 4750 +z +" transform="scale(0.015625)"/> + + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - - + + - + - + - + - - + - - +M 1159 2969 +Q 1341 3281 1617 3432 +Q 1894 3584 2278 3584 +Q 2916 3584 3314 3078 +Q 3713 2572 3713 1747 +Q 3713 922 3314 415 +Q 2916 -91 2278 -91 +Q 1894 -91 1617 61 +Q 1341 213 1159 525 +L 1159 0 +L 581 0 +L 581 4863 +L 1159 4863 +L 1159 2969 +z +" transform="scale(0.015625)"/> + + - - - - - - + + + + + + @@ -313,180 +338,186 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - - + + - + - + - + - - + + - + - + - - - - + + + + - - + + - - + + - - +" transform="scale(0.015625)"/> + - - - - - - + + + + + + - - + + - + - + - - +" transform="scale(0.015625)"/> + - - - - + + + + @@ -497,104 +528,104 @@ L 271.943437 387.399375 L 271.943437 236.3675 L 52.433438 236.3675 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p7d460e1d1c)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + @@ -602,84 +633,84 @@ L 271.943437 236.3675 - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + - + - - - - + + + + @@ -690,104 +721,104 @@ L 553.463437 387.399375 L 553.463437 26.8475 L 333.953437 26.8475 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p7f81023593)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + @@ -795,97 +826,97 @@ L 553.463437 26.8475 - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - - - - - - + + + + + + - + - - - - + + + + - - + + - - + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout4.svg b/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout4.svg index de5e5efd5a1a..27f30e5a363b 100644 --- a/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout4.svg +++ b/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout4.svg @@ -1,12 +1,23 @@ - - + + + + + + 2024-07-07T03:48:18.594236 + image/svg+xml + + + Matplotlib v0.1.0.dev50519+g9c53d4f.d20240707, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,293 +35,302 @@ L 178.103437 108.039375 L 178.103437 26.8475 L 52.433438 26.8475 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p2cd46811c6)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - + - - - - - +" transform="scale(0.015625)"/> + + + + - + - + - - + + - - - - - +" transform="scale(0.015625)"/> + + + + - + - + - - + + - - - - - +" transform="scale(0.015625)"/> + + + + - - + + - + - + - + - + - + - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + @@ -318,289 +338,294 @@ z - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + - + - + - - - - + + + + - + - + - - + + - - - - - +" transform="scale(0.015625)"/> + + + + - - + + - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + - - + + - + - + - - - - - - - +" transform="scale(0.015625)"/> + + + + + + +" style="fill: #ffffff"/> - + +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + +L 553.463438 108.039375 +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +L 553.463438 26.8475 +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - - - - + + + + - + - + - - - - + + + + - + - + - - - - + + + + - - - - - - - - + + + + + + + + @@ -608,192 +633,192 @@ L 553.463437 26.8475 - + - + - - - - + + + + - + - + - - - - + + + + - + - + - - - - + + + + - - - - - - - - + + + + + + + + - - - - - - + + + + + + +" style="fill: #ffffff"/> - + +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + +L 365.783438 387.399375 +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +L 365.783438 166.5275 +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - - - - + + + + - + - + - - - - + + + + - + - + - - - - + + + + - - - - - - - - + + + + + + + + @@ -801,192 +826,192 @@ L 365.783437 166.5275 - + - + - - - - + + + + - + - + - - - - + + + + - + - + - - - - + + + + - - - - - - - - + + + + + + + + - - - - - - + + + + + + - +" style="fill: #ffffff"/> - + - + - + - + - + - + - + - - - - + + + + - + - + - - - - + + + + - + - + - - - - + + + + - - - - - - - - + + + + + + + + @@ -994,100 +1019,100 @@ L 553.463437 166.5275 - + - + - - - - + + + + - + - + - - - - + + + + - + - + - - - - + + + + - - - - - - - - + + + + + + + + - - - - - - + + + + + + - - + + - - + + - - + + - - + + diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index b13cabe67614..689495eb31ac 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -631,12 +631,13 @@ def test_svg_font_string(font_str, include_generic): text_count = 0 for text_element in tree.findall(f".//{{{ns}}}text"): text_count += 1 - font_info = dict( + font_style = dict( map(lambda x: x.strip(), _.strip().split(":")) for _ in dict(text_element.items())["style"].split(";") - )["font"] + ) - assert font_info == f"{size}px {font_str}" + assert font_style["font-size"] == f"{size}px" + assert font_style["font-family"] == font_str assert text_count == len(ax.texts) diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index 6ce327f38341..f12c859b311c 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -4,7 +4,6 @@ from pathlib import Path import platform import re -import shlex from xml.etree import ElementTree as ET from typing import Any @@ -198,8 +197,8 @@ *('}' for font in fonts), '$', ]) - for set in chars: - font_tests.append(wrapper % set) + for font_set in chars: + font_tests.append(wrapper % font_set) @pytest.fixture @@ -433,7 +432,7 @@ def test_mathtext_fallback_invalid(): @pytest.mark.parametrize( "fallback,fontlist", [("cm", ['DejaVu Sans', 'mpltest', 'STIXGeneral', 'cmr10', 'STIXGeneral']), - ("stix", ['DejaVu Sans', 'mpltest', 'STIXGeneral'])]) + ("stix", ['DejaVu Sans', 'mpltest', 'STIXGeneral', 'STIXGeneral', 'STIXGeneral'])]) def test_mathtext_fallback(fallback, fontlist): mpl.font_manager.fontManager.addfont( str(Path(__file__).resolve().parent / 'mpltest.ttf')) @@ -453,10 +452,10 @@ def test_mathtext_fallback(fallback, fontlist): fig.savefig(buff, format="svg") tspans = (ET.fromstring(buff.getvalue()) .findall(".//{http://www.w3.org/2000/svg}tspan[@style]")) - # Getting the last element of the style attrib is a close enough - # approximation for parsing the font property. - char_fonts = [shlex.split(tspan.attrib["style"])[-1] for tspan in tspans] - assert char_fonts == fontlist + char_fonts = [ + re.search(r"font-family: '([\w ]+)'", tspan.attrib["style"]).group(1) + for tspan in tspans] + assert char_fonts == fontlist, f'Expected {fontlist}, got {char_fonts}' mpl.font_manager.fontManager.ttflist.pop() From 1bc4ab62bc977013b084afbd5560682f2e62daf0 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 01:33:40 -0400 Subject: [PATCH 0056/1230] TYP: Use pipes for Union types --- lib/matplotlib/_mathtext.py | 4 +-- lib/matplotlib/_mathtext_data.py | 10 +++--- lib/matplotlib/typing.py | 53 +++++++++++++++++--------------- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index e47c58c72f63..30bfcbfb26d7 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -2645,7 +2645,7 @@ def _genfrac(self, ldelim: str, rdelim: str, rule: float | None, style: _MathSty if rdelim == '': rdelim = '.' return self._auto_sized_delimiter(ldelim, - T.cast(list[T.Union[Box, Char, str]], + T.cast(list[Box | Char | str], result), rdelim) return result @@ -2786,7 +2786,7 @@ def _auto_sized_delimiter(self, front: str, del middle[idx] # There should only be \middle and its delimiter as str, which have # just been removed. - middle_part = T.cast(list[T.Union[Box, Char]], middle) + middle_part = T.cast(list[Box | Char], middle) else: height = 0 depth = 0 diff --git a/lib/matplotlib/_mathtext_data.py b/lib/matplotlib/_mathtext_data.py index 8928800a108b..5819ee743044 100644 --- a/lib/matplotlib/_mathtext_data.py +++ b/lib/matplotlib/_mathtext_data.py @@ -3,7 +3,7 @@ """ from __future__ import annotations -from typing import overload, Union +from typing import overload latex_to_bakoma = { '\\__sqrt__' : ('cmex10', 0x70), @@ -1113,11 +1113,10 @@ # Each element is a 4-tuple of the form: # src_start, src_end, dst_font, dst_start -_EntryTypeIn = tuple[str, str, str, Union[str, int]] +_EntryTypeIn = tuple[str, str, str, str | int] _EntryTypeOut = tuple[int, int, str, int] -_stix_virtual_fonts: dict[str, Union[dict[ - str, list[_EntryTypeIn]], list[_EntryTypeIn]]] = { +_stix_virtual_fonts: dict[str, dict[str, list[_EntryTypeIn]] | list[_EntryTypeIn]] = { 'bb': { "rm": [ ("\N{DIGIT ZERO}", @@ -1729,8 +1728,7 @@ def _normalize_stix_fontcodes(d): return {k: _normalize_stix_fontcodes(v) for k, v in d.items()} -stix_virtual_fonts: dict[str, Union[dict[str, list[_EntryTypeOut]], - list[_EntryTypeOut]]] +stix_virtual_fonts: dict[str, dict[str, list[_EntryTypeOut]] | list[_EntryTypeOut]] stix_virtual_fonts = _normalize_stix_fontcodes(_stix_virtual_fonts) # Free redundant list now that it has been normalized diff --git a/lib/matplotlib/typing.py b/lib/matplotlib/typing.py index 02059be94ba2..9c50eb4f318a 100644 --- a/lib/matplotlib/typing.py +++ b/lib/matplotlib/typing.py @@ -11,50 +11,53 @@ """ from collections.abc import Hashable, Sequence import pathlib -from typing import Any, Literal, TypeVar, Union +from typing import Any, Literal, TypeVar from . import path from ._enums import JoinStyle, CapStyle from .markers import MarkerStyle # The following are type aliases. Once python 3.9 is dropped, they should be annotated -# using ``typing.TypeAlias`` and Unions should be converted to using ``|`` syntax. +# using ``typing.TypeAlias``. -RGBColorType = Union[tuple[float, float, float], str] -RGBAColorType = Union[ - str, # "none" or "#RRGGBBAA"/"#RGBA" hex strings - tuple[float, float, float, float], +RGBColorType = tuple[float, float, float] | str +RGBAColorType = ( + str | # "none" or "#RRGGBBAA"/"#RGBA" hex strings + tuple[float, float, float, float] | # 2 tuple (color, alpha) representations, not infinitely recursive # RGBColorType includes the (str, float) tuple, even for RGBA strings - tuple[RGBColorType, float], + tuple[RGBColorType, float] | # (4-tuple, float) is odd, but accepted as the outer float overriding A of 4-tuple - tuple[tuple[float, float, float, float], float], -] + tuple[tuple[float, float, float, float], float] +) -ColorType = Union[RGBColorType, RGBAColorType] +ColorType = RGBColorType | RGBAColorType RGBColourType = RGBColorType RGBAColourType = RGBAColorType ColourType = ColorType -LineStyleType = Union[str, tuple[float, Sequence[float]]] +LineStyleType = str | tuple[float, Sequence[float]] DrawStyleType = Literal["default", "steps", "steps-pre", "steps-mid", "steps-post"] -MarkEveryType = Union[ - None, int, tuple[int, int], slice, list[int], float, tuple[float, float], list[bool] -] - -MarkerType = Union[str, path.Path, MarkerStyle] +MarkEveryType = ( + None | + int | tuple[int, int] | slice | list[int] | + float | tuple[float, float] | + list[bool] +) + +MarkerType = str | path.Path | MarkerStyle FillStyleType = Literal["full", "left", "right", "bottom", "top", "none"] -JoinStyleType = Union[JoinStyle, Literal["miter", "round", "bevel"]] -CapStyleType = Union[CapStyle, Literal["butt", "projecting", "round"]] +JoinStyleType = JoinStyle | Literal["miter", "round", "bevel"] +CapStyleType = CapStyle | Literal["butt", "projecting", "round"] -RcStyleType = Union[ - str, - dict[str, Any], - pathlib.Path, - Sequence[Union[str, pathlib.Path, dict[str, Any]]], -] +RcStyleType = ( + str | + dict[str, Any] | + pathlib.Path | + Sequence[str | pathlib.Path | dict[str, Any]] +) _HT = TypeVar("_HT", bound=Hashable) -HashableList = list[Union[_HT, "HashableList[_HT]"]] +HashableList = list[_HT | "HashableList[_HT]"] """A nested list of Hashable values.""" From f734bb5d9ef2e0fb72d992464f6473407abf044b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 01:41:24 -0400 Subject: [PATCH 0057/1230] TYP: Hint typing aliases as TypeAlias --- lib/matplotlib/typing.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/typing.py b/lib/matplotlib/typing.py index 9c50eb4f318a..b70b4cc264dc 100644 --- a/lib/matplotlib/typing.py +++ b/lib/matplotlib/typing.py @@ -11,17 +11,14 @@ """ from collections.abc import Hashable, Sequence import pathlib -from typing import Any, Literal, TypeVar +from typing import Any, Literal, TypeAlias, TypeVar from . import path from ._enums import JoinStyle, CapStyle from .markers import MarkerStyle -# The following are type aliases. Once python 3.9 is dropped, they should be annotated -# using ``typing.TypeAlias``. - -RGBColorType = tuple[float, float, float] | str -RGBAColorType = ( +RGBColorType: TypeAlias = tuple[float, float, float] | str +RGBAColorType: TypeAlias = ( str | # "none" or "#RRGGBBAA"/"#RGBA" hex strings tuple[float, float, float, float] | # 2 tuple (color, alpha) representations, not infinitely recursive @@ -31,27 +28,28 @@ tuple[tuple[float, float, float, float], float] ) -ColorType = RGBColorType | RGBAColorType +ColorType: TypeAlias = RGBColorType | RGBAColorType -RGBColourType = RGBColorType -RGBAColourType = RGBAColorType -ColourType = ColorType +RGBColourType: TypeAlias = RGBColorType +RGBAColourType: TypeAlias = RGBAColorType +ColourType: TypeAlias = ColorType -LineStyleType = str | tuple[float, Sequence[float]] -DrawStyleType = Literal["default", "steps", "steps-pre", "steps-mid", "steps-post"] -MarkEveryType = ( +LineStyleType: TypeAlias = str | tuple[float, Sequence[float]] +DrawStyleType: TypeAlias = Literal["default", "steps", "steps-pre", "steps-mid", + "steps-post"] +MarkEveryType: TypeAlias = ( None | int | tuple[int, int] | slice | list[int] | float | tuple[float, float] | list[bool] ) -MarkerType = str | path.Path | MarkerStyle -FillStyleType = Literal["full", "left", "right", "bottom", "top", "none"] -JoinStyleType = JoinStyle | Literal["miter", "round", "bevel"] -CapStyleType = CapStyle | Literal["butt", "projecting", "round"] +MarkerType: TypeAlias = str | path.Path | MarkerStyle +FillStyleType: TypeAlias = Literal["full", "left", "right", "bottom", "top", "none"] +JoinStyleType: TypeAlias = JoinStyle | Literal["miter", "round", "bevel"] +CapStyleType: TypeAlias = CapStyle | Literal["butt", "projecting", "round"] -RcStyleType = ( +RcStyleType: TypeAlias = ( str | dict[str, Any] | pathlib.Path | @@ -59,5 +57,5 @@ ) _HT = TypeVar("_HT", bound=Hashable) -HashableList = list[_HT | "HashableList[_HT]"] +HashableList: TypeAlias = list[_HT | "HashableList[_HT]"] """A nested list of Hashable values.""" From dbad6cf50ed7222c54dc809effa35ad73971a97c Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 02:17:17 -0400 Subject: [PATCH 0058/1230] Replace full list evaluations with more efficient calls - UP027: Replace unpacked list comprehensions with generator expressions; these are supposedly more efficient since no temporary list is created. - RUF015: Replace accessing the first element of an evaluated list with `next(iter(...))`, avoiding the temporary list. - RUF017: Replace quadratic `sum(list of list, [])` with faster `functools.reduce` implementation. --- lib/matplotlib/_docstring.py | 4 ++-- lib/matplotlib/_mathtext.py | 2 +- lib/matplotlib/artist.py | 6 ++++-- lib/matplotlib/axes/_axes.py | 2 +- lib/matplotlib/axis.py | 6 +++--- lib/matplotlib/backends/backend_qt.py | 4 ++-- lib/matplotlib/backends/backend_wx.py | 4 ++-- lib/matplotlib/bezier.py | 2 +- lib/matplotlib/quiver.py | 4 ++-- lib/matplotlib/tests/test_axes.py | 4 ++-- lib/matplotlib/tests/test_font_manager.py | 4 ++-- lib/matplotlib/tests/test_mathtext.py | 2 +- lib/matplotlib/tests/test_sphinxext.py | 2 +- lib/matplotlib/tests/test_text.py | 2 +- lib/matplotlib/tests/test_type1font.py | 8 ++++---- lib/matplotlib/tests/test_widgets.py | 3 ++- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 4 ++-- tools/generate_matplotlibrc.py | 4 ++-- 18 files changed, 35 insertions(+), 32 deletions(-) diff --git a/lib/matplotlib/_docstring.py b/lib/matplotlib/_docstring.py index f44d7b2c7674..6c80b080af4c 100644 --- a/lib/matplotlib/_docstring.py +++ b/lib/matplotlib/_docstring.py @@ -82,8 +82,8 @@ def __missing__(self, key): name = key[:-len(":kwdoc")] from matplotlib.artist import Artist, kwdoc try: - cls, = [cls for cls in _api.recursive_subclasses(Artist) - if cls.__name__ == name] + cls, = (cls for cls in _api.recursive_subclasses(Artist) + if cls.__name__ == name) except ValueError as e: raise KeyError(key) from e return self.setdefault(key, kwdoc(cls)) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 30bfcbfb26d7..a2f83c11f871 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -376,7 +376,7 @@ def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float, font.set_size(fontsize, dpi) glyph = font.load_char(num, flags=self.load_glyph_flags) - xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox] + xmin, ymin, xmax, ymax = (val / 64 for val in glyph.bbox) offset = self._get_offset(font, glyph, fontsize, dpi) metrics = FontMetrics( advance = glyph.linearHoriAdvance/65536.0, diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 345a61bfc16a..981365d852be 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -1,10 +1,11 @@ from collections import namedtuple import contextlib -from functools import cache, wraps +from functools import cache, reduce, wraps import inspect from inspect import Signature, Parameter import logging from numbers import Number, Real +import operator import re import warnings @@ -1290,7 +1291,8 @@ def matchfunc(x): raise ValueError('match must be None, a matplotlib.artist.Artist ' 'subclass, or a callable') - artists = sum([c.findobj(matchfunc) for c in self.get_children()], []) + artists = reduce(operator.iadd, + [c.findobj(matchfunc) for c in self.get_children()], []) if include_self and matchfunc(self): artists.append(self) return artists diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 5c236efbe429..e4d2087424b1 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6000,7 +6000,7 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs): # unit conversion allows e.g. datetime objects as axis values X, Y = args[:2] X, Y = self._process_unit_info([("x", X), ("y", Y)], kwargs) - X, Y = [cbook.safe_masked_invalid(a, copy=True) for a in [X, Y]] + X, Y = (cbook.safe_masked_invalid(a, copy=True) for a in [X, Y]) if funcname == 'pcolormesh': if np.ma.is_masked(X) or np.ma.is_masked(Y): diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 1eb1b2331db3..483c9a3db15f 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -29,7 +29,7 @@ # allows all Line2D kwargs. _line_inspector = martist.ArtistInspector(mlines.Line2D) _line_param_names = _line_inspector.get_setters() -_line_param_aliases = [list(d)[0] for d in _line_inspector.aliasd.values()] +_line_param_aliases = [next(iter(d)) for d in _line_inspector.aliasd.values()] _gridline_param_names = ['grid_' + name for name in _line_param_names + _line_param_aliases] @@ -728,8 +728,8 @@ def _get_shared_axis(self): def _get_axis_name(self): """Return the axis name.""" - return [name for name, axis in self.axes._axis_map.items() - if axis is self][0] + return next(name for name, axis in self.axes._axis_map.items() + if axis is self) # During initialization, Axis objects often create ticks that are later # unused; this turns out to be a very slow step. Instead, use a custom diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 6603883075d4..c592858cef0b 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -483,7 +483,7 @@ def blit(self, bbox=None): if bbox is None and self.figure: bbox = self.figure.bbox # Blit the entire canvas if bbox is None. # repaint uses logical pixels, not physical pixels like the renderer. - l, b, w, h = [int(pt / self.device_pixel_ratio) for pt in bbox.bounds] + l, b, w, h = (int(pt / self.device_pixel_ratio) for pt in bbox.bounds) t = b + h self.repaint(l, self.rect().height() - t, w, h) @@ -504,7 +504,7 @@ def drawRectangle(self, rect): # Draw the zoom rectangle to the QPainter. _draw_rect_callback needs # to be called at the end of paintEvent. if rect is not None: - x0, y0, w, h = [int(pt / self.device_pixel_ratio) for pt in rect] + x0, y0, w, h = (int(pt / self.device_pixel_ratio) for pt in rect) x1 = x0 + w y1 = y0 + h def _draw_rect_callback(painter): diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index d39edf40f151..c7e26b92134a 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -1197,8 +1197,8 @@ def _get_tool_pos(self, tool): ``ToolBar.GetToolPos`` is not useful because wx assigns the same Id to all Separators and StretchableSpaces. """ - pos, = [pos for pos in range(self.ToolsCount) - if self.GetToolByPos(pos) == tool] + pos, = (pos for pos in range(self.ToolsCount) + if self.GetToolByPos(pos) == tool) return pos def add_toolitem(self, name, group, position, image_file, description, diff --git a/lib/matplotlib/bezier.py b/lib/matplotlib/bezier.py index 069e20d05916..42a6b478d729 100644 --- a/lib/matplotlib/bezier.py +++ b/lib/matplotlib/bezier.py @@ -54,7 +54,7 @@ def get_intersection(cx1, cy1, cos_t1, sin_t1, # rhs_inverse a_, b_ = d, -b c_, d_ = -c, a - a_, b_, c_, d_ = [k / ad_bc for k in [a_, b_, c_, d_]] + a_, b_, c_, d_ = (k / ad_bc for k in [a_, b_, c_, d_]) x = a_ * line1_rhs + b_ * line2_rhs y = c_ * line1_rhs + d_ * line2_rhs diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index 240d7737b516..15d3b4cf9735 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -424,13 +424,13 @@ def _parse_args(*args, caller_name='function'): X = X.ravel() Y = Y.ravel() if len(X) == nc and len(Y) == nr: - X, Y = [a.ravel() for a in np.meshgrid(X, Y)] + X, Y = (a.ravel() for a in np.meshgrid(X, Y)) elif len(X) != len(Y): raise ValueError('X and Y must be the same size, but ' f'X.size is {X.size} and Y.size is {Y.size}.') else: indexgrid = np.meshgrid(np.arange(nc), np.arange(nr)) - X, Y = [np.ravel(a) for a in indexgrid] + X, Y = (np.ravel(a) for a in indexgrid) # Size validation for U, V, C is left to the set_UVC method. return X, Y, U, V, C diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 69a580fe515b..e5ae14c6e66b 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -3086,10 +3086,10 @@ def test_log_scales(): ax.set_yscale('log', base=5.5) ax.invert_yaxis() ax.set_xscale('log', base=9.0) - xticks, yticks = [ + xticks, yticks = ( [(t.get_loc(), t.label1.get_text()) for t in axis._update_ticks()] for axis in [ax.xaxis, ax.yaxis] - ] + ) assert xticks == [ (1.0, '$\\mathdefault{9^{0}}$'), (9.0, '$\\mathdefault{9^{1}}$'), diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index c6fc422ca613..ab8c6c70d1bf 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -183,8 +183,8 @@ def test_addfont_as_path(): path = Path(__file__).parent / font_test_file try: fontManager.addfont(path) - added, = [font for font in fontManager.ttflist - if font.fname.endswith(font_test_file)] + added, = (font for font in fontManager.ttflist + if font.fname.endswith(font_test_file)) fontManager.ttflist.remove(added) finally: to_remove = [font for font in fontManager.ttflist diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index f12c859b311c..4dcd08ba0718 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -270,7 +270,7 @@ def test_short_long_accents(fig_test, fig_ref): short_accs = [s for s in acc_map if len(s) == 1] corresponding_long_accs = [] for s in short_accs: - l, = [l for l in acc_map if len(l) > 1 and acc_map[l] == acc_map[s]] + l, = (l for l in acc_map if len(l) > 1 and acc_map[l] == acc_map[s]) corresponding_long_accs.append(l) fig_test.text(0, .5, "$" + "".join(rf"\{s}a" for s in short_accs) + "$") fig_ref.text( diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 24efecbeae9d..6e7b5ec5e50e 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -62,7 +62,7 @@ def plot_directive_file(num): # This is always next to the doctree dir. return doctree_dir.parent / 'plot_directive' / f'some_plots-{num}.png' - range_10, range_6, range_4 = [plot_file(i) for i in range(1, 4)] + range_10, range_6, range_4 = (plot_file(i) for i in range(1, 4)) # Plot 5 is range(6) plot assert filecmp.cmp(range_6, plot_file(5)) # Plot 7 is range(4) plot diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 8904337f68ba..19262202e5c1 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -921,7 +921,7 @@ def test_annotate_offset_fontsize(): fontsize='10', xycoords='data', textcoords=text_coords[i]) for i in range(2)] - points_coords, fontsize_coords = [ann.get_window_extent() for ann in anns] + points_coords, fontsize_coords = (ann.get_window_extent() for ann in anns) fig.canvas.draw() assert str(points_coords) == str(fontsize_coords) diff --git a/lib/matplotlib/tests/test_type1font.py b/lib/matplotlib/tests/test_type1font.py index 1e173d5ea84d..9b8a2d1f07c6 100644 --- a/lib/matplotlib/tests/test_type1font.py +++ b/lib/matplotlib/tests/test_type1font.py @@ -141,11 +141,11 @@ def test_overprecision(): font = t1f.Type1Font(filename) slanted = font.transform({'slant': .167}) lines = slanted.parts[0].decode('ascii').splitlines() - matrix, = [line[line.index('[')+1:line.index(']')] - for line in lines if '/FontMatrix' in line] - angle, = [word + matrix, = (line[line.index('[')+1:line.index(']')] + for line in lines if '/FontMatrix' in line) + angle, = (word for line in lines if '/ItalicAngle' in line - for word in line.split() if word[0] in '-0123456789'] + for word in line.split() if word[0] in '-0123456789') # the following used to include 0.00016700000000000002 assert matrix == '0.001 0 0.000167 0.001 0 0' # and here we had -9.48090361795083 diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 0f2cc411dbdf..d559ad99ef0f 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1,5 +1,6 @@ import functools import io +import operator from unittest import mock import matplotlib as mpl @@ -1573,7 +1574,7 @@ def test_polygon_selector_remove(idx, draw_bounding_box): # Remove the extra point event_sequence.append(polygon_remove_vertex(200, 200)) # Flatten list of lists - event_sequence = sum(event_sequence, []) + event_sequence = functools.reduce(operator.iadd, event_sequence, []) check_polygon_selector(event_sequence, verts, 2, draw_bounding_box=draw_bounding_box) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index f519b42098e5..e68b4319671b 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -508,10 +508,10 @@ def test_scatter3d_sorting(fig_ref, fig_test, depthshade): linewidths[0::2, 0::2] = 5 linewidths[1::2, 1::2] = 5 - x, y, z, sizes, facecolors, edgecolors, linewidths = [ + x, y, z, sizes, facecolors, edgecolors, linewidths = ( a.flatten() for a in [x, y, z, sizes, facecolors, edgecolors, linewidths] - ] + ) ax_ref = fig_ref.add_subplot(projection='3d') sets = (np.unique(a) for a in [sizes, facecolors, edgecolors, linewidths]) diff --git a/tools/generate_matplotlibrc.py b/tools/generate_matplotlibrc.py index b779187c1e0d..a5619a0fec3e 100755 --- a/tools/generate_matplotlibrc.py +++ b/tools/generate_matplotlibrc.py @@ -20,9 +20,9 @@ backend = sys.argv[3] template_lines = input.read_text(encoding="utf-8").splitlines(True) -backend_line_idx, = [ # Also asserts that there is a single such line. +backend_line_idx, = ( # Also asserts that there is a single such line. idx for idx, line in enumerate(template_lines) - if "#backend:" in line] + if "#backend:" in line) template_lines[backend_line_idx] = ( f"#backend: {backend}\n" if backend not in ['', 'auto'] else "##backend: Agg\n") output.write_text("".join(template_lines), encoding="utf-8") From 6946a766fb03f2734ab4d7902df51dbb7436e543 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 02:28:41 -0400 Subject: [PATCH 0059/1230] TYP: Clean up deprecated typing imports --- lib/matplotlib/_api/__init__.pyi | 4 ++-- lib/matplotlib/_docstring.pyi | 3 ++- lib/matplotlib/figure.pyi | 4 ++-- lib/matplotlib/tests/test_api.py | 3 ++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/_api/__init__.pyi b/lib/matplotlib/_api/__init__.pyi index 8dbef9528a82..415cd3207fb7 100644 --- a/lib/matplotlib/_api/__init__.pyi +++ b/lib/matplotlib/_api/__init__.pyi @@ -1,5 +1,5 @@ -from collections.abc import Callable, Generator, Mapping, Sequence -from typing import Any, Iterable, TypeVar, overload +from collections.abc import Callable, Generator, Iterable, Mapping, Sequence +from typing import Any, TypeVar, overload from typing_extensions import Self # < Py 3.11 from numpy.typing import NDArray diff --git a/lib/matplotlib/_docstring.pyi b/lib/matplotlib/_docstring.pyi index bcb4b29ab922..62cea3da4476 100644 --- a/lib/matplotlib/_docstring.pyi +++ b/lib/matplotlib/_docstring.pyi @@ -1,4 +1,5 @@ -from typing import Any, Callable, TypeVar, overload +from collections.abc import Callable +from typing import Any, TypeVar, overload _T = TypeVar('_T') diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi index 3c6876b3441b..27366e83bc4d 100644 --- a/lib/matplotlib/figure.pyi +++ b/lib/matplotlib/figure.pyi @@ -1,6 +1,6 @@ -from collections.abc import Callable, Hashable, Iterable +from collections.abc import Callable, Hashable, Iterable, Sequence import os -from typing import Any, IO, Literal, Sequence, TypeVar, overload +from typing import Any, IO, Literal, TypeVar, overload import numpy as np from numpy.typing import ArrayLike diff --git a/lib/matplotlib/tests/test_api.py b/lib/matplotlib/tests/test_api.py index 8b0f1e70114e..23d3ec48f31f 100644 --- a/lib/matplotlib/tests/test_api.py +++ b/lib/matplotlib/tests/test_api.py @@ -1,8 +1,9 @@ from __future__ import annotations +from collections.abc import Callable import re import typing -from typing import Any, Callable, TypeVar +from typing import Any, TypeVar import numpy as np import pytest From 33093682dcde9d0adbffdfabd5ab08e645a3e191 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 03:45:54 -0400 Subject: [PATCH 0060/1230] Use itertools.pairwise instead of zip This was added in 3.10. --- galleries/examples/units/basic_units.py | 3 ++- lib/matplotlib/tests/test_cbook.py | 4 ++-- lib/matplotlib/transforms.py | 5 +++-- lib/mpl_toolkits/mplot3d/axes3d.py | 6 +++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/galleries/examples/units/basic_units.py b/galleries/examples/units/basic_units.py index f9a94bcf6e37..d6f788c20fd9 100644 --- a/galleries/examples/units/basic_units.py +++ b/galleries/examples/units/basic_units.py @@ -7,6 +7,7 @@ """ +import itertools import math from packaging.version import parse as parse_version @@ -254,7 +255,7 @@ def get_unit(self): class UnitResolver: def addition_rule(self, units): - for unit_1, unit_2 in zip(units[:-1], units[1:]): + for unit_1, unit_2 in itertools.pairwise(units): if unit_1 != unit_2: return NotImplemented return units[0] diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 5d46c0a75775..3f4efb7bd4c7 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -799,8 +799,8 @@ def check(x, rstride, cstride): row_inds = [*range(0, rows-1, rstride), rows-1] col_inds = [*range(0, cols-1, cstride), cols-1] polys = [] - for rs, rs_next in zip(row_inds[:-1], row_inds[1:]): - for cs, cs_next in zip(col_inds[:-1], col_inds[1:]): + for rs, rs_next in itertools.pairwise(row_inds): + for cs, cs_next in itertools.pairwise(col_inds): # +1 ensures we share edges between polygons ps = cbook._array_perimeter(x[rs:rs_next+1, cs:cs_next+1]).T polys.append(ps) diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 3575bd1fc14d..20cbe1e68b9a 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -38,6 +38,7 @@ import copy import functools +import itertools import textwrap import weakref import math @@ -553,7 +554,7 @@ def splitx(self, *args): x0, y0, x1, y1 = self.extents w = x1 - x0 return [Bbox([[x0 + xf0 * w, y0], [x0 + xf1 * w, y1]]) - for xf0, xf1 in zip(xf[:-1], xf[1:])] + for xf0, xf1 in itertools.pairwise(xf)] def splity(self, *args): """ @@ -564,7 +565,7 @@ def splity(self, *args): x0, y0, x1, y1 = self.extents h = y1 - y0 return [Bbox([[x0, y0 + yf0 * h], [x1, y0 + yf1 * h]]) - for yf0, yf1 in zip(yf[:-1], yf[1:])] + for yf0, yf1 in itertools.pairwise(yf)] def count_contains(self, vertices): """ diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 12f3682ae5e9..ea93d3eadf82 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -2206,8 +2206,8 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, col_inds = list(range(0, cols-1, cstride)) + [cols-1] polys = [] - for rs, rs_next in zip(row_inds[:-1], row_inds[1:]): - for cs, cs_next in zip(col_inds[:-1], col_inds[1:]): + for rs, rs_next in itertools.pairwise(row_inds): + for cs, cs_next in itertools.pairwise(col_inds): ps = [ # +1 ensures we share edges between polygons cbook._array_perimeter(a[rs:rs_next+1, cs:cs_next+1]) @@ -3385,7 +3385,7 @@ def permutation_matrices(n): voxel_faces[i0].append(p0 + square_rot_neg) # draw middle faces - for r1, r2 in zip(rinds[:-1], rinds[1:]): + for r1, r2 in itertools.pairwise(rinds): p1 = permute.dot([p, q, r1]) p2 = permute.dot([p, q, r2]) From 927bb9e28a7d8fad6137240ebcfe70164663a879 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 04:28:19 -0400 Subject: [PATCH 0061/1230] Use dict.fromkeys where possible --- galleries/examples/showcase/stock_prices.py | 2 +- lib/matplotlib/axes/_base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/galleries/examples/showcase/stock_prices.py b/galleries/examples/showcase/stock_prices.py index a3ec7ad2a252..bc372fb1211a 100644 --- a/galleries/examples/showcase/stock_prices.py +++ b/galleries/examples/showcase/stock_prices.py @@ -42,7 +42,7 @@ 'ADBE', 'GSPC', 'IXIC'] # Manually adjust the label positions vertically (units are points = 1/72 inch) -y_offsets = {k: 0 for k in stocks_ticker} +y_offsets = dict.fromkeys(stocks_ticker, 0) y_offsets['IBM'] = 5 y_offsets['AAPL'] = -5 y_offsets['AMZN'] = -6 diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index a29583668a17..80188a4acf72 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -646,7 +646,7 @@ def __init__(self, fig, self._aspect = 'auto' self._adjustable = 'box' self._anchor = 'C' - self._stale_viewlims = {name: False for name in self._axis_names} + self._stale_viewlims = dict.fromkeys(self._axis_names, False) self._forward_navigation_events = forward_navigation_events self._sharex = sharex self._sharey = sharey From 54c84914a1ed3bab0d2a23cc94544512e5c2bc9c Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 04:44:34 -0400 Subject: [PATCH 0062/1230] Use parentheses for multi-line context expressions Now allowed in Python 3.10. --- lib/matplotlib/animation.py | 4 ++-- lib/matplotlib/backend_bases.py | 18 +++++++++--------- lib/matplotlib/backend_tools.py | 8 ++++---- lib/matplotlib/backends/backend_pgf.py | 8 ++++---- lib/matplotlib/backends/backend_svg.py | 4 ++-- lib/matplotlib/colorbar.py | 6 ++---- lib/matplotlib/tests/test_backend_pdf.py | 8 ++++---- lib/matplotlib/tests/test_backend_pgf.py | 8 ++++---- lib/matplotlib/tests/test_cbook.py | 3 +-- 9 files changed, 32 insertions(+), 35 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 1efb72cb52e6..9108b727b50c 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -1086,8 +1086,8 @@ def _pre_composite_to_white(color): # canvas._is_saving = True makes the draw_event animation-starting # callback a no-op; canvas.manager = None prevents resizing the GUI # widget (both are likewise done in savefig()). - with writer.saving(self._fig, filename, dpi), \ - cbook._setattr_cm(self._fig.canvas, _is_saving=True, manager=None): + with (writer.saving(self._fig, filename, dpi), + cbook._setattr_cm(self._fig.canvas, _is_saving=True, manager=None)): for anim in all_anim: anim._init_draw() # Clear the initial frame frame_number = 0 diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 2c9f6188a97c..4b818d7fcdbd 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1972,8 +1972,8 @@ def _switch_canvas_and_return_print_method(self, fmt, backend=None): """ Context manager temporarily setting the canvas for saving the figure:: - with canvas._switch_canvas_and_return_print_method(fmt, backend) \\ - as print_method: + with (canvas._switch_canvas_and_return_print_method(fmt, backend) + as print_method): # ``print_method`` is a suitable ``print_{fmt}`` method, and # the figure's canvas is temporarily switched to the method's # canvas within the with... block. ``print_method`` is also @@ -2110,13 +2110,13 @@ def print_figure( "'figure', or omit the *papertype* argument entirely.") # Remove the figure manager, if any, to avoid resizing the GUI widget. - with cbook._setattr_cm(self, manager=None), \ - self._switch_canvas_and_return_print_method(format, backend) \ - as print_method, \ - cbook._setattr_cm(self.figure, dpi=dpi), \ - cbook._setattr_cm(self.figure.canvas, _device_pixel_ratio=1), \ - cbook._setattr_cm(self.figure.canvas, _is_saving=True), \ - ExitStack() as stack: + with (cbook._setattr_cm(self, manager=None), + self._switch_canvas_and_return_print_method(format, backend) + as print_method, + cbook._setattr_cm(self.figure, dpi=dpi), + cbook._setattr_cm(self.figure.canvas, _device_pixel_ratio=1), + cbook._setattr_cm(self.figure.canvas, _is_saving=True), + ExitStack() as stack): for prop in ["facecolor", "edgecolor"]: color = locals()[prop] diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 221332663767..87ed794022a0 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -382,8 +382,8 @@ def trigger(self, sender, event, data=None): sentinel = str(uuid.uuid4()) # Trigger grid switching by temporarily setting :rc:`keymap.grid` # to a unique key and sending an appropriate event. - with cbook._setattr_cm(event, key=sentinel), \ - mpl.rc_context({'keymap.grid': sentinel}): + with (cbook._setattr_cm(event, key=sentinel), + mpl.rc_context({'keymap.grid': sentinel})): mpl.backend_bases.key_press_handler(event, self.figure.canvas) @@ -397,8 +397,8 @@ def trigger(self, sender, event, data=None): sentinel = str(uuid.uuid4()) # Trigger grid switching by temporarily setting :rc:`keymap.grid_minor` # to a unique key and sending an appropriate event. - with cbook._setattr_cm(event, key=sentinel), \ - mpl.rc_context({'keymap.grid_minor': sentinel}): + with (cbook._setattr_cm(event, key=sentinel), + mpl.rc_context({'keymap.grid_minor': sentinel})): mpl.backend_bases.key_press_handler(event, self.figure.canvas) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 736656b0cc61..daefdb0640ca 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -849,8 +849,8 @@ def print_pdf(self, fname_or_fh, *, metadata=None, **kwargs): cbook._check_and_log_subprocess( [texcommand, "-interaction=nonstopmode", "-halt-on-error", "figure.tex"], _log, cwd=tmpdir) - with (tmppath / "figure.pdf").open("rb") as orig, \ - cbook.open_file_cm(fname_or_fh, "wb") as dest: + with ((tmppath / "figure.pdf").open("rb") as orig, + cbook.open_file_cm(fname_or_fh, "wb") as dest): shutil.copyfileobj(orig, dest) # copy file contents to target def print_png(self, fname_or_fh, **kwargs): @@ -862,8 +862,8 @@ def print_png(self, fname_or_fh, **kwargs): png_path = tmppath / "figure.png" self.print_pdf(pdf_path, **kwargs) converter(pdf_path, png_path, dpi=self.figure.dpi) - with png_path.open("rb") as orig, \ - cbook.open_file_cm(fname_or_fh, "wb") as dest: + with (png_path.open("rb") as orig, + cbook.open_file_cm(fname_or_fh, "wb") as dest): shutil.copyfileobj(orig, dest) # copy file contents to target def get_renderer(self): diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 84e4f96ad4a7..a0bb7d1dbfe2 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1347,8 +1347,8 @@ def print_svg(self, filename, *, bbox_inches_restore=None, metadata=None): renderer.finalize() def print_svgz(self, filename, **kwargs): - with cbook.open_file_cm(filename, "wb") as fh, \ - gzip.GzipFile(mode='w', fileobj=fh) as gzipwriter: + with (cbook.open_file_cm(filename, "wb") as fh, + gzip.GzipFile(mode='w', fileobj=fh) as gzipwriter): return self.print_svg(gzipwriter, **kwargs) def get_default_filetype(self): diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 156ea2ff6497..296f072a4af1 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -1115,10 +1115,8 @@ def _mesh(self): # Update the norm values in a context manager as it is only # a temporary change and we don't want to propagate any signals # attached to the norm (callbacks.blocked). - with self.norm.callbacks.blocked(), \ - cbook._setattr_cm(self.norm, - vmin=self.vmin, - vmax=self.vmax): + with (self.norm.callbacks.blocked(), + cbook._setattr_cm(self.norm, vmin=self.vmin, vmax=self.vmax)): y = self.norm.inverse(y) self._y = y X, Y = np.meshgrid([0., 1.], y) diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index ad565ea9e81b..6a6dc1a6bac1 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -91,8 +91,8 @@ def test_multipage_keep_empty(tmp_path): # an empty pdf is left behind with keep_empty=True fn = tmp_path / "b.pdf" - with pytest.warns(mpl.MatplotlibDeprecationWarning), \ - PdfPages(fn, keep_empty=True) as pdf: + with (pytest.warns(mpl.MatplotlibDeprecationWarning), + PdfPages(fn, keep_empty=True) as pdf): pass assert fn.exists() @@ -112,8 +112,8 @@ def test_multipage_keep_empty(tmp_path): # a non-empty pdf is left behind with keep_empty=True fn = tmp_path / "e.pdf" - with pytest.warns(mpl.MatplotlibDeprecationWarning), \ - PdfPages(fn, keep_empty=True) as pdf: + with (pytest.warns(mpl.MatplotlibDeprecationWarning), + PdfPages(fn, keep_empty=True) as pdf): pdf.savefig(plt.figure()) assert fn.exists() diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index 04b51f4d3781..54b1c3b5896e 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -298,8 +298,8 @@ def test_multipage_keep_empty(tmp_path): # an empty pdf is left behind with keep_empty=True fn = tmp_path / "b.pdf" - with pytest.warns(mpl.MatplotlibDeprecationWarning), \ - PdfPages(fn, keep_empty=True) as pdf: + with (pytest.warns(mpl.MatplotlibDeprecationWarning), + PdfPages(fn, keep_empty=True) as pdf): pass assert fn.exists() @@ -319,8 +319,8 @@ def test_multipage_keep_empty(tmp_path): # a non-empty pdf is left behind with keep_empty=True fn = tmp_path / "e.pdf" - with pytest.warns(mpl.MatplotlibDeprecationWarning), \ - PdfPages(fn, keep_empty=True) as pdf: + with (pytest.warns(mpl.MatplotlibDeprecationWarning), + PdfPages(fn, keep_empty=True) as pdf): pdf.savefig(plt.figure()) assert fn.exists() diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 3f4efb7bd4c7..6e0ad71f68be 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -466,8 +466,7 @@ def test_sanitize_sequence(): @pytest.mark.parametrize('inp, kwargs_to_norm', fail_mapping) def test_normalize_kwargs_fail(inp, kwargs_to_norm): - with pytest.raises(TypeError), \ - _api.suppress_matplotlib_deprecation_warning(): + with pytest.raises(TypeError), _api.suppress_matplotlib_deprecation_warning(): cbook.normalize_kwargs(inp, **kwargs_to_norm) From 32e9fbad20cc7b3a6674ae20f60862900dbfe475 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 8 Mar 2024 11:44:40 +0100 Subject: [PATCH 0063/1230] Simplify ttconv python<->C++ conversion using std::optional. --- src/_ttconv.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/_ttconv.cpp b/src/_ttconv.cpp index a99ea9d1c891..a9c1f7fb9f91 100644 --- a/src/_ttconv.cpp +++ b/src/_ttconv.cpp @@ -7,8 +7,8 @@ */ #include +#include #include "pprdrv.h" -#include namespace py = pybind11; using namespace pybind11::literals; @@ -40,25 +40,20 @@ static void convert_ttf_to_ps( const char *filename, py::object &output, int fonttype, - py::iterable* glyph_ids) + std::optional> glyph_ids_or_none) { PythonFileWriter output_(output); - std::vector glyph_ids_; - if (glyph_ids) { - for (py::handle glyph_id: *glyph_ids) { - glyph_ids_.push_back(glyph_id.cast()); - } - } - if (fonttype != 3 && fonttype != 42) { throw py::value_error( "fonttype must be either 3 (raw Postscript) or 42 (embedded Truetype)"); } + auto glyph_ids = glyph_ids_or_none.value_or(std::vector{}); + try { - insert_ttfont(filename, output_, static_cast(fonttype), glyph_ids_); + insert_ttfont(filename, output_, static_cast(fonttype), glyph_ids); } catch (TTException &e) { From f498ee23a652ddda8b740a5f7b2d2c47c420a91d Mon Sep 17 00:00:00 2001 From: Anthony Lee Date: Mon, 22 Jul 2024 19:22:32 -0700 Subject: [PATCH 0064/1230] cycler signature update. --- lib/matplotlib/axes/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index a29583668a17..2fb9a1e82335 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1548,7 +1548,7 @@ def set_prop_cycle(self, *args, **kwargs): Axes. If multiple properties are given, their value lists must have the same length. This is just a shortcut for explicitly creating a cycler and passing it to the function, i.e. it's short for - ``set_prop_cycle(cycler(label=values label2=values2, ...))``. + ``set_prop_cycle(cycler(label=values, label2=values2, ...))``. Form 3 creates a `~cycler.Cycler` for a single property and set it as the property cycle of the Axes. This form exists for compatibility From b8be220c6937b10a36700d5aa6b02dae8cc8c458 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 23 Jul 2024 07:17:28 +0200 Subject: [PATCH 0065/1230] Backport PR #28604: cycler signature update. --- lib/matplotlib/axes/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 17feef5b2105..6b3f2750575c 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1548,7 +1548,7 @@ def set_prop_cycle(self, *args, **kwargs): Axes. If multiple properties are given, their value lists must have the same length. This is just a shortcut for explicitly creating a cycler and passing it to the function, i.e. it's short for - ``set_prop_cycle(cycler(label=values label2=values2, ...))``. + ``set_prop_cycle(cycler(label=values, label2=values2, ...))``. Form 3 creates a `~cycler.Cycler` for a single property and set it as the property cycle of the Axes. This form exists for compatibility From 58d40e3c901c686c100927c040022bdd04a4156a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 23 Jul 2024 16:35:24 -0400 Subject: [PATCH 0066/1230] svg: Ensure marker-only lines get URLs Fixes #28595 --- lib/matplotlib/backends/backend_svg.py | 4 ++++ lib/matplotlib/tests/test_backend_svg.py | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 84e4f96ad4a7..623e1eb9ad82 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -715,6 +715,8 @@ def draw_markers( self._markers[dictkey] = oid writer.start('g', **self._get_clip_attrs(gc)) + if gc.get_url() is not None: + self.writer.start('a', {'xlink:href': gc.get_url()}) trans_and_flip = self._make_flip_transform(trans) attrib = {'xlink:href': f'#{oid}'} clip = (0, 0, self.width*72, self.height*72) @@ -726,6 +728,8 @@ def draw_markers( attrib['y'] = _short_float_fmt(y) attrib['style'] = self._get_style(gc, rgbFace) writer.element('use', attrib=attrib) + if gc.get_url() is not None: + self.writer.end('a') writer.end('g') def draw_path_collection(self, gc, master_transform, paths, all_transforms, diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index 689495eb31ac..b850a9ab6ff5 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -343,13 +343,17 @@ def test_url(): s.set_urls(['https://example.com/foo', 'https://example.com/bar', None]) # Line2D - p, = plt.plot([1, 3], [6, 5]) + p, = plt.plot([2, 3, 4], [4, 5, 6]) p.set_url('https://example.com/baz') + # Line2D markers-only + p, = plt.plot([3, 4, 5], [4, 5, 6], linestyle='none', marker='x') + p.set_url('https://example.com/quux') + b = BytesIO() fig.savefig(b, format='svg') b = b.getvalue() - for v in [b'foo', b'bar', b'baz']: + for v in [b'foo', b'bar', b'baz', b'quux']: assert b'https://example.com/' + v in b From 11855d1543dc0613221401260a9721ff43aa33a3 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh <14363975+scottshambaugh@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:42:07 -0600 Subject: [PATCH 0067/1230] Update lib/mpl_toolkits/mplot3d/proj3d.py Co-authored-by: Thomas A Caswell --- lib/mpl_toolkits/mplot3d/proj3d.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/proj3d.py b/lib/mpl_toolkits/mplot3d/proj3d.py index f010ddda44a9..3c7ccfbd6711 100644 --- a/lib/mpl_toolkits/mplot3d/proj3d.py +++ b/lib/mpl_toolkits/mplot3d/proj3d.py @@ -223,8 +223,8 @@ def proj_transform(xs, ys, zs, M): @_api.deprecated("3.10") -def proj_transform_clip(xs, ys, zs, M, focal_length=np.inf): - return _proj_transform_clip(xs, ys, zs, M, focal_length) +def proj_transform_clip(xs, ys, zs, M): + return _proj_transform_clip(xs, ys, zs, M, focal_length=np.inf) def _proj_transform_clip(xs, ys, zs, M, focal_length): From cc710854f457407b69fbaad5c4276ba0ec433b66 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sun, 14 Jul 2024 19:14:28 -0700 Subject: [PATCH 0068/1230] Inline Axis._MARKER_DICT This is used only for XTick._apply_tickdir, and YTick._apply_tickdir uses a different dictionary. --- lib/matplotlib/axis.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 483c9a3db15f..9c390fd8e917 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -33,12 +33,6 @@ _gridline_param_names = ['grid_' + name for name in _line_param_names + _line_param_aliases] -_MARKER_DICT = { - 'out': (mlines.TICKDOWN, mlines.TICKUP), - 'in': (mlines.TICKUP, mlines.TICKDOWN), - 'inout': ('|', '|'), -} - class Tick(martist.Artist): """ @@ -425,7 +419,11 @@ def _get_text2_transform(self): def _apply_tickdir(self, tickdir): # docstring inherited super()._apply_tickdir(tickdir) - mark1, mark2 = _MARKER_DICT[self._tickdir] + mark1, mark2 = { + 'out': (mlines.TICKDOWN, mlines.TICKUP), + 'in': (mlines.TICKUP, mlines.TICKDOWN), + 'inout': ('|', '|'), + }[self._tickdir] self.tick1line.set_marker(mark1) self.tick2line.set_marker(mark2) From 9b8c80cea76d1d1889864e03a9d01e9d4c4147d5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sun, 14 Jul 2024 20:05:09 -0700 Subject: [PATCH 0069/1230] Copy all internals from initial Tick to lazy ones Fixes #28574 --- lib/matplotlib/axis.py | 21 ++++++++++++++++----- lib/matplotlib/tests/test_axes.py | 22 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 9c390fd8e917..d1bbd095da87 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -198,18 +198,21 @@ def _set_labelrotation(self, labelrotation): _api.check_in_list(['auto', 'default'], labelrotation=mode) self._labelrotation = (mode, angle) + @property + def _pad(self): + return self._base_pad + self.get_tick_padding() + def _apply_tickdir(self, tickdir): """Set tick direction. Valid values are 'out', 'in', 'inout'.""" - # This method is responsible for updating `_pad`, and, in subclasses, - # for setting the tick{1,2}line markers as well. From the user - # perspective this should always be called through _apply_params, which - # further updates ticklabel positions using the new pads. + # This method is responsible for verifying input and, in subclasses, for setting + # the tick{1,2}line markers. From the user perspective this should always be + # called through _apply_params, which further updates ticklabel positions using + # the new pads. if tickdir is None: tickdir = mpl.rcParams[f'{self.__name__}.direction'] else: _api.check_in_list(['in', 'out', 'inout'], tickdir=tickdir) self._tickdir = tickdir - self._pad = self._base_pad + self.get_tick_padding() def get_tickdir(self): return self._tickdir @@ -1615,6 +1618,14 @@ def _copy_tick_props(self, src, dest): dest.tick1line.update_from(src.tick1line) dest.tick2line.update_from(src.tick2line) dest.gridline.update_from(src.gridline) + dest.update_from(src) + dest._loc = src._loc + dest._size = src._size + dest._width = src._width + dest._base_pad = src._base_pad + dest._labelrotation = src._labelrotation + dest._zorder = src._zorder + dest._tickdir = src._tickdir def get_label_text(self): """Get the text of the label.""" diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index e5ae14c6e66b..2c10a93796fa 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -5741,6 +5741,28 @@ def test_reset_ticks(fig_test, fig_ref): ax.yaxis.reset_ticks() +@mpl.style.context('mpl20') +def test_context_ticks(): + with plt.rc_context({ + 'xtick.direction': 'in', 'xtick.major.size': 30, 'xtick.major.width': 5, + 'xtick.color': 'C0', 'xtick.major.pad': 12, + 'xtick.bottom': True, 'xtick.top': True, + 'xtick.labelsize': 14, 'xtick.labelcolor': 'C1'}): + fig, ax = plt.subplots() + # Draw outside the context so that all-but-first tick are generated with the normal + # mpl20 style in place. + fig.draw_without_rendering() + + first_tick = ax.xaxis.majorTicks[0] + for tick in ax.xaxis.majorTicks[1:]: + assert tick._size == first_tick._size + assert tick._width == first_tick._width + assert tick._base_pad == first_tick._base_pad + assert tick._labelrotation == first_tick._labelrotation + assert tick._zorder == first_tick._zorder + assert tick._tickdir == first_tick._tickdir + + def test_vline_limit(): fig = plt.figure() ax = fig.gca() From fb5fb586f53a6e8e15f56a71d2e31a617c49c428 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 24 May 2024 16:03:53 -0400 Subject: [PATCH 0070/1230] BLD: Enable building Python 3.13 wheels for nightlies --- .github/workflows/cibuildwheel.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index aeb502cf7587..90526af740ba 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -134,6 +134,27 @@ jobs: name: cibw-sdist path: dist/ + - name: Build wheels for CPython 3.13 + uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 + with: + package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} + env: + CIBW_BUILD: "cp313-* cp313t-*" + # No free-threading wheels for NumPy; musllinux skipped for main builds also. + CIBW_SKIP: "cp313t-win_amd64 *-musllinux_aarch64" + CIBW_BUILD_FRONTEND: + "pip; args: --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" + CIBW_PRERELEASE_PYTHONS: true + CIBW_FREE_THREADED_SUPPORT: true + # No free-threading wheels available for aarch64 on Pillow. + CIBW_TEST_SKIP: "cp313t-manylinux_aarch64" + # We need pre-releases to get the nightly wheels. + CIBW_BEFORE_TEST: >- + pip install --pre + --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple + contourpy numpy pillow + CIBW_ARCHS: ${{ matrix.cibw_archs }} + - name: Build wheels for CPython 3.12 uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 with: From d73bd6dee726b81efb73c630711319011e2b2689 Mon Sep 17 00:00:00 2001 From: MadPhysicist Date: Tue, 11 Jun 2024 10:25:34 -0500 Subject: [PATCH 0071/1230] DOC: Added release note --- doc/api/next_api_changes/behavior/28375-MP.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 doc/api/next_api_changes/behavior/28375-MP.rst diff --git a/doc/api/next_api_changes/behavior/28375-MP.rst b/doc/api/next_api_changes/behavior/28375-MP.rst new file mode 100644 index 000000000000..75d7f7cf5030 --- /dev/null +++ b/doc/api/next_api_changes/behavior/28375-MP.rst @@ -0,0 +1,5 @@ +``transforms.AffineDeltaTransform`` updates correctly on axis limit changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before this change, transform sub-graphs with ``AffineDeltaTransform`` did not update correctly. +This PR ensures that changes to the child transform are passed through correctly. From 0b91d8ec999d89407285f83d9faab142cacbad6b Mon Sep 17 00:00:00 2001 From: MadPhysicist Date: Tue, 11 Jun 2024 12:20:23 -0500 Subject: [PATCH 0072/1230] TST: Added a unit test to avoid CI problems --- lib/matplotlib/tests/test_transforms.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/matplotlib/tests/test_transforms.py b/lib/matplotlib/tests/test_transforms.py index 959814de82db..fb8b7d74bc94 100644 --- a/lib/matplotlib/tests/test_transforms.py +++ b/lib/matplotlib/tests/test_transforms.py @@ -341,6 +341,31 @@ def test_deepcopy(self): assert_array_equal(s.get_matrix(), a.get_matrix()) +class TestAffineDeltaTransform: + def test_invalidate(self): + before = np.array([[1.0, 4.0, 0.0], + [5.0, 1.0, 0.0], + [0.0, 0.0, 1.0]]) + after = np.array([[1.0, 3.0, 0.0], + [5.0, 1.0, 0.0], + [0.0, 0.0, 1.0]]) + + # Translation and skew present + base = mtransforms.Affine2D.from_values(1, 5, 4, 1, 2, 3) + t = mtransforms.AffineDeltaTransform(base) + assert_array_equal(t.get_matrix(), before) + + # Mess with the internal structure of `base` without invalidating + # This should not affect this transform because it's a passthrough: + # it's always invalid + base.get_matrix()[0, 1:] = 3 + assert_array_equal(t.get_matrix(), after) + + # Invalidate the base + base.invalidate() + assert_array_equal(t.get_matrix(), after) + + def test_non_affine_caching(): class AssertingNonAffineTransform(mtransforms.Transform): """ From 9961c6fedb1d2c07feb47c416b463b5b93a716ea Mon Sep 17 00:00:00 2001 From: juanis2112 Date: Sun, 14 Jul 2024 11:16:37 -0700 Subject: [PATCH 0073/1230] Add branch tracking to development workflow instructions Delete tab for branch tracking workflow Apply suggestions from code review Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- doc/devel/development_workflow.rst | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/doc/devel/development_workflow.rst b/doc/devel/development_workflow.rst index 23e2c17732e2..efbef2a0a1ac 100644 --- a/doc/devel/development_workflow.rst +++ b/doc/devel/development_workflow.rst @@ -79,19 +79,13 @@ default, git will have a link to your fork of the GitHub repo, called git push origin my-new-feature -In git >= 1.7 you can ensure that the link is correctly set by using the -``--set-upstream`` option:: +.. hint:: - git push --set-upstream origin my-new-feature - -From now on git will know that ``my-new-feature`` is related to the -``my-new-feature`` branch in the GitHub repo. - -If you first opened the pull request from your ``main`` branch and then -converted it to a feature branch, you will need to close the original pull -request and open a new pull request from the renamed branch. See -`GitHub: working with branches -`_. + If you first opened the pull request from your ``main`` branch and then + converted it to a feature branch, you will need to close the original pull + request and open a new pull request from the renamed branch. See + `GitHub: working with branches + `_. .. _edit-flow: @@ -167,6 +161,17 @@ You can achieve this by using git commit -a --amend --no-edit git push [your-remote-repo] [your-branch] --force-with-lease +.. tip:: + Instead of typying your branch name every time, you can once do:: + + git push --set-upstream origin my-new-feature + + From now on git will know that ``my-new-feature`` is related to the + ``my-new-feature`` branch in the GitHub repo. After this, you will be able to + push your changes with:: + + git push + Manage commit history ===================== From 8fc27037c4010912b241522f59347bb72f6bcbb8 Mon Sep 17 00:00:00 2001 From: hannah Date: Thu, 25 Jul 2024 18:30:35 -0400 Subject: [PATCH 0074/1230] hack to suppress sphinx-gallery 17.0 warning --- doc/conf.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index b0546dec485d..1e6b3aefb5c5 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -15,6 +15,7 @@ import logging import os from pathlib import Path +import re import shutil import subprocess import sys @@ -201,6 +202,17 @@ def _check_dependencies(): warnings.filterwarnings('ignore', category=UserWarning, message=r'(\n|.)*is non-interactive, and thus cannot be shown') + +# hack to catch sphinx-gallery 17.0 warnings +def tutorials_download_error(record): + if re.match("download file not readable: .*tutorials_(python|jupyter).zip", + record.msg): + return False + + +logger = logging.getLogger('sphinx') +logger.addFilter(tutorials_download_error) + autosummary_generate = True autodoc_typehints = "none" From e775fc6cdfe8745b05fbab50287690c90a02e733 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 15 Feb 2024 22:57:05 -0500 Subject: [PATCH 0075/1230] DOC: Use video files for saving animations Because the default is Base64-encoded frames of PNGs, this should save a substantial amount of space in the resulting docs. --- doc/conf.py | 7 ++++++- requirements/doc/doc-requirements.txt | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 1e6b3aefb5c5..3eed7c5bce78 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -195,6 +195,11 @@ def _check_dependencies(): subsectionorder as gallery_order_subsectionorder) from sphinxext.util import clear_basic_units, matplotlib_reduced_latex_scraper +if parse_version(sphinx_gallery.__version__) >= parse_version('0.17.0'): + sg_matplotlib_animations = (True, 'mp4') +else: + sg_matplotlib_animations = True + # The following import is only necessary to monkey patch the signature later on from sphinx_gallery import gen_rst @@ -273,7 +278,7 @@ def tutorials_download_error(record): 'image_scrapers': (matplotlib_reduced_latex_scraper, ), 'image_srcset': ["2x"], 'junit': '../test-results/sphinx-gallery/junit.xml' if CIRCLECI else '', - 'matplotlib_animations': True, + 'matplotlib_animations': sg_matplotlib_animations, 'min_reported_time': 1, 'plot_gallery': 'True', # sphinx-gallery/913 'reference_url': {'matplotlib': None}, diff --git a/requirements/doc/doc-requirements.txt b/requirements/doc/doc-requirements.txt index cee389da9e94..21b6ffa38dd1 100644 --- a/requirements/doc/doc-requirements.txt +++ b/requirements/doc/doc-requirements.txt @@ -18,6 +18,7 @@ pydata-sphinx-theme~=0.15.0 mpl-sphinx-theme~=3.9.0 pyyaml sphinxcontrib-svg2pdfconverter>=1.1.0 +sphinxcontrib-video>=0.2.1 sphinx-copybutton sphinx-design sphinx-gallery>=0.12.0 From 33640e85a1d80eddb53534cd4e93c899d614e7ab Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 25 Jul 2024 22:13:32 -0400 Subject: [PATCH 0076/1230] CI: Build docs on latest Python There have been improvements to Python performance in 3.11 and 3.12 thanks to the Faster CPython project, and we hope this might help a little with docs. --- .circleci/config.yml | 6 +++--- .github/workflows/circleci.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7436698c8068..a438254a9d92 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -216,9 +216,9 @@ commands: # jobs: - docs-python310: + docs-python3: docker: - - image: cimg/python:3.10 + - image: cimg/python:3.12 resource_class: large steps: - checkout @@ -259,4 +259,4 @@ workflows: jobs: # NOTE: If you rename this job, then you must update the `if` condition # and `circleci-jobs` option in `.github/workflows/circleci.yml`. - - docs-python310 + - docs-python3 diff --git a/.github/workflows/circleci.yml b/.github/workflows/circleci.yml index c96dbecda7a1..a64b312e8246 100644 --- a/.github/workflows/circleci.yml +++ b/.github/workflows/circleci.yml @@ -3,7 +3,7 @@ name: "CircleCI artifact handling" on: [status] jobs: circleci_artifacts_redirector_job: - if: "${{ github.event.context == 'ci/circleci: docs-python310' }}" + if: "${{ github.event.context == 'ci/circleci: docs-python3' }}" permissions: statuses: write runs-on: ubuntu-latest @@ -16,11 +16,11 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} api-token: ${{ secrets.CIRCLECI_TOKEN }} artifact-path: 0/doc/build/html/index.html - circleci-jobs: docs-python310 + circleci-jobs: docs-python3 job-title: View the built docs post_warnings_as_review: - if: "${{ github.event.context == 'ci/circleci: docs-python310' }}" + if: "${{ github.event.context == 'ci/circleci: docs-python3' }}" permissions: contents: read checks: write From 82349cf1dbb2be880bc4c88366dbe922846c6bc3 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 25 Jul 2024 22:31:36 -0400 Subject: [PATCH 0077/1230] DOC: Enable parallel builds The latest sphinx-gallery 0.17.0 adds support for parallel building of examples, and the issue in pydata-sphinx-theme was fixed in 0.15.4, so we can try enabling it again. --- .circleci/config.yml | 2 +- doc/conf.py | 5 +++++ requirements/doc/doc-requirements.txt | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7436698c8068..688941ae9d9b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -147,7 +147,7 @@ commands: export RELEASE_TAG='-t release' fi mkdir -p logs - make html O="-T $RELEASE_TAG -j1 -w /tmp/sphinxerrorswarnings.log" + make html O="-T $RELEASE_TAG -j4 -w /tmp/sphinxerrorswarnings.log" rm -r build/html/_sources working_directory: doc - save_cache: diff --git a/doc/conf.py b/doc/conf.py index 1e6b3aefb5c5..6e736e16844f 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -286,6 +286,11 @@ def tutorials_download_error(record): 'copyfile_regex': r'.*\.rst', } +if parse_version(sphinx_gallery.__version__) >= parse_version('0.17.0'): + sphinx_gallery_conf['parallel'] = True + # Any warnings from joblib turned into errors may cause a deadlock. + warnings.filterwarnings('default', category=UserWarning, module='joblib') + if 'plot_gallery=0' in sys.argv: # Gallery images are not created. Suppress warnings triggered where other # parts of the documentation link to these images. diff --git a/requirements/doc/doc-requirements.txt b/requirements/doc/doc-requirements.txt index cee389da9e94..0666af1d49e8 100644 --- a/requirements/doc/doc-requirements.txt +++ b/requirements/doc/doc-requirements.txt @@ -20,5 +20,5 @@ pyyaml sphinxcontrib-svg2pdfconverter>=1.1.0 sphinx-copybutton sphinx-design -sphinx-gallery>=0.12.0 +sphinx-gallery[parallel]>=0.12.0 sphinx-tags>=0.4.0 From 92bbaa24e35aebf82f72f3b94a0ce959690ff653 Mon Sep 17 00:00:00 2001 From: Juanita Gomez Date: Thu, 25 Jul 2024 22:59:58 -0700 Subject: [PATCH 0078/1230] Update doc/devel/development_workflow.rst --- doc/devel/development_workflow.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/devel/development_workflow.rst b/doc/devel/development_workflow.rst index efbef2a0a1ac..03d59ad097e4 100644 --- a/doc/devel/development_workflow.rst +++ b/doc/devel/development_workflow.rst @@ -162,7 +162,7 @@ You can achieve this by using git push [your-remote-repo] [your-branch] --force-with-lease .. tip:: - Instead of typying your branch name every time, you can once do:: + Instead of typing your branch name every time, you only need to type the following once to link the remote branch to the local branch:: git push --set-upstream origin my-new-feature From 5cd2f80c897820dbcec9a8e24438dcb125434921 Mon Sep 17 00:00:00 2001 From: Refael Ackermann Date: Sat, 27 Jul 2024 16:56:16 -0400 Subject: [PATCH 0079/1230] TYP: Fix a typo in animation.pyi frames can be an Iterable of anything. I'm assuming this typo was caused by a file-wide search and replace, since all other Iterables in this file are indeed `Iterable[Artist]` --- lib/matplotlib/animation.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/animation.pyi b/lib/matplotlib/animation.pyi index 56c27a465b7f..345e3c6dbe61 100644 --- a/lib/matplotlib/animation.pyi +++ b/lib/matplotlib/animation.pyi @@ -207,7 +207,7 @@ class FuncAnimation(TimedAnimation): self, fig: Figure, func: Callable[..., Iterable[Artist]], - frames: Iterable[Artist] | int | Callable[[], Generator] | None = ..., + frames: Iterable | int | Callable[[], Generator] | None = ..., init_func: Callable[[], Iterable[Artist]] | None = ..., fargs: tuple[Any, ...] | None = ..., save_count: int | None = ..., From eecc0a09ddaadb575f001a1c3c57f3a838977dd9 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:53:59 +0200 Subject: [PATCH 0080/1230] DOC: Standardize example titles Following recommendatinos from #28527, this improves example titles. Take this as an incremental improvement. I've changed what I saw at a glance when going through the examples once. Certainly, one could do further improvements, but that can be done in follow-ups. Co-authored-by: hannah --- galleries/examples/animation/pause_resume.py | 2 +- galleries/examples/animation/unchained.py | 6 +++--- .../examples/axes_grid1/scatter_hist_locatable_axes.py | 6 +++--- galleries/examples/axisartist/simple_axis_pad.py | 2 +- galleries/examples/event_handling/close_event.py | 2 +- galleries/examples/event_handling/looking_glass.py | 2 +- galleries/examples/event_handling/poly_editor.py | 6 +++--- galleries/examples/event_handling/zoom_window.py | 6 +++--- .../examples/images_contours_and_fields/barb_demo.py | 2 +- .../colormap_interactive_adjustment.py | 2 +- .../images_contours_and_fields/contour_corner_mask.py | 2 +- .../examples/images_contours_and_fields/contour_image.py | 2 +- .../images_contours_and_fields/contourf_hatching.py | 2 +- .../examples/images_contours_and_fields/image_masked.py | 6 +++--- .../examples/images_contours_and_fields/layer_images.py | 6 +++--- .../examples/lines_bars_and_markers/bar_label_demo.py | 6 +++--- .../examples/lines_bars_and_markers/fill_between_alpha.py | 5 +++-- .../examples/lines_bars_and_markers/scatter_masked.py | 6 +++--- galleries/examples/lines_bars_and_markers/simple_plot.py | 8 ++++---- galleries/examples/lines_bars_and_markers/stem_plot.py | 2 +- galleries/examples/misc/image_thumbnail_sgskip.py | 2 +- galleries/examples/misc/print_stdout_sgskip.py | 6 +++--- galleries/examples/misc/svg_filter_line.py | 6 +++--- galleries/examples/scales/aspect_loglog.py | 2 +- galleries/examples/shapes_and_collections/quad_bezier.py | 2 +- .../subplots_axes_and_figures/align_labels_demo.py | 6 +++--- .../examples/subplots_axes_and_figures/axes_props.py | 6 +++--- .../subplots_axes_and_figures/axes_zoom_effect.py | 2 +- .../subplots_axes_and_figures/axis_labels_demo.py | 2 +- .../examples/subplots_axes_and_figures/broken_axis.py | 2 +- .../text_labels_and_annotations/annotate_transform.py | 2 +- .../text_labels_and_annotations/annotation_demo.py | 6 +++--- .../text_labels_and_annotations/annotation_polar.py | 6 +++--- .../text_labels_and_annotations/custom_legends.py | 6 +++--- .../demo_text_rotation_mode.py | 2 +- .../text_labels_and_annotations/mathtext_examples.py | 6 +++--- .../examples/text_labels_and_annotations/text_commands.py | 6 +++--- .../text_rotation_relative_to_line.py | 6 +++--- .../text_labels_and_annotations/usetex_baseline_test.py | 2 +- galleries/examples/ticks/date_precision_and_epochs.py | 2 +- galleries/examples/units/units_sample.py | 2 +- galleries/examples/widgets/range_slider.py | 6 +++--- 42 files changed, 85 insertions(+), 84 deletions(-) diff --git a/galleries/examples/animation/pause_resume.py b/galleries/examples/animation/pause_resume.py index 7b1fade30322..13de31f36f89 100644 --- a/galleries/examples/animation/pause_resume.py +++ b/galleries/examples/animation/pause_resume.py @@ -1,6 +1,6 @@ """ ================================= -Pausing and Resuming an Animation +Pausing and resuming an animation ================================= This example showcases: diff --git a/galleries/examples/animation/unchained.py b/galleries/examples/animation/unchained.py index e93ed03ff99e..4c49d80bba81 100644 --- a/galleries/examples/animation/unchained.py +++ b/galleries/examples/animation/unchained.py @@ -1,7 +1,7 @@ """ -======================== -MATPLOTLIB **UNCHAINED** -======================== +==================== +Matplotlib unchained +==================== Comparative path demonstration of frequency from a fake signal of a pulsar (mostly known because of the cover for Joy Division's Unknown Pleasures). diff --git a/galleries/examples/axes_grid1/scatter_hist_locatable_axes.py b/galleries/examples/axes_grid1/scatter_hist_locatable_axes.py index e5ff19d9ee08..3f9bc4305b3f 100644 --- a/galleries/examples/axes_grid1/scatter_hist_locatable_axes.py +++ b/galleries/examples/axes_grid1/scatter_hist_locatable_axes.py @@ -1,7 +1,7 @@ """ -================================== -Scatter Histogram (Locatable Axes) -================================== +==================================================== +Align histogram to scatter plot using locatable Axes +==================================================== Show the marginal distributions of a scatter plot as histograms at the sides of the plot. diff --git a/galleries/examples/axisartist/simple_axis_pad.py b/galleries/examples/axisartist/simple_axis_pad.py index 9c613c820b2b..95f30ce1ffbc 100644 --- a/galleries/examples/axisartist/simple_axis_pad.py +++ b/galleries/examples/axisartist/simple_axis_pad.py @@ -1,6 +1,6 @@ """ =============== -Simple Axis Pad +Simple axis pad =============== """ diff --git a/galleries/examples/event_handling/close_event.py b/galleries/examples/event_handling/close_event.py index 24b45b74ea48..060388269c8c 100644 --- a/galleries/examples/event_handling/close_event.py +++ b/galleries/examples/event_handling/close_event.py @@ -1,6 +1,6 @@ """ =========== -Close Event +Close event =========== Example to show connecting events that occur when the figure closes. diff --git a/galleries/examples/event_handling/looking_glass.py b/galleries/examples/event_handling/looking_glass.py index 6032b39b5b9e..a2a5f396c75a 100644 --- a/galleries/examples/event_handling/looking_glass.py +++ b/galleries/examples/event_handling/looking_glass.py @@ -1,6 +1,6 @@ """ ============= -Looking Glass +Looking glass ============= Example using mouse events to simulate a looking glass for inspecting data. diff --git a/galleries/examples/event_handling/poly_editor.py b/galleries/examples/event_handling/poly_editor.py index 5465cca0ed94..f6efd8bb8446 100644 --- a/galleries/examples/event_handling/poly_editor.py +++ b/galleries/examples/event_handling/poly_editor.py @@ -1,7 +1,7 @@ """ -=========== -Poly Editor -=========== +============== +Polygon editor +============== This is an example to show how to build cross-GUI applications using Matplotlib event handling to interact with objects on the canvas. diff --git a/galleries/examples/event_handling/zoom_window.py b/galleries/examples/event_handling/zoom_window.py index b8ba4c1048a9..6a90a175fb68 100644 --- a/galleries/examples/event_handling/zoom_window.py +++ b/galleries/examples/event_handling/zoom_window.py @@ -1,7 +1,7 @@ """ -=========== -Zoom Window -=========== +======================== +Zoom modifies other Axes +======================== This example shows how to connect events in one window, for example, a mouse press, to another figure window. diff --git a/galleries/examples/images_contours_and_fields/barb_demo.py b/galleries/examples/images_contours_and_fields/barb_demo.py index d3ade99d927c..9229b5262a2c 100644 --- a/galleries/examples/images_contours_and_fields/barb_demo.py +++ b/galleries/examples/images_contours_and_fields/barb_demo.py @@ -1,6 +1,6 @@ """ ========== -Wind Barbs +Wind barbs ========== Demonstration of wind barb plots. diff --git a/galleries/examples/images_contours_and_fields/colormap_interactive_adjustment.py b/galleries/examples/images_contours_and_fields/colormap_interactive_adjustment.py index 3ab9074fd1b6..3db799894c95 100644 --- a/galleries/examples/images_contours_and_fields/colormap_interactive_adjustment.py +++ b/galleries/examples/images_contours_and_fields/colormap_interactive_adjustment.py @@ -1,6 +1,6 @@ """ ======================================== -Interactive Adjustment of Colormap Range +Interactive adjustment of colormap range ======================================== Demonstration of how a colorbar can be used to interactively adjust the diff --git a/galleries/examples/images_contours_and_fields/contour_corner_mask.py b/galleries/examples/images_contours_and_fields/contour_corner_mask.py index 400f47aa4db5..696231146733 100644 --- a/galleries/examples/images_contours_and_fields/contour_corner_mask.py +++ b/galleries/examples/images_contours_and_fields/contour_corner_mask.py @@ -1,6 +1,6 @@ """ =================== -Contour Corner Mask +Contour corner mask =================== Illustrate the difference between ``corner_mask=False`` and diff --git a/galleries/examples/images_contours_and_fields/contour_image.py b/galleries/examples/images_contours_and_fields/contour_image.py index 3b33233852b7..f60cfee2b61e 100644 --- a/galleries/examples/images_contours_and_fields/contour_image.py +++ b/galleries/examples/images_contours_and_fields/contour_image.py @@ -1,6 +1,6 @@ """ ============= -Contour Image +Contour image ============= Test combinations of contouring, filled contouring, and image plotting. diff --git a/galleries/examples/images_contours_and_fields/contourf_hatching.py b/galleries/examples/images_contours_and_fields/contourf_hatching.py index f8131b41cfa5..020c20b44ec4 100644 --- a/galleries/examples/images_contours_and_fields/contourf_hatching.py +++ b/galleries/examples/images_contours_and_fields/contourf_hatching.py @@ -1,6 +1,6 @@ """ ================= -Contourf Hatching +Contourf hatching ================= Demo filled contour plots with hatched patterns. diff --git a/galleries/examples/images_contours_and_fields/image_masked.py b/galleries/examples/images_contours_and_fields/image_masked.py index d64ab2cff8c7..3d4058c62eb7 100644 --- a/galleries/examples/images_contours_and_fields/image_masked.py +++ b/galleries/examples/images_contours_and_fields/image_masked.py @@ -1,7 +1,7 @@ """ -============ -Image Masked -============ +======================== +Image with masked values +======================== imshow with masked array input and out-of-range colors. diff --git a/galleries/examples/images_contours_and_fields/layer_images.py b/galleries/examples/images_contours_and_fields/layer_images.py index bcaa25471500..c67c08960ecd 100644 --- a/galleries/examples/images_contours_and_fields/layer_images.py +++ b/galleries/examples/images_contours_and_fields/layer_images.py @@ -1,7 +1,7 @@ """ -============ -Layer Images -============ +================================ +Layer images with alpha blending +================================ Layer images above one another using alpha blending """ diff --git a/galleries/examples/lines_bars_and_markers/bar_label_demo.py b/galleries/examples/lines_bars_and_markers/bar_label_demo.py index d60bd2a16299..8393407d1c57 100644 --- a/galleries/examples/lines_bars_and_markers/bar_label_demo.py +++ b/galleries/examples/lines_bars_and_markers/bar_label_demo.py @@ -1,7 +1,7 @@ """ -============== -Bar Label Demo -============== +===================== +Bar chart with labels +===================== This example shows how to use the `~.Axes.bar_label` helper function to create bar chart labels. diff --git a/galleries/examples/lines_bars_and_markers/fill_between_alpha.py b/galleries/examples/lines_bars_and_markers/fill_between_alpha.py index 3894d9d1d45c..2887310378d1 100644 --- a/galleries/examples/lines_bars_and_markers/fill_between_alpha.py +++ b/galleries/examples/lines_bars_and_markers/fill_between_alpha.py @@ -1,6 +1,7 @@ """ -Fill Between and Alpha -====================== +============================== +Fill Between with transparency +============================== The `~matplotlib.axes.Axes.fill_between` function generates a shaded region between a min and max boundary that is useful for illustrating ranges. diff --git a/galleries/examples/lines_bars_and_markers/scatter_masked.py b/galleries/examples/lines_bars_and_markers/scatter_masked.py index 22c0943bf28a..c8e603e6f3b0 100644 --- a/galleries/examples/lines_bars_and_markers/scatter_masked.py +++ b/galleries/examples/lines_bars_and_markers/scatter_masked.py @@ -1,7 +1,7 @@ """ -============== -Scatter Masked -============== +========================== +Scatter with masked values +========================== Mask some data points and add a line demarking masked regions. diff --git a/galleries/examples/lines_bars_and_markers/simple_plot.py b/galleries/examples/lines_bars_and_markers/simple_plot.py index 520d6fac8204..c8182035fc41 100644 --- a/galleries/examples/lines_bars_and_markers/simple_plot.py +++ b/galleries/examples/lines_bars_and_markers/simple_plot.py @@ -1,9 +1,9 @@ """ -=========== -Simple Plot -=========== +========= +Line plot +========= -Create a simple plot. +Create a basic line plot. """ import matplotlib.pyplot as plt diff --git a/galleries/examples/lines_bars_and_markers/stem_plot.py b/galleries/examples/lines_bars_and_markers/stem_plot.py index f3035c1673e6..d779197e50cc 100644 --- a/galleries/examples/lines_bars_and_markers/stem_plot.py +++ b/galleries/examples/lines_bars_and_markers/stem_plot.py @@ -1,6 +1,6 @@ """ ========= -Stem Plot +Stem plot ========= `~.pyplot.stem` plots vertical lines from a baseline to the y-coordinate and diff --git a/galleries/examples/misc/image_thumbnail_sgskip.py b/galleries/examples/misc/image_thumbnail_sgskip.py index edc1e5aa3573..55217cfdca02 100644 --- a/galleries/examples/misc/image_thumbnail_sgskip.py +++ b/galleries/examples/misc/image_thumbnail_sgskip.py @@ -1,6 +1,6 @@ """ =============== -Image Thumbnail +Image thumbnail =============== You can use Matplotlib to generate thumbnails from existing images. diff --git a/galleries/examples/misc/print_stdout_sgskip.py b/galleries/examples/misc/print_stdout_sgskip.py index 4a8b63f6d03e..9c9848a73d9c 100644 --- a/galleries/examples/misc/print_stdout_sgskip.py +++ b/galleries/examples/misc/print_stdout_sgskip.py @@ -1,7 +1,7 @@ """ -============ -Print Stdout -============ +===================== +Print image to stdout +===================== print png to standard out diff --git a/galleries/examples/misc/svg_filter_line.py b/galleries/examples/misc/svg_filter_line.py index 5cc4af5d7a66..c6adec093bee 100644 --- a/galleries/examples/misc/svg_filter_line.py +++ b/galleries/examples/misc/svg_filter_line.py @@ -1,7 +1,7 @@ """ -=============== -SVG Filter Line -=============== +========================== +Apply SVG filter to a line +========================== Demonstrate SVG filtering effects which might be used with Matplotlib. diff --git a/galleries/examples/scales/aspect_loglog.py b/galleries/examples/scales/aspect_loglog.py index 90c0422ca389..420721b9b411 100644 --- a/galleries/examples/scales/aspect_loglog.py +++ b/galleries/examples/scales/aspect_loglog.py @@ -1,6 +1,6 @@ """ ============= -Loglog Aspect +Loglog aspect ============= """ diff --git a/galleries/examples/shapes_and_collections/quad_bezier.py b/galleries/examples/shapes_and_collections/quad_bezier.py index 6f91ad85bf8f..f4a688233ba9 100644 --- a/galleries/examples/shapes_and_collections/quad_bezier.py +++ b/galleries/examples/shapes_and_collections/quad_bezier.py @@ -1,6 +1,6 @@ """ ============ -Bezier Curve +Bezier curve ============ This example showcases the `~.patches.PathPatch` object to create a Bezier diff --git a/galleries/examples/subplots_axes_and_figures/align_labels_demo.py b/galleries/examples/subplots_axes_and_figures/align_labels_demo.py index 4935878ee027..8e9a70d4ccd9 100644 --- a/galleries/examples/subplots_axes_and_figures/align_labels_demo.py +++ b/galleries/examples/subplots_axes_and_figures/align_labels_demo.py @@ -1,7 +1,7 @@ """ -========================== -Aligning Labels and Titles -========================== +======================= +Align labels and titles +======================= Aligning xlabel, ylabel, and title using `.Figure.align_xlabels`, `.Figure.align_ylabels`, and `.Figure.align_titles`. diff --git a/galleries/examples/subplots_axes_and_figures/axes_props.py b/galleries/examples/subplots_axes_and_figures/axes_props.py index f2e52febed34..106c8e0db1ee 100644 --- a/galleries/examples/subplots_axes_and_figures/axes_props.py +++ b/galleries/examples/subplots_axes_and_figures/axes_props.py @@ -1,7 +1,7 @@ """ -========== -Axes Props -========== +=============== +Axes properties +=============== You can control the axis tick and grid properties """ diff --git a/galleries/examples/subplots_axes_and_figures/axes_zoom_effect.py b/galleries/examples/subplots_axes_and_figures/axes_zoom_effect.py index 49a44b9e4f43..f139d0209427 100644 --- a/galleries/examples/subplots_axes_and_figures/axes_zoom_effect.py +++ b/galleries/examples/subplots_axes_and_figures/axes_zoom_effect.py @@ -1,6 +1,6 @@ """ ================ -Axes Zoom Effect +Axes zoom effect ================ """ diff --git a/galleries/examples/subplots_axes_and_figures/axis_labels_demo.py b/galleries/examples/subplots_axes_and_figures/axis_labels_demo.py index 8b9d38240e42..ea99b78d8fb0 100644 --- a/galleries/examples/subplots_axes_and_figures/axis_labels_demo.py +++ b/galleries/examples/subplots_axes_and_figures/axis_labels_demo.py @@ -1,6 +1,6 @@ """ =================== -Axis Label Position +Axis label position =================== Choose axis label position when calling `~.Axes.set_xlabel` and diff --git a/galleries/examples/subplots_axes_and_figures/broken_axis.py b/galleries/examples/subplots_axes_and_figures/broken_axis.py index 06263b9c120a..4d6ece305ed6 100644 --- a/galleries/examples/subplots_axes_and_figures/broken_axis.py +++ b/galleries/examples/subplots_axes_and_figures/broken_axis.py @@ -1,6 +1,6 @@ """ =========== -Broken Axis +Broken axis =========== Broken axis example, where the y-axis will have a portion cut out. diff --git a/galleries/examples/text_labels_and_annotations/annotate_transform.py b/galleries/examples/text_labels_and_annotations/annotate_transform.py index b2ce1de6a0c1..e7d4e11d9d38 100644 --- a/galleries/examples/text_labels_and_annotations/annotate_transform.py +++ b/galleries/examples/text_labels_and_annotations/annotate_transform.py @@ -1,6 +1,6 @@ """ ================== -Annotate Transform +Annotate transform ================== This example shows how to use different coordinate systems for annotations. diff --git a/galleries/examples/text_labels_and_annotations/annotation_demo.py b/galleries/examples/text_labels_and_annotations/annotation_demo.py index 5358bfaac60a..562948bcc512 100644 --- a/galleries/examples/text_labels_and_annotations/annotation_demo.py +++ b/galleries/examples/text_labels_and_annotations/annotation_demo.py @@ -1,7 +1,7 @@ """ -================ -Annotating Plots -================ +============== +Annotate plots +============== The following examples show ways to annotate plots in Matplotlib. This includes highlighting specific points of interest and using various diff --git a/galleries/examples/text_labels_and_annotations/annotation_polar.py b/galleries/examples/text_labels_and_annotations/annotation_polar.py index bbd46478bced..c2418519cf8c 100644 --- a/galleries/examples/text_labels_and_annotations/annotation_polar.py +++ b/galleries/examples/text_labels_and_annotations/annotation_polar.py @@ -1,7 +1,7 @@ """ -================ -Annotation Polar -================ +==================== +Annotate polar plots +==================== This example shows how to create an annotation on a polar graph. diff --git a/galleries/examples/text_labels_and_annotations/custom_legends.py b/galleries/examples/text_labels_and_annotations/custom_legends.py index 18ace0513228..80200c528224 100644 --- a/galleries/examples/text_labels_and_annotations/custom_legends.py +++ b/galleries/examples/text_labels_and_annotations/custom_legends.py @@ -1,7 +1,7 @@ """ -======================== -Composing Custom Legends -======================== +====================== +Compose custom legends +====================== Composing custom legends piece-by-piece. diff --git a/galleries/examples/text_labels_and_annotations/demo_text_rotation_mode.py b/galleries/examples/text_labels_and_annotations/demo_text_rotation_mode.py index f8f3a108629c..9cb7f30302fc 100644 --- a/galleries/examples/text_labels_and_annotations/demo_text_rotation_mode.py +++ b/galleries/examples/text_labels_and_annotations/demo_text_rotation_mode.py @@ -1,6 +1,6 @@ r""" ================== -Text Rotation Mode +Text rotation mode ================== This example illustrates the effect of ``rotation_mode`` on the positioning diff --git a/galleries/examples/text_labels_and_annotations/mathtext_examples.py b/galleries/examples/text_labels_and_annotations/mathtext_examples.py index 0cdae3c8193c..f9f8e628e08b 100644 --- a/galleries/examples/text_labels_and_annotations/mathtext_examples.py +++ b/galleries/examples/text_labels_and_annotations/mathtext_examples.py @@ -1,7 +1,7 @@ """ -================= -Mathtext Examples -================= +======================== +Mathematical expressions +======================== Selected features of Matplotlib's math rendering engine. """ diff --git a/galleries/examples/text_labels_and_annotations/text_commands.py b/galleries/examples/text_labels_and_annotations/text_commands.py index 35f2c1c1a0c4..0650ff53bd5d 100644 --- a/galleries/examples/text_labels_and_annotations/text_commands.py +++ b/galleries/examples/text_labels_and_annotations/text_commands.py @@ -1,7 +1,7 @@ """ -============= -Text Commands -============= +=============== +Text properties +=============== Plotting text of many different kinds. diff --git a/galleries/examples/text_labels_and_annotations/text_rotation_relative_to_line.py b/galleries/examples/text_labels_and_annotations/text_rotation_relative_to_line.py index 4672f5c5772d..ae29385e8a6d 100644 --- a/galleries/examples/text_labels_and_annotations/text_rotation_relative_to_line.py +++ b/galleries/examples/text_labels_and_annotations/text_rotation_relative_to_line.py @@ -1,7 +1,7 @@ """ -============================== -Text Rotation Relative To Line -============================== +======================================= +Text rotation angle in data coordinates +======================================= Text objects in matplotlib are normally rotated with respect to the screen coordinate system (i.e., 45 degrees rotation plots text along a diff --git a/galleries/examples/text_labels_and_annotations/usetex_baseline_test.py b/galleries/examples/text_labels_and_annotations/usetex_baseline_test.py index 49303e244821..e529b1c8b2de 100644 --- a/galleries/examples/text_labels_and_annotations/usetex_baseline_test.py +++ b/galleries/examples/text_labels_and_annotations/usetex_baseline_test.py @@ -1,6 +1,6 @@ """ ==================== -Usetex Baseline Test +Usetex text baseline ==================== Comparison of text baselines computed for mathtext and usetex. diff --git a/galleries/examples/ticks/date_precision_and_epochs.py b/galleries/examples/ticks/date_precision_and_epochs.py index c4b87127d3c0..eb4926cab68d 100644 --- a/galleries/examples/ticks/date_precision_and_epochs.py +++ b/galleries/examples/ticks/date_precision_and_epochs.py @@ -1,6 +1,6 @@ """ ========================= -Date Precision and Epochs +Date precision and epochs ========================= Matplotlib can handle `.datetime` objects and `numpy.datetime64` objects using diff --git a/galleries/examples/units/units_sample.py b/galleries/examples/units/units_sample.py index 5c1d53fa2dee..2690ee7db727 100644 --- a/galleries/examples/units/units_sample.py +++ b/galleries/examples/units/units_sample.py @@ -1,6 +1,6 @@ """ ====================== -Inches and Centimeters +Inches and centimeters ====================== The example illustrates the ability to override default x and y units (ax1) to diff --git a/galleries/examples/widgets/range_slider.py b/galleries/examples/widgets/range_slider.py index 1ae40c9841fe..f1bed7431e39 100644 --- a/galleries/examples/widgets/range_slider.py +++ b/galleries/examples/widgets/range_slider.py @@ -1,7 +1,7 @@ """ -====================================== -Thresholding an Image with RangeSlider -====================================== +================================= +Image scaling using a RangeSlider +================================= Using the RangeSlider widget to control the thresholding of an image. From 4bedccf11b683b7b9ada06261818ab01ae56c659 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:04:18 +0200 Subject: [PATCH 0081/1230] Backport PR #28621: TYP: Fix a typo in animation.pyi --- lib/matplotlib/animation.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/animation.pyi b/lib/matplotlib/animation.pyi index 56c27a465b7f..345e3c6dbe61 100644 --- a/lib/matplotlib/animation.pyi +++ b/lib/matplotlib/animation.pyi @@ -207,7 +207,7 @@ class FuncAnimation(TimedAnimation): self, fig: Figure, func: Callable[..., Iterable[Artist]], - frames: Iterable[Artist] | int | Callable[[], Generator] | None = ..., + frames: Iterable | int | Callable[[], Generator] | None = ..., init_func: Callable[[], Iterable[Artist]] | None = ..., fargs: tuple[Any, ...] | None = ..., save_count: int | None = ..., From 7aefebaa986649022c36e9a3917d2a1293875ac6 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Mon, 29 Jul 2024 20:02:21 +0200 Subject: [PATCH 0082/1230] added typing_extensions.Self to _AxesBase.twinx --- environment.yml | 1 + lib/matplotlib/axes/_base.pyi | 5 +++-- pyproject.toml | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index 264f02800690..3ca4ca6b5050 100644 --- a/environment.yml +++ b/environment.yml @@ -28,6 +28,7 @@ dependencies: - python-dateutil>=2.1 - setuptools_scm - wxpython + - typing-extensions>=4.0.0 # building documentation - colorspacious - graphviz diff --git a/lib/matplotlib/axes/_base.pyi b/lib/matplotlib/axes/_base.pyi index 1fdc0750f0bc..4903bb41645e 100644 --- a/lib/matplotlib/axes/_base.pyi +++ b/lib/matplotlib/axes/_base.pyi @@ -27,6 +27,7 @@ from cycler import Cycler import numpy as np from numpy.typing import ArrayLike from typing import Any, Literal, TypeVar, overload +from typing_extensions import Self from matplotlib.typing import ColorType _T = TypeVar("_T", bound=Artist) @@ -384,8 +385,8 @@ class _AxesBase(martist.Artist): bbox_extra_artists: Sequence[Artist] | None = ..., for_layout_only: bool = ... ) -> Bbox | None: ... - def twinx(self) -> _AxesBase: ... - def twiny(self) -> _AxesBase: ... + def twinx(self) -> Self: ... + def twiny(self) -> Self: ... def get_shared_x_axes(self) -> cbook.GrouperView: ... def get_shared_y_axes(self) -> cbook.GrouperView: ... def label_outer(self, remove_inner_ticks: bool = ...) -> None: ... diff --git a/pyproject.toml b/pyproject.toml index 0f181ccb629e..b7663c968878 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ dependencies = [ "pillow >= 8", "pyparsing >= 2.3.1", "python-dateutil >= 2.7", + "typing-extensions >= 4.0.0", ] requires-python = ">=3.10" From 58e16b1ce9c0c6f7accd26528500f7e301e7da07 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Tue, 30 Jul 2024 11:31:20 +0200 Subject: [PATCH 0083/1230] removed dependency --- environment.yml | 1 - pyproject.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/environment.yml b/environment.yml index 3ca4ca6b5050..264f02800690 100644 --- a/environment.yml +++ b/environment.yml @@ -28,7 +28,6 @@ dependencies: - python-dateutil>=2.1 - setuptools_scm - wxpython - - typing-extensions>=4.0.0 # building documentation - colorspacious - graphviz diff --git a/pyproject.toml b/pyproject.toml index b7663c968878..0f181ccb629e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,6 @@ dependencies = [ "pillow >= 8", "pyparsing >= 2.3.1", "python-dateutil >= 2.7", - "typing-extensions >= 4.0.0", ] requires-python = ">=3.10" From ab2c055c286755f402b19414bfd2db48bc8e7d3d Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 30 Jul 2024 12:07:10 +0200 Subject: [PATCH 0084/1230] DOC: Sub-structure next API changes overview --- doc/api/next_api_changes.rst | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/doc/api/next_api_changes.rst b/doc/api/next_api_changes.rst index d33c8014f735..9c0630697763 100644 --- a/doc/api/next_api_changes.rst +++ b/doc/api/next_api_changes.rst @@ -5,11 +5,40 @@ Next API changes .. ifconfig:: releaselevel == 'dev' + This page lists API changes for the next release. + + Behavior changes + ---------------- + .. toctree:: :glob: :maxdepth: 1 next_api_changes/behavior/* + + Deprecations + ------------ + + .. toctree:: + :glob: + :maxdepth: 1 + next_api_changes/deprecations/* - next_api_changes/development/* + + Removals + -------- + + .. toctree:: + :glob: + :maxdepth: 1 + next_api_changes/removals/* + + Development changes + ------------------- + + .. toctree:: + :glob: + :maxdepth: 1 + + next_api_changes/development/* From a63dc33454466615cfadae63e64cb5ead255c2e9 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:49:51 +0000 Subject: [PATCH 0085/1230] FIX: Axis.set_in_layout respected --- lib/matplotlib/axis.py | 2 +- lib/matplotlib/tests/test_axis.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 483c9a3db15f..158d4a02ee61 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1362,7 +1362,7 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): collapsed to near zero. This allows tight/constrained_layout to ignore too-long labels when doing their layout. """ - if not self.get_visible(): + if not self.get_visible() or for_layout_only and not self.get_in_layout(): return if renderer is None: renderer = self.figure._get_renderer() diff --git a/lib/matplotlib/tests/test_axis.py b/lib/matplotlib/tests/test_axis.py index 97b5f88dede1..33af30662a33 100644 --- a/lib/matplotlib/tests/test_axis.py +++ b/lib/matplotlib/tests/test_axis.py @@ -8,3 +8,24 @@ def test_tick_labelcolor_array(): # Smoke test that we can instantiate a Tick with labelcolor as array. ax = plt.axes() XTick(ax, 0, labelcolor=np.array([1, 0, 0, 1])) + + +def test_axis_not_in_layout(): + fig1, (ax1_left, ax1_right) = plt.subplots(ncols=2, layout='constrained') + fig2, (ax2_left, ax2_right) = plt.subplots(ncols=2, layout='constrained') + + # 100 label overlapping the end of the axis + ax1_left.set_xlim([0, 100]) + # 100 label not overlapping the end of the axis + ax2_left.set_xlim([0, 120]) + + for ax in ax1_left, ax2_left: + ax.set_xticks([0, 100]) + ax.xaxis.set_in_layout(False) + + for fig in fig1, fig2: + fig.draw_without_rendering() + + # Positions should not be affected by overlapping 100 label + assert ax1_left.get_position().bounds == ax2_left.get_position().bounds + assert ax1_right.get_position().bounds == ax2_right.get_position().bounds From 64cd3dea3c4b8501ddf78fd1371dc699ebe41124 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 26 Jul 2024 04:03:24 -0400 Subject: [PATCH 0086/1230] DOC: Bump minimum Sphinx to 5.1.0 We depend on `pydata-sphinx-theme` 0.15.0, which requires Sphinx 5, and `sphinx-tags`, which requires Sphinx 5.1, so we really shouldn't create an environment with Sphinx older than that. --- doc/conf.py | 4 ++-- requirements/doc/doc-requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 6e736e16844f..882370b7e255 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -360,8 +360,8 @@ def gallery_image_warning_filter(record): # This is the default encoding, but it doesn't hurt to be explicit source_encoding = "utf-8" -# The toplevel toctree document (renamed to root_doc in Sphinx 4.0) -root_doc = master_doc = 'index' +# The toplevel toctree document. +root_doc = 'index' # General substitutions. try: diff --git a/requirements/doc/doc-requirements.txt b/requirements/doc/doc-requirements.txt index 0666af1d49e8..1a009f5854fc 100644 --- a/requirements/doc/doc-requirements.txt +++ b/requirements/doc/doc-requirements.txt @@ -7,7 +7,7 @@ # Install the documentation requirements with: # pip install -r requirements/doc/doc-requirements.txt # -sphinx>=3.0.0,!=6.1.2 +sphinx>=5.1.0,!=6.1.2 colorspacious ipython ipywidgets From 1f339e88f4d8f66048a893728269f4ad2c443ffb Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 26 Jul 2024 20:32:26 -0400 Subject: [PATCH 0087/1230] DOC: Remove unused template Its use was removed in #11451. --- doc/_templates/autofunctions.rst | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 doc/_templates/autofunctions.rst diff --git a/doc/_templates/autofunctions.rst b/doc/_templates/autofunctions.rst deleted file mode 100644 index 291b8eee2ede..000000000000 --- a/doc/_templates/autofunctions.rst +++ /dev/null @@ -1,22 +0,0 @@ - -{{ fullname | escape | underline }} - - -.. automodule:: {{ fullname }} - :no-members: - -{% block functions %} -{% if functions %} - -Functions ---------- - -.. autosummary:: - :template: autosummary.rst - :toctree: - -{% for item in functions %}{% if item not in ['plotting', 'colormaps'] %} - {{ item }}{% endif %}{% endfor %} - -{% endif %} -{% endblock %} From 350ed268ba6f7687d5c17209433f55b471e88135 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 29 Jul 2024 18:50:54 -0400 Subject: [PATCH 0088/1230] DOC: Fix some broken references --- doc/missing-references.json | 18 ------------------ doc/users/next_whats_new/README.rst | 3 ++- lib/matplotlib/axes/_base.py | 2 +- lib/matplotlib/backend_bases.py | 2 +- lib/matplotlib/backends/backend_template.py | 4 ++-- lib/matplotlib/testing/decorators.py | 2 +- 6 files changed, 7 insertions(+), 24 deletions(-) diff --git a/doc/missing-references.json b/doc/missing-references.json index 1b0a6f9ef226..b04a729772eb 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -337,9 +337,6 @@ "Artist.stale_callback": [ "doc/users/explain/figure/interactive_guide.rst:323" ], - "Artist.sticky_edges": [ - "doc/api/axes_api.rst:356::1" - ], "Axes.dataLim": [ "doc/api/axes_api.rst:293::1", "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.update_datalim:2" @@ -357,9 +354,6 @@ "Image": [ "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.gci:4" ], - "ImageComparisonFailure": [ - "lib/matplotlib/testing/decorators.py:docstring of matplotlib.testing.decorators.image_comparison:2" - ], "Line2D.pick": [ "doc/users/explain/figure/event_handling.rst:568" ], @@ -665,29 +659,17 @@ "mpl_toolkits.axislines.Axes": [ "lib/mpl_toolkits/axisartist/axis_artist.py:docstring of mpl_toolkits.axisartist.axis_artist:7" ], - "next_whats_new": [ - "doc/users/next_whats_new/README.rst:6" - ], "option_scale_image": [ "lib/matplotlib/backends/backend_cairo.py:docstring of matplotlib.backends.backend_cairo.RendererCairo.draw_image:22", "lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.RendererPdf.draw_image:22", "lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps.RendererPS.draw_image:22", "lib/matplotlib/backends/backend_template.py:docstring of matplotlib.backends.backend_template.RendererTemplate.draw_image:22" ], - "print_xyz": [ - "lib/matplotlib/backends/backend_template.py:docstring of matplotlib.backends.backend_template:22" - ], "toggled": [ "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.AxisScaleBase.disable:4", "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.AxisScaleBase.enable:4", "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.AxisScaleBase.trigger:2", "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.ZoomPanBase.trigger:2" - ], - "tool_removed_event": [ - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase.remove_toolitem:6" - ], - "whats_new.rst": [ - "doc/users/next_whats_new/README.rst:6" ] } } diff --git a/doc/users/next_whats_new/README.rst b/doc/users/next_whats_new/README.rst index 98b601ee32d8..dabe676afaaf 100644 --- a/doc/users/next_whats_new/README.rst +++ b/doc/users/next_whats_new/README.rst @@ -3,7 +3,8 @@ Instructions for writing "What's new" entries ============================================= -Please place new portions of `whats_new.rst` in the `next_whats_new` directory. +Please place new portions of :file:`whats_new.rst` in the :file:`next_whats_new` +directory. When adding an entry please look at the currently existing files to see if you can extend any of them. If you create a file, name it diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 688b8d78b601..a0e588569465 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2635,7 +2635,7 @@ def set_autoscale_on(self, b): @property def use_sticky_edges(self): """ - When autoscaling, whether to obey all `Artist.sticky_edges`. + When autoscaling, whether to obey all `.Artist.sticky_edges`. Default is ``True``. diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 4b818d7fcdbd..788dd42ce917 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3419,7 +3419,7 @@ def remove_toolitem(self, name): This hook must be implemented in each backend and contains the backend-specific code to remove an element from the toolbar; it is - called when `.ToolManager` emits a `tool_removed_event`. + called when `.ToolManager` emits a ``tool_removed_event``. Because some tools are present only on the `.ToolManager` but not on the `ToolContainer`, this method must be a no-op when called on a tool diff --git a/lib/matplotlib/backends/backend_template.py b/lib/matplotlib/backends/backend_template.py index d997ec160a53..83aa6bb567c1 100644 --- a/lib/matplotlib/backends/backend_template.py +++ b/lib/matplotlib/backends/backend_template.py @@ -20,8 +20,8 @@ import matplotlib matplotlib.use("module://my.backend") -If your backend implements support for saving figures (i.e. has a `print_xyz` -method), you can register it as the default handler for a given file type:: +If your backend implements support for saving figures (i.e. has a ``print_xyz`` method), +you can register it as the default handler for a given file type:: from matplotlib.backend_bases import register_backend register_backend('xyz', 'my_backend', 'XYZ File Format') diff --git a/lib/matplotlib/testing/decorators.py b/lib/matplotlib/testing/decorators.py index 49ac4a1506f8..6f1af7debdb3 100644 --- a/lib/matplotlib/testing/decorators.py +++ b/lib/matplotlib/testing/decorators.py @@ -263,7 +263,7 @@ def image_comparison(baseline_images, extensions=None, tol=0, style=("classic", "_classic_test_patch")): """ Compare images generated by the test with those specified in - *baseline_images*, which must correspond, else an `ImageComparisonFailure` + *baseline_images*, which must correspond, else an `.ImageComparisonFailure` exception will be raised. Parameters From 42bfa268429efffb04c0712ea7d9d4dd773ae886 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 29 Jul 2024 19:22:27 -0400 Subject: [PATCH 0089/1230] DOC: Fix inclusion of next API/what's new instructions --- doc/api/next_api_changes/README.rst | 16 +++++++++++----- doc/devel/api_changes.rst | 8 ++++---- doc/users/next_whats_new/README.rst | 17 ++++++++++++----- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/doc/api/next_api_changes/README.rst b/doc/api/next_api_changes/README.rst index 75e70b456eb9..030a2644cdd4 100644 --- a/doc/api/next_api_changes/README.rst +++ b/doc/api/next_api_changes/README.rst @@ -1,10 +1,18 @@ :orphan: +.. NOTE TO EDITORS OF THIS FILE + This file serves as the README directly available in the file system next to the + next_api_changes entries. The content between the ``api-change-guide-*`` markers is + additionally included in the documentation page ``doc/devel/api_changes.rst``. Please + check that the page builds correctly after changing this file. + Adding API change notes ======================= -API change notes for future releases are collected in -:file:`next_api_changes`. They are divided into four subdirectories: +.. api-change-guide-start + +API change notes for future releases are collected in :file:`doc/api/next_api_changes/`. +They are divided into four subdirectories: - **Deprecations**: Announcements of future changes. Typically, these will raise a deprecation warning and users of this API should change their code @@ -33,6 +41,4 @@ Please avoid using references in section titles, as it causes links to be confusing in the table of contents. Instead, ensure that a reference is included in the descriptive text. -.. NOTE - Lines 5-30 of this file are include in :ref:`api_whats_new`; - therefore, please check the doc build after changing this file. +.. api-change-guide-end diff --git a/doc/devel/api_changes.rst b/doc/devel/api_changes.rst index b7d0a4b063ce..0e86f11a3694 100644 --- a/doc/devel/api_changes.rst +++ b/doc/devel/api_changes.rst @@ -216,8 +216,8 @@ API change notes """""""""""""""" .. include:: ../api/next_api_changes/README.rst - :start-line: 5 - :end-line: 31 + :start-after: api-change-guide-start + :end-before: api-change-guide-end .. _whats-new-notes: @@ -225,5 +225,5 @@ What's new notes """""""""""""""" .. include:: ../users/next_whats_new/README.rst - :start-line: 5 - :end-line: 24 + :start-after: whats-new-guide-start + :end-before: whats-new-guide-end diff --git a/doc/users/next_whats_new/README.rst b/doc/users/next_whats_new/README.rst index dabe676afaaf..362feda65271 100644 --- a/doc/users/next_whats_new/README.rst +++ b/doc/users/next_whats_new/README.rst @@ -1,10 +1,19 @@ :orphan: +.. NOTE TO EDITORS OF THIS FILE + This file serves as the README directly available in the file system next to the + next_whats_new entries. The content between the ``whats-new-guide-*`` markers is + additionally included in the documentation page ``doc/devel/api_changes.rst``. Please + check that the page builds correctly after changing this file. + + Instructions for writing "What's new" entries ============================================= -Please place new portions of :file:`whats_new.rst` in the :file:`next_whats_new` -directory. +.. whats-new-guide-start + +Please place new portions of :file:`whats_new.rst` in the +:file:`doc/users/next_whats_new/` directory. When adding an entry please look at the currently existing files to see if you can extend any of them. If you create a file, name it @@ -27,6 +36,4 @@ Please avoid using references in section titles, as it causes links to be confusing in the table of contents. Instead, ensure that a reference is included in the descriptive text. -.. NOTE - Lines 5-24 of this file are include in :ref:`api_whats_new`; - therefore, please check the doc build after changing this file. +.. whats-new-guide-end From cda437289b0cf41a01571904b6a13bedf8f037a4 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 30 Jul 2024 01:01:52 -0400 Subject: [PATCH 0090/1230] DOC: Simplify missing references extension Sphinx 3.4 added a `warn-missing-reference` event, from which we can: 1. record the missing reference, avoiding any of the messing about with logging, and; 2. suppress the warning, avoiding any messing about with the `nitpicky_ignore` settings and their changing defaults. Also, simplify some of the callbacks by simply not connect the events if not necessary, instead of checking in every one. --- doc/missing-references.json | 151 +++++++++++++------------- doc/sphinxext/missing_references.py | 162 ++++++++-------------------- 2 files changed, 119 insertions(+), 194 deletions(-) diff --git a/doc/missing-references.json b/doc/missing-references.json index b04a729772eb..7eb45863589d 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -36,11 +36,11 @@ "lib/matplotlib/colorbar.py:docstring of matplotlib.colorbar.Colorbar.add_lines:4" ], "matplotlib.axes.Axes.patch": [ - "doc/tutorials/artists.rst:188", - "doc/tutorials/artists.rst:427" + "doc/tutorials/artists.rst:185", + "doc/tutorials/artists.rst:424" ], "matplotlib.axes.Axes.patches": [ - "doc/tutorials/artists.rst:465" + "doc/tutorials/artists.rst:462" ], "matplotlib.axes.Axes.transAxes": [ "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredDirectionArrows:8" @@ -51,25 +51,22 @@ "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredSizeBar:8" ], "matplotlib.axes.Axes.xaxis": [ - "doc/tutorials/artists.rst:611", + "doc/tutorials/artists.rst:608", "doc/users/explain/axes/axes_intro.rst:133" ], "matplotlib.axes.Axes.yaxis": [ - "doc/tutorials/artists.rst:611", + "doc/tutorials/artists.rst:608", "doc/users/explain/axes/axes_intro.rst:133" ], "matplotlib.axis.Axis.label": [ - "doc/tutorials/artists.rst:658" - ], - "matplotlib.colors.Colormap.name": [ - "lib/matplotlib/cm.py:docstring of matplotlib.cm.register_cmap:14" + "doc/tutorials/artists.rst:655" ], "matplotlib.figure.Figure.patch": [ - "doc/tutorials/artists.rst:188", - "doc/tutorials/artists.rst:321" + "doc/tutorials/artists.rst:185", + "doc/tutorials/artists.rst:318" ], "matplotlib.figure.Figure.transFigure": [ - "doc/tutorials/artists.rst:370" + "doc/tutorials/artists.rst:367" ], "max": [ "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Bbox.p1:4" @@ -105,7 +102,7 @@ "lib/matplotlib/tri/_trirefine.py:docstring of matplotlib.tri._trirefine.UniformTriRefiner.refine_triangulation:2" ], "use_sticky_edges": [ - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.margins:53" + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.margins:57" ], "width": [ "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Bbox.bounds:2" @@ -125,19 +122,19 @@ }, "py:class": { "HashableList[_HT]": [ - "doc/docstring of builtins.list:17" + ":1" ], "matplotlib.axes._base._AxesBase": [ "doc/api/artist_api.rst:202" ], "matplotlib.backend_bases.FigureCanvas": [ - "doc/tutorials/artists.rst:36", - "doc/tutorials/artists.rst:38", - "doc/tutorials/artists.rst:43" + "doc/tutorials/artists.rst:33", + "doc/tutorials/artists.rst:35", + "doc/tutorials/artists.rst:40" ], "matplotlib.backend_bases.Renderer": [ - "doc/tutorials/artists.rst:38", - "doc/tutorials/artists.rst:43" + "doc/tutorials/artists.rst:35", + "doc/tutorials/artists.rst:40" ], "matplotlib.backend_bases._Backend": [ "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ShowBase:1" @@ -238,36 +235,41 @@ "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.Scaled:1" ], "mpl_toolkits.axes_grid1.parasite_axes.AxesHostAxes": [ - "doc/api/_as_gen/mpl_toolkits.axes_grid1.parasite_axes.rst:32::1" + ":1", + "doc/api/_as_gen/mpl_toolkits.axes_grid1.parasite_axes.rst:30::1" ], "mpl_toolkits.axes_grid1.parasite_axes.AxesParasite": [ - "doc/api/_as_gen/mpl_toolkits.axes_grid1.parasite_axes.rst:32::1" + ":1", + "doc/api/_as_gen/mpl_toolkits.axes_grid1.parasite_axes.rst:30::1" ], "mpl_toolkits.axisartist.Axes": [ "doc/api/toolkits/axisartist.rst:6" ], "mpl_toolkits.axisartist.axisline_style.AxislineStyle._Base": [ - "lib/mpl_toolkits/axisartist/axisline_style.py:docstring of mpl_toolkits.axisartist.axisline_style.AxislineStyle:1" + "lib/mpl_toolkits/axisartist/axisline_style.py:docstring of mpl_toolkits.axisartist.axisline_style.AxislineStyle.SimpleArrow:1" ], "mpl_toolkits.axisartist.axisline_style._FancyAxislineStyle.FilledArrow": [ - "lib/mpl_toolkits/axisartist/axisline_style.py:docstring of mpl_toolkits.axisartist.axisline_style.AxislineStyle:1" + ":1" ], "mpl_toolkits.axisartist.axisline_style._FancyAxislineStyle.SimpleArrow": [ - "lib/mpl_toolkits/axisartist/axisline_style.py:docstring of mpl_toolkits.axisartist.axisline_style.AxislineStyle:1" + ":1" ], "mpl_toolkits.axisartist.axislines._FixedAxisArtistHelperBase": [ + ":1", "lib/mpl_toolkits/axisartist/axislines.py:docstring of mpl_toolkits.axisartist.axislines.FixedAxisArtistHelperRectilinear:1", "lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py:docstring of mpl_toolkits.axisartist.grid_helper_curvelinear.FixedAxisArtistHelper:1" ], "mpl_toolkits.axisartist.axislines._FloatingAxisArtistHelperBase": [ + ":1", "lib/mpl_toolkits/axisartist/axislines.py:docstring of mpl_toolkits.axisartist.axislines.FloatingAxisArtistHelperRectilinear:1", "lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py:docstring of mpl_toolkits.axisartist.grid_helper_curvelinear.FloatingAxisArtistHelper:1" ], "mpl_toolkits.axisartist.floating_axes.FloatingAxesHostAxes": [ - "doc/api/_as_gen/mpl_toolkits.axisartist.floating_axes.rst:34::1" + ":1", + "doc/api/_as_gen/mpl_toolkits.axisartist.floating_axes.rst:32::1" ], "numpy.uint8": [ - "lib/matplotlib/path.py:docstring of matplotlib.path:1" + ":1" ] }, "py:data": { @@ -297,40 +299,39 @@ "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:9" ], "matplotlib.collections._CollectionWithSizes.set_sizes": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.barbs:176", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.barbs:177", "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.broken_barh:82", "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_between:118", "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_betweenx:118", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.hexbin:206", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolor:178", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.quiver:212", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.hexbin:211", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolor:180", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.quiver:213", "lib/matplotlib/collections.py:docstring of matplotlib.artist.AsteriskPolygonCollection.set:44", - "lib/matplotlib/collections.py:docstring of matplotlib.artist.BrokenBarHCollection.set:44", "lib/matplotlib/collections.py:docstring of matplotlib.artist.CircleCollection.set:44", "lib/matplotlib/collections.py:docstring of matplotlib.artist.PathCollection.set:44", "lib/matplotlib/collections.py:docstring of matplotlib.artist.PolyCollection.set:44", "lib/matplotlib/collections.py:docstring of matplotlib.artist.PolyQuadMesh.set:44", "lib/matplotlib/collections.py:docstring of matplotlib.artist.RegularPolyCollection.set:44", "lib/matplotlib/collections.py:docstring of matplotlib.artist.StarPolygonCollection.set:44", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.barbs:176", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.barbs:177", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.broken_barh:82", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_between:118", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_betweenx:118", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.hexbin:206", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolor:178", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.quiver:212", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.hexbin:211", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolor:180", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.quiver:213", "lib/matplotlib/quiver.py:docstring of matplotlib.artist.Barbs.set:45", "lib/matplotlib/quiver.py:docstring of matplotlib.artist.Quiver.set:45", - "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:209", - "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Quiver:248", + "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:210", + "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Quiver:249", "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Path3DCollection.set:46", "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Poly3DCollection.set:44" ], "matplotlib.collections._MeshData.set_array": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolormesh:160", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolormesh:162", "lib/matplotlib/collections.py:docstring of matplotlib.artist.PolyQuadMesh.set:17", "lib/matplotlib/collections.py:docstring of matplotlib.artist.QuadMesh.set:17", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolormesh:160" + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolormesh:162" ] }, "py:obj": { @@ -349,7 +350,7 @@ "doc/users/explain/figure/interactive_guide.rst:333" ], "Glyph": [ - "doc/gallery/misc/ftface_props.rst:28" + "doc/gallery/misc/ftface_props.rst:25" ], "Image": [ "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.gci:4" @@ -358,24 +359,24 @@ "doc/users/explain/figure/event_handling.rst:568" ], "QuadContourSet.changed()": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contour:152", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contourf:152", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contour:152", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contourf:152" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contour:154", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contourf:154", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contour:154", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contourf:154" ], "Rectangle.contains": [ "doc/users/explain/figure/event_handling.rst:280" ], "Size.from_any": [ - "lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid:84", - "lib/mpl_toolkits/axisartist/axes_grid.py:docstring of mpl_toolkits.axisartist.axes_grid.ImageGrid:84" + "lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid:87", + "lib/mpl_toolkits/axisartist/axes_grid.py:docstring of mpl_toolkits.axisartist.axes_grid.ImageGrid:87" ], "Timer": [ "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.FigureCanvasBase.new_timer:2", "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.TimerBase:14" ], "ToolContainer": [ - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase.remove_toolitem:2", + "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase.remove_toolitem:8", "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase:20" ], "_iter_collection": [ @@ -396,7 +397,7 @@ "lib/matplotlib/dviread.py:docstring of matplotlib.dviread.Vf:20" ], "active": [ - "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.AxesWidget:34" + "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.AxesWidget:32" ], "ax.transAxes": [ "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.indicate_inset:19", @@ -435,43 +436,40 @@ "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size:1" ], "ipykernel.pylab.backend_inline": [ - "doc/users/explain/figure/interactive.rst:340" + "doc/users/explain/figure/interactive.rst:361" ], "kde.covariance_factor": [ - "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:41" + "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:40" ], "kde.factor": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.violinplot:46", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.violinplot:58", "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:12", - "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:45", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.violinplot:46" + "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:44", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.violinplot:58" ], "make_image": [ "lib/matplotlib/image.py:docstring of matplotlib.image.composite_images:9" ], "matplotlib.animation.ArtistAnimation.new_frame_seq": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" ], "matplotlib.animation.ArtistAnimation.new_saved_frame_seq": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" ], "matplotlib.animation.ArtistAnimation.pause": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" - ], - "matplotlib.animation.ArtistAnimation.repeat": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:33::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" ], "matplotlib.animation.ArtistAnimation.resume": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" ], "matplotlib.animation.ArtistAnimation.save": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" ], "matplotlib.animation.ArtistAnimation.to_html5_video": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" ], "matplotlib.animation.ArtistAnimation.to_jshtml": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" ], "matplotlib.animation.FFMpegFileWriter.bin_path": [ "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:27::1" @@ -543,22 +541,19 @@ "lib/matplotlib/animation.py:docstring of matplotlib.animation.FileMovieWriter.finish:1::1" ], "matplotlib.animation.FuncAnimation.pause": [ - "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" - ], - "matplotlib.animation.FuncAnimation.repeat": [ "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" ], "matplotlib.animation.FuncAnimation.resume": [ - "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" ], "matplotlib.animation.FuncAnimation.save": [ - "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" ], "matplotlib.animation.FuncAnimation.to_html5_video": [ - "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" ], "matplotlib.animation.FuncAnimation.to_jshtml": [ - "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" ], "matplotlib.animation.HTMLWriter.bin_path": [ "doc/api/_as_gen/matplotlib.animation.HTMLWriter.rst:27::1" @@ -633,28 +628,28 @@ "doc/api/_as_gen/matplotlib.animation.PillowWriter.rst:26::1" ], "matplotlib.animation.TimedAnimation.new_frame_seq": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" ], "matplotlib.animation.TimedAnimation.new_saved_frame_seq": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" ], "matplotlib.animation.TimedAnimation.pause": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" ], "matplotlib.animation.TimedAnimation.resume": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" ], "matplotlib.animation.TimedAnimation.save": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" ], "matplotlib.animation.TimedAnimation.to_html5_video": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" ], "matplotlib.animation.TimedAnimation.to_jshtml": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" ], "matplotlib.typing._HT": [ - "doc/docstring of builtins.list:17" + ":1" ], "mpl_toolkits.axislines.Axes": [ "lib/mpl_toolkits/axisartist/axis_artist.py:docstring of mpl_toolkits.axisartist.axis_artist:7" diff --git a/doc/sphinxext/missing_references.py b/doc/sphinxext/missing_references.py index c621adb2c945..9c3b8256cd91 100644 --- a/doc/sphinxext/missing_references.py +++ b/doc/sphinxext/missing_references.py @@ -17,11 +17,9 @@ from collections import defaultdict import json -import logging from pathlib import Path from docutils.utils import get_source_line -from docutils import nodes from sphinx.util import logging as sphinx_logging import matplotlib @@ -29,59 +27,6 @@ logger = sphinx_logging.getLogger(__name__) -class MissingReferenceFilter(logging.Filter): - """ - A logging filter designed to record missing reference warning messages - for use by this extension - """ - def __init__(self, app): - self.app = app - super().__init__() - - def _record_reference(self, record): - if not (getattr(record, 'type', '') == 'ref' and - isinstance(getattr(record, 'location', None), nodes.Node)): - return - - if not hasattr(self.app.env, "missing_references_warnings"): - self.app.env.missing_references_warnings = defaultdict(set) - - record_missing_reference(self.app, - self.app.env.missing_references_warnings, - record.location) - - def filter(self, record): - self._record_reference(record) - return True - - -def record_missing_reference(app, record, node): - domain = node["refdomain"] - typ = node["reftype"] - target = node["reftarget"] - location = get_location(node, app) - - domain_type = f"{domain}:{typ}" - - record[(domain_type, target)].add(location) - - -def record_missing_reference_handler(app, env, node, contnode): - """ - When the sphinx app notices a missing reference, it emits an - event which calls this function. This function records the missing - references for analysis at the end of the sphinx build. - """ - if not app.config.missing_references_enabled: - # no-op when we are disabled. - return - - if not hasattr(env, "missing_references_events"): - env.missing_references_events = defaultdict(set) - - record_missing_reference(app, env.missing_references_events, node) - - def get_location(node, app): """ Given a docutils node and a sphinx application, return a string @@ -146,10 +91,34 @@ def _truncate_location(location): return location.split(":", 1)[0] -def _warn_unused_missing_references(app): - if not app.config.missing_references_warn_unused_ignores: - return +def handle_missing_reference(app, domain, node): + """ + Handle the warn-missing-reference Sphinx event. + + This function will: + #. record missing references for saving/comparing with ignored list. + #. prevent Sphinx from raising a warning on ignored references. + """ + typ = node["reftype"] + target = node["reftarget"] + location = get_location(node, app) + domain_type = f"{domain.name}:{typ}" + + app.env.missing_references_events[(domain_type, target)].add(location) + + # If we're ignoring this event, return True so that Sphinx thinks we handled it, + # even though we didn't print or warn. If we aren't ignoring it, Sphinx will print a + # warning about the missing reference. + if location in app.env.missing_references_ignored_references.get( + (domain_type, target), []): + return True + + +def warn_unused_missing_references(app, exc): + """ + Check that all lines of the existing JSON file are still necessary. + """ # We can only warn if we are building from a source install # otherwise, we just have to skip this step. basepath = Path(matplotlib.__file__).parent.parent.parent.resolve() @@ -159,9 +128,8 @@ def _warn_unused_missing_references(app): return # This is a dictionary of {(domain_type, target): locations} - references_ignored = getattr( - app.env, 'missing_references_ignored_references', {}) - references_events = getattr(app.env, 'missing_references_events', {}) + references_ignored = app.env.missing_references_ignored_references + references_events = app.env.missing_references_events # Warn about any reference which is no longer missing. for (domain_type, target), locations in references_ignored.items(): @@ -184,26 +152,13 @@ def _warn_unused_missing_references(app): subtype=domain_type) -def save_missing_references_handler(app, exc): +def save_missing_references(app, exc): """ - At the end of the sphinx build, check that all lines of the existing JSON - file are still necessary. - - If the configuration value ``missing_references_write_json`` is set - then write a new JSON file containing missing references. + Write a new JSON file containing missing references. """ - if not app.config.missing_references_enabled: - # no-op when we are disabled. - return - - _warn_unused_missing_references(app) - json_path = Path(app.confdir) / app.config.missing_references_filename - - references_warnings = getattr(app.env, 'missing_references_warnings', {}) - - if app.config.missing_references_write_json: - _write_missing_references_json(references_warnings, json_path) + references_warnings = app.env.missing_references_events + _write_missing_references_json(references_warnings, json_path) def _write_missing_references_json(records, json_path): @@ -220,6 +175,7 @@ def _write_missing_references_json(records, json_path): transformed_records[domain_type][target] = sorted(paths) with json_path.open("w") as stream: json.dump(transformed_records, stream, sort_keys=True, indent=2) + stream.write("\n") # Silence pre-commit no-newline-at-end-of-file warning. def _read_missing_references_json(json_path): @@ -242,49 +198,25 @@ def _read_missing_references_json(json_path): return ignored_references -def prepare_missing_references_handler(app): +def prepare_missing_references_setup(app): """ - Handler called to initialize this extension once the configuration - is ready. - - Reads the missing references file and populates ``nitpick_ignore`` if - appropriate. + Initialize this extension once the configuration is ready. """ if not app.config.missing_references_enabled: # no-op when we are disabled. return - sphinx_logger = logging.getLogger('sphinx') - missing_reference_filter = MissingReferenceFilter(app) - for handler in sphinx_logger.handlers: - if (isinstance(handler, sphinx_logging.WarningStreamHandler) - and missing_reference_filter not in handler.filters): - - # This *must* be the first filter, because subsequent filters - # throw away the node information and then we can't identify - # the reference uniquely. - handler.filters.insert(0, missing_reference_filter) - - app.env.missing_references_ignored_references = {} + app.connect("warn-missing-reference", handle_missing_reference) + if app.config.missing_references_warn_unused_ignores: + app.connect("build-finished", warn_unused_missing_references) + if app.config.missing_references_write_json: + app.connect("build-finished", save_missing_references) json_path = Path(app.confdir) / app.config.missing_references_filename - if not json_path.exists(): - return - - ignored_references = _read_missing_references_json(json_path) - - app.env.missing_references_ignored_references = ignored_references - - # If we are going to re-write the JSON file, then don't suppress missing - # reference warnings. We want to record a full list of missing references - # for use later. Otherwise, add all known missing references to - # ``nitpick_ignore``` - if not app.config.missing_references_write_json: - # Since Sphinx v6.2, nitpick_ignore may be a list, set or tuple, and - # defaults to set. Previously it was always a list. Cast to list for - # consistency across versions. - app.config.nitpick_ignore = list(app.config.nitpick_ignore) - app.config.nitpick_ignore.extend(ignored_references.keys()) + app.env.missing_references_ignored_references = ( + _read_missing_references_json(json_path) if json_path.exists() else {} + ) + app.env.missing_references_events = defaultdict(set) def setup(app): @@ -294,8 +226,6 @@ def setup(app): app.add_config_value("missing_references_filename", "missing-references.json", "env") - app.connect("builder-inited", prepare_missing_references_handler) - app.connect("missing-reference", record_missing_reference_handler) - app.connect("build-finished", save_missing_references_handler) + app.connect("builder-inited", prepare_missing_references_setup) return {'parallel_read_safe': True} From 8e9c478283575abaa4647f355f02e64205baa639 Mon Sep 17 00:00:00 2001 From: Alan Date: Wed, 31 Jul 2024 13:27:15 +0900 Subject: [PATCH 0091/1230] Added documentation for parameters vmin and vmax inside specgram function. (#28613) Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/axes/_axes.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index b42d5267012e..2c9cc8cc1e9a 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -8136,6 +8136,11 @@ def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, data : indexable object, optional DATA_PARAMETER_PLACEHOLDER + vmin, vmax : float, optional + vmin and vmax define the data range that the colormap covers. + By default, the colormap covers the complete value range of the + data. + **kwargs Additional keyword arguments are passed on to `~.axes.Axes.imshow` which makes the specgram image. The origin keyword argument From f7b3def52b39a070064e0e713042cd948c985643 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 31 Jul 2024 06:34:27 +0200 Subject: [PATCH 0092/1230] DOC: Clarify/simplify example of multiple images with one colorbar (#28546) Co-authored-by: hannah --- .../images_contours_and_fields/multi_image.py | 88 +++++++++++-------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/galleries/examples/images_contours_and_fields/multi_image.py b/galleries/examples/images_contours_and_fields/multi_image.py index 5634a32abeb9..8be048055dec 100644 --- a/galleries/examples/images_contours_and_fields/multi_image.py +++ b/galleries/examples/images_contours_and_fields/multi_image.py @@ -1,9 +1,19 @@ """ -=============== -Multiple images -=============== +================================= +Multiple images with one colorbar +================================= -Make a set of images with a single colormap, norm, and colorbar. +Use a single colorbar for multiple images. + +Currently, a colorbar can only be connected to one image. The connection +guarantees that the data coloring is consistent with the colormap scale +(i.e. the color of value *x* in the colormap is used for coloring a data +value *x* in the image). + +If we want one colorbar to be representative for multiple images, we have +to explicitly ensure consistent data coloring by using the same data +normalization for all the images. We ensure this by explicitly creating a +``norm`` object that we pass to all the image plotting methods. """ import matplotlib.pyplot as plt @@ -12,47 +22,53 @@ from matplotlib import colors np.random.seed(19680801) -Nr = 3 -Nc = 2 -fig, axs = plt.subplots(Nr, Nc) +datasets = [ + (i+1)/10 * np.random.rand(10, 20) + for i in range(4) +] + +fig, axs = plt.subplots(2, 2) fig.suptitle('Multiple images') -images = [] -for i in range(Nr): - for j in range(Nc): - # Generate data with a range that varies from one plot to the next. - data = ((1 + i + j) / 10) * np.random.rand(10, 20) - images.append(axs[i, j].imshow(data)) - axs[i, j].label_outer() +# create a single norm to be shared across all images +norm = colors.Normalize(vmin=np.min(datasets), vmax=np.max(datasets)) -# Find the min and max of all colors for use in setting the color scale. -vmin = min(image.get_array().min() for image in images) -vmax = max(image.get_array().max() for image in images) -norm = colors.Normalize(vmin=vmin, vmax=vmax) -for im in images: - im.set_norm(norm) +images = [] +for ax, data in zip(axs.flat, datasets): + images.append(ax.imshow(data, norm=norm)) fig.colorbar(images[0], ax=axs, orientation='horizontal', fraction=.1) - -# Make images respond to changes in the norm of other images (e.g. via the -# "edit axis, curves and images parameters" GUI on Qt), but be careful not to -# recurse infinitely! -def update(changed_image): - for im in images: - if (changed_image.get_cmap() != im.get_cmap() - or changed_image.get_clim() != im.get_clim()): - im.set_cmap(changed_image.get_cmap()) - im.set_clim(changed_image.get_clim()) - - -for im in images: - im.callbacks.connect('changed', update) - plt.show() # %% +# The colors are now kept consistent across all images when changing the +# scaling, e.g. through zooming in the colorbar or via the "edit axis, +# curves and images parameters" GUI of the Qt backend. This is sufficient +# for most practical use cases. +# +# Advanced: Additionally sync the colormap +# ---------------------------------------- +# +# Sharing a common norm object guarantees synchronized scaling because scale +# changes modify the norm object in-place and thus propagate to all images +# that use this norm. This approach does not help with synchronizing colormaps +# because changing the colormap of an image (e.g. through the "edit axis, +# curves and images parameters" GUI of the Qt backend) results in the image +# referencing the new colormap object. Thus, the other images are not updated. +# +# To update the other images, sync the +# colormaps using the following code:: +# +# def sync_cmaps(changed_image): +# for im in images: +# if changed_image.get_cmap() != im.get_cmap(): +# im.set_cmap(changed_image.get_cmap()) +# +# for im in images: +# im.callbacks.connect('changed', sync_cmaps) +# # # .. admonition:: References # @@ -63,6 +79,4 @@ def update(changed_image): # - `matplotlib.figure.Figure.colorbar` / `matplotlib.pyplot.colorbar` # - `matplotlib.colors.Normalize` # - `matplotlib.cm.ScalarMappable.set_cmap` -# - `matplotlib.cm.ScalarMappable.set_norm` -# - `matplotlib.cm.ScalarMappable.set_clim` # - `matplotlib.cbook.CallbackRegistry.connect` From cf441b945f96659f565ec42fc2303ca7541eaa79 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Thu, 1 Aug 2024 14:28:21 +0200 Subject: [PATCH 0093/1230] use Axes instead of Self --- lib/matplotlib/axes/_base.pyi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/axes/_base.pyi b/lib/matplotlib/axes/_base.pyi index 4903bb41645e..362d644d11f2 100644 --- a/lib/matplotlib/axes/_base.pyi +++ b/lib/matplotlib/axes/_base.pyi @@ -4,6 +4,7 @@ import datetime from collections.abc import Callable, Iterable, Iterator, Sequence from matplotlib import cbook from matplotlib.artist import Artist +from matplotlib.axes import Axes from matplotlib.axis import XAxis, YAxis, Tick from matplotlib.backend_bases import RendererBase, MouseButton, MouseEvent from matplotlib.cbook import CallbackRegistry @@ -27,7 +28,6 @@ from cycler import Cycler import numpy as np from numpy.typing import ArrayLike from typing import Any, Literal, TypeVar, overload -from typing_extensions import Self from matplotlib.typing import ColorType _T = TypeVar("_T", bound=Artist) @@ -385,8 +385,8 @@ class _AxesBase(martist.Artist): bbox_extra_artists: Sequence[Artist] | None = ..., for_layout_only: bool = ... ) -> Bbox | None: ... - def twinx(self) -> Self: ... - def twiny(self) -> Self: ... + def twinx(self) -> Axes: ... + def twiny(self) -> Axes: ... def get_shared_x_axes(self) -> cbook.GrouperView: ... def get_shared_y_axes(self) -> cbook.GrouperView: ... def label_outer(self, remove_inner_ticks: bool = ...) -> None: ... From a19303b0901915d49122c7c67da483a78a8fb9a0 Mon Sep 17 00:00:00 2001 From: Sean Smith Date: Thu, 1 Aug 2024 07:40:53 -0700 Subject: [PATCH 0094/1230] Closed open div tag in color.ColorMap._repr_html_ --- lib/matplotlib/colors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 177557b371a6..5f40e7b0fb9a 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -960,6 +960,7 @@ def color_block(color): '' '

' f'over {color_block(self.get_over())}' + '
' '') def copy(self): From 5c4c028dcca38eef023c46d392e66fe4271d27f3 Mon Sep 17 00:00:00 2001 From: Martino Sorbaro Date: Wed, 17 Jul 2024 15:10:43 +0200 Subject: [PATCH 0095/1230] added dark-mode diverging colormaps --- .../next_whats_new/diverging_colormaps.rst | 24 + .../examples/color/colormap_reference.py | 3 +- galleries/users_explain/colors/colormaps.py | 8 +- lib/matplotlib/_cm_listed.py | 776 ++++++++++++++++++ 4 files changed, 809 insertions(+), 2 deletions(-) create mode 100644 doc/users/next_whats_new/diverging_colormaps.rst diff --git a/doc/users/next_whats_new/diverging_colormaps.rst b/doc/users/next_whats_new/diverging_colormaps.rst new file mode 100644 index 000000000000..8137acbf13d2 --- /dev/null +++ b/doc/users/next_whats_new/diverging_colormaps.rst @@ -0,0 +1,24 @@ +Dark-mode diverging colormaps +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Three diverging colormaps have been added: "berlin", "managua", and "vanimo". +They are dark-mode diverging colormaps, with minimum lightness at the center, +and maximum at the extremes. These are taken from F. Crameri's Scientific +colour maps version 8.0.1 (DOI: https://doi.org/10.5281/zenodo.1243862). + + +.. plot:: + :include-source: true + :alt: Example figures using "imshow" with dark-mode diverging colormaps on positive and negative data. First panel: "berlin" (blue to red with a black center); second panel: "managua" (orange to cyan with a dark purple center); third panel: "vanimo" (pink to green with a black center). + + import numpy as np + import matplotlib.pyplot as plt + + vals = np.linspace(-5, 5, 100) + x, y = np.meshgrid(vals, vals) + img = np.sin(x*y) + + _, ax = plt.subplots(1, 3) + ax[0].imshow(img, cmap=plt.cm.berlin) + ax[1].imshow(img, cmap=plt.cm.managua) + ax[2].imshow(img, cmap=plt.cm.vanimo) diff --git a/galleries/examples/color/colormap_reference.py b/galleries/examples/color/colormap_reference.py index ee01d7432b37..38e91ad25408 100644 --- a/galleries/examples/color/colormap_reference.py +++ b/galleries/examples/color/colormap_reference.py @@ -29,7 +29,8 @@ 'hot', 'afmhot', 'gist_heat', 'copper']), ('Diverging', [ 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', - 'RdYlBu', 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic']), + 'RdYlBu', 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic', + 'berlin', 'managua', 'vanimo']), ('Cyclic', ['twilight', 'twilight_shifted', 'hsv']), ('Qualitative', [ 'Pastel1', 'Pastel2', 'Paired', 'Accent', diff --git a/galleries/users_explain/colors/colormaps.py b/galleries/users_explain/colors/colormaps.py index 92b56d298976..ff146cacf170 100644 --- a/galleries/users_explain/colors/colormaps.py +++ b/galleries/users_explain/colors/colormaps.py @@ -175,10 +175,15 @@ def plot_color_gradients(category, cmap_list): # equal minimum :math:`L^*` values at opposite ends of the colormap. By these # measures, BrBG and RdBu are good options. coolwarm is a good option, but it # doesn't span a wide range of :math:`L^*` values (see grayscale section below). +# +# Berlin, Managua, and Vanimo are dark-mode diverging colormaps, with minimum +# lightness at the center, and maximum at the extremes. These are taken from +# F. Crameri's [scientific colour maps]_ version 8.0.1. plot_color_gradients('Diverging', ['PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', 'RdYlBu', - 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic']) + 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic', + 'berlin', 'managua', 'vanimo']) # %% # Cyclic @@ -441,3 +446,4 @@ def plot_color_gradients(cmap_category, cmap_list): # .. [colorblindness] http://www.color-blindness.com/ # .. [IBM] https://doi.org/10.1109/VISUAL.1995.480803 # .. [turbo] https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html +# .. [scientific colour maps] https://doi.org/10.5281/zenodo.1243862 diff --git a/lib/matplotlib/_cm_listed.py b/lib/matplotlib/_cm_listed.py index a331ad74a5f0..b90e0a23acb0 100644 --- a/lib/matplotlib/_cm_listed.py +++ b/lib/matplotlib/_cm_listed.py @@ -2057,6 +2057,779 @@ [0.49321, 0.01963, 0.00955], [0.47960, 0.01583, 0.01055]] +_berlin_data = [ + [0.62108, 0.69018, 0.99951], + [0.61216, 0.68923, 0.99537], + [0.6032, 0.68825, 0.99124], + [0.5942, 0.68726, 0.98709], + [0.58517, 0.68625, 0.98292], + [0.57609, 0.68522, 0.97873], + [0.56696, 0.68417, 0.97452], + [0.55779, 0.6831, 0.97029], + [0.54859, 0.68199, 0.96602], + [0.53933, 0.68086, 0.9617], + [0.53003, 0.67969, 0.95735], + [0.52069, 0.67848, 0.95294], + [0.51129, 0.67723, 0.94847], + [0.50186, 0.67591, 0.94392], + [0.49237, 0.67453, 0.9393], + [0.48283, 0.67308, 0.93457], + [0.47324, 0.67153, 0.92975], + [0.46361, 0.6699, 0.92481], + [0.45393, 0.66815, 0.91974], + [0.44421, 0.66628, 0.91452], + [0.43444, 0.66427, 0.90914], + [0.42465, 0.66212, 0.90359], + [0.41482, 0.65979, 0.89785], + [0.40498, 0.65729, 0.89191], + [0.39514, 0.65458, 0.88575], + [0.3853, 0.65167, 0.87937], + [0.37549, 0.64854, 0.87276], + [0.36574, 0.64516, 0.8659], + [0.35606, 0.64155, 0.8588], + [0.34645, 0.63769, 0.85145], + [0.33698, 0.63357, 0.84386], + [0.32764, 0.62919, 0.83602], + [0.31849, 0.62455, 0.82794], + [0.30954, 0.61966, 0.81963], + [0.30078, 0.6145, 0.81111], + [0.29231, 0.60911, 0.80238], + [0.2841, 0.60348, 0.79347], + [0.27621, 0.59763, 0.78439], + [0.26859, 0.59158, 0.77514], + [0.26131, 0.58534, 0.76578], + [0.25437, 0.57891, 0.7563], + [0.24775, 0.57233, 0.74672], + [0.24146, 0.5656, 0.73707], + [0.23552, 0.55875, 0.72735], + [0.22984, 0.5518, 0.7176], + [0.2245, 0.54475, 0.7078], + [0.21948, 0.53763, 0.698], + [0.21469, 0.53043, 0.68819], + [0.21017, 0.52319, 0.67838], + [0.20589, 0.5159, 0.66858], + [0.20177, 0.5086, 0.65879], + [0.19788, 0.50126, 0.64903], + [0.19417, 0.4939, 0.63929], + [0.19056, 0.48654, 0.62957], + [0.18711, 0.47918, 0.6199], + [0.18375, 0.47183, 0.61024], + [0.1805, 0.46447, 0.60062], + [0.17737, 0.45712, 0.59104], + [0.17426, 0.44979, 0.58148], + [0.17122, 0.44247, 0.57197], + [0.16824, 0.43517, 0.56249], + [0.16529, 0.42788, 0.55302], + [0.16244, 0.42061, 0.5436], + [0.15954, 0.41337, 0.53421], + [0.15674, 0.40615, 0.52486], + [0.15391, 0.39893, 0.51552], + [0.15112, 0.39176, 0.50623], + [0.14835, 0.38459, 0.49697], + [0.14564, 0.37746, 0.48775], + [0.14288, 0.37034, 0.47854], + [0.14014, 0.36326, 0.46939], + [0.13747, 0.3562, 0.46024], + [0.13478, 0.34916, 0.45115], + [0.13208, 0.34215, 0.44209], + [0.1294, 0.33517, 0.43304], + [0.12674, 0.3282, 0.42404], + [0.12409, 0.32126, 0.41507], + [0.12146, 0.31435, 0.40614], + [0.1189, 0.30746, 0.39723], + [0.11632, 0.30061, 0.38838], + [0.11373, 0.29378, 0.37955], + [0.11119, 0.28698, 0.37075], + [0.10861, 0.28022, 0.362], + [0.10616, 0.2735, 0.35328], + [0.10367, 0.26678, 0.34459], + [0.10118, 0.26011, 0.33595], + [0.098776, 0.25347, 0.32734], + [0.096347, 0.24685, 0.31878], + [0.094059, 0.24026, 0.31027], + [0.091788, 0.23373, 0.30176], + [0.089506, 0.22725, 0.29332], + [0.087341, 0.2208, 0.28491], + [0.085142, 0.21436, 0.27658], + [0.083069, 0.20798, 0.26825], + [0.081098, 0.20163, 0.25999], + [0.07913, 0.19536, 0.25178], + [0.077286, 0.18914, 0.24359], + [0.075571, 0.18294, 0.2355], + [0.073993, 0.17683, 0.22743], + [0.07241, 0.17079, 0.21943], + [0.071045, 0.1648, 0.2115], + [0.069767, 0.1589, 0.20363], + [0.068618, 0.15304, 0.19582], + [0.06756, 0.14732, 0.18812], + [0.066665, 0.14167, 0.18045], + [0.065923, 0.13608, 0.17292], + [0.065339, 0.1307, 0.16546], + [0.064911, 0.12535, 0.15817], + [0.064636, 0.12013, 0.15095], + [0.064517, 0.11507, 0.14389], + [0.064554, 0.11022, 0.13696], + [0.064749, 0.10543, 0.13023], + [0.0651, 0.10085, 0.12357], + [0.065383, 0.096469, 0.11717], + [0.065574, 0.092338, 0.11101], + [0.065892, 0.088201, 0.10498], + [0.066388, 0.084134, 0.099288], + [0.067108, 0.080051, 0.093829], + [0.068193, 0.076099, 0.08847], + [0.06972, 0.072283, 0.083025], + [0.071639, 0.068654, 0.077544], + [0.073978, 0.065058, 0.07211], + [0.076596, 0.061657, 0.066651], + [0.079637, 0.05855, 0.061133], + [0.082963, 0.055666, 0.055745], + [0.086537, 0.052997, 0.050336], + [0.090315, 0.050699, 0.04504], + [0.09426, 0.048753, 0.039773], + [0.098319, 0.047041, 0.034683], + [0.10246, 0.045624, 0.030074], + [0.10673, 0.044705, 0.026012], + [0.11099, 0.043972, 0.022379], + [0.11524, 0.043596, 0.01915], + [0.11955, 0.043567, 0.016299], + [0.12381, 0.043861, 0.013797], + [0.1281, 0.044459, 0.011588], + [0.13232, 0.045229, 0.0095315], + [0.13645, 0.046164, 0.0078947], + [0.14063, 0.047374, 0.006502], + [0.14488, 0.048634, 0.0053266], + [0.14923, 0.049836, 0.0043455], + [0.15369, 0.050997, 0.0035374], + [0.15831, 0.05213, 0.0028824], + [0.16301, 0.053218, 0.0023628], + [0.16781, 0.05424, 0.0019629], + [0.17274, 0.055172, 0.001669], + [0.1778, 0.056018, 0.0014692], + [0.18286, 0.05682, 0.0013401], + [0.18806, 0.057574, 0.0012617], + [0.19323, 0.058514, 0.0012261], + [0.19846, 0.05955, 0.0012271], + [0.20378, 0.060501, 0.0012601], + [0.20909, 0.061486, 0.0013221], + [0.21447, 0.06271, 0.0014116], + [0.2199, 0.063823, 0.0015287], + [0.22535, 0.065027, 0.0016748], + [0.23086, 0.066297, 0.0018529], + [0.23642, 0.067645, 0.0020675], + [0.24202, 0.069092, 0.0023247], + [0.24768, 0.070458, 0.0026319], + [0.25339, 0.071986, 0.0029984], + [0.25918, 0.07364, 0.003435], + [0.265, 0.075237, 0.0039545], + [0.27093, 0.076965, 0.004571], + [0.27693, 0.078822, 0.0053006], + [0.28302, 0.080819, 0.0061608], + [0.2892, 0.082879, 0.0071713], + [0.29547, 0.085075, 0.0083494], + [0.30186, 0.08746, 0.0097258], + [0.30839, 0.089912, 0.011455], + [0.31502, 0.09253, 0.013324], + [0.32181, 0.095392, 0.015413], + [0.32874, 0.098396, 0.01778], + [0.3358, 0.10158, 0.020449], + [0.34304, 0.10498, 0.02344], + [0.35041, 0.10864, 0.026771], + [0.35795, 0.11256, 0.030456], + [0.36563, 0.11666, 0.034571], + [0.37347, 0.12097, 0.039115], + [0.38146, 0.12561, 0.043693], + [0.38958, 0.13046, 0.048471], + [0.39785, 0.13547, 0.053136], + [0.40622, 0.1408, 0.057848], + [0.41469, 0.14627, 0.062715], + [0.42323, 0.15198, 0.067685], + [0.43184, 0.15791, 0.073044], + [0.44044, 0.16403, 0.07862], + [0.44909, 0.17027, 0.084644], + [0.4577, 0.17667, 0.090869], + [0.46631, 0.18321, 0.097335], + [0.4749, 0.18989, 0.10406], + [0.48342, 0.19668, 0.11104], + [0.49191, 0.20352, 0.11819], + [0.50032, 0.21043, 0.1255], + [0.50869, 0.21742, 0.13298], + [0.51698, 0.22443, 0.14062], + [0.5252, 0.23154, 0.14835], + [0.53335, 0.23862, 0.15626], + [0.54144, 0.24575, 0.16423], + [0.54948, 0.25292, 0.17226], + [0.55746, 0.26009, 0.1804], + [0.56538, 0.26726, 0.18864], + [0.57327, 0.27446, 0.19692], + [0.58111, 0.28167, 0.20524], + [0.58892, 0.28889, 0.21362], + [0.59672, 0.29611, 0.22205], + [0.60448, 0.30335, 0.23053], + [0.61223, 0.31062, 0.23905], + [0.61998, 0.31787, 0.24762], + [0.62771, 0.32513, 0.25619], + [0.63544, 0.33244, 0.26481], + [0.64317, 0.33975, 0.27349], + [0.65092, 0.34706, 0.28218], + [0.65866, 0.3544, 0.29089], + [0.66642, 0.36175, 0.29964], + [0.67419, 0.36912, 0.30842], + [0.68198, 0.37652, 0.31722], + [0.68978, 0.38392, 0.32604], + [0.6976, 0.39135, 0.33493], + [0.70543, 0.39879, 0.3438], + [0.71329, 0.40627, 0.35272], + [0.72116, 0.41376, 0.36166], + [0.72905, 0.42126, 0.37062], + [0.73697, 0.4288, 0.37962], + [0.7449, 0.43635, 0.38864], + [0.75285, 0.44392, 0.39768], + [0.76083, 0.45151, 0.40675], + [0.76882, 0.45912, 0.41584], + [0.77684, 0.46676, 0.42496], + [0.78488, 0.47441, 0.43409], + [0.79293, 0.48208, 0.44327], + [0.80101, 0.48976, 0.45246], + [0.80911, 0.49749, 0.46167], + [0.81722, 0.50521, 0.47091], + [0.82536, 0.51296, 0.48017], + [0.83352, 0.52073, 0.48945], + [0.84169, 0.52853, 0.49876], + [0.84988, 0.53634, 0.5081], + [0.85809, 0.54416, 0.51745], + [0.86632, 0.55201, 0.52683], + [0.87457, 0.55988, 0.53622], + [0.88283, 0.56776, 0.54564], + [0.89111, 0.57567, 0.55508], + [0.89941, 0.58358, 0.56455], + [0.90772, 0.59153, 0.57404], + [0.91603, 0.59949, 0.58355], + [0.92437, 0.60747, 0.59309], + [0.93271, 0.61546, 0.60265], + [0.94108, 0.62348, 0.61223], + [0.94945, 0.63151, 0.62183], + [0.95783, 0.63956, 0.63147], + [0.96622, 0.64763, 0.64111], + [0.97462, 0.65572, 0.65079], + [0.98303, 0.66382, 0.66049], + [0.99145, 0.67194, 0.67022], + [0.99987, 0.68007, 0.67995]] + +_managua_data = [ + [1, 0.81263, 0.40424], + [0.99516, 0.80455, 0.40155], + [0.99024, 0.79649, 0.39888], + [0.98532, 0.78848, 0.39622], + [0.98041, 0.7805, 0.39356], + [0.97551, 0.77257, 0.39093], + [0.97062, 0.76468, 0.3883], + [0.96573, 0.75684, 0.38568], + [0.96087, 0.74904, 0.3831], + [0.95601, 0.74129, 0.38052], + [0.95116, 0.7336, 0.37795], + [0.94631, 0.72595, 0.37539], + [0.94149, 0.71835, 0.37286], + [0.93667, 0.7108, 0.37034], + [0.93186, 0.7033, 0.36784], + [0.92706, 0.69585, 0.36536], + [0.92228, 0.68845, 0.36289], + [0.9175, 0.68109, 0.36042], + [0.91273, 0.67379, 0.358], + [0.90797, 0.66653, 0.35558], + [0.90321, 0.65932, 0.35316], + [0.89846, 0.65216, 0.35078], + [0.89372, 0.64503, 0.34839], + [0.88899, 0.63796, 0.34601], + [0.88426, 0.63093, 0.34367], + [0.87953, 0.62395, 0.34134], + [0.87481, 0.617, 0.33902], + [0.87009, 0.61009, 0.3367], + [0.86538, 0.60323, 0.33442], + [0.86067, 0.59641, 0.33213], + [0.85597, 0.58963, 0.32987], + [0.85125, 0.5829, 0.3276], + [0.84655, 0.57621, 0.32536], + [0.84185, 0.56954, 0.32315], + [0.83714, 0.56294, 0.32094], + [0.83243, 0.55635, 0.31874], + [0.82772, 0.54983, 0.31656], + [0.82301, 0.54333, 0.31438], + [0.81829, 0.53688, 0.31222], + [0.81357, 0.53046, 0.3101], + [0.80886, 0.52408, 0.30796], + [0.80413, 0.51775, 0.30587], + [0.7994, 0.51145, 0.30375], + [0.79466, 0.50519, 0.30167], + [0.78991, 0.49898, 0.29962], + [0.78516, 0.4928, 0.29757], + [0.7804, 0.48668, 0.29553], + [0.77564, 0.48058, 0.29351], + [0.77086, 0.47454, 0.29153], + [0.76608, 0.46853, 0.28954], + [0.76128, 0.46255, 0.28756], + [0.75647, 0.45663, 0.28561], + [0.75166, 0.45075, 0.28369], + [0.74682, 0.44491, 0.28178], + [0.74197, 0.4391, 0.27988], + [0.73711, 0.43333, 0.27801], + [0.73223, 0.42762, 0.27616], + [0.72732, 0.42192, 0.2743], + [0.72239, 0.41628, 0.27247], + [0.71746, 0.41067, 0.27069], + [0.71247, 0.40508, 0.26891], + [0.70747, 0.39952, 0.26712], + [0.70244, 0.39401, 0.26538], + [0.69737, 0.38852, 0.26367], + [0.69227, 0.38306, 0.26194], + [0.68712, 0.37761, 0.26025], + [0.68193, 0.37219, 0.25857], + [0.67671, 0.3668, 0.25692], + [0.67143, 0.36142, 0.25529], + [0.6661, 0.35607, 0.25367], + [0.66071, 0.35073, 0.25208], + [0.65528, 0.34539, 0.25049], + [0.6498, 0.34009, 0.24895], + [0.64425, 0.3348, 0.24742], + [0.63866, 0.32953, 0.2459], + [0.633, 0.32425, 0.24442], + [0.62729, 0.31901, 0.24298], + [0.62152, 0.3138, 0.24157], + [0.6157, 0.3086, 0.24017], + [0.60983, 0.30341, 0.23881], + [0.60391, 0.29826, 0.23752], + [0.59793, 0.29314, 0.23623], + [0.59191, 0.28805, 0.235], + [0.58585, 0.28302, 0.23377], + [0.57974, 0.27799, 0.23263], + [0.57359, 0.27302, 0.23155], + [0.56741, 0.26808, 0.23047], + [0.5612, 0.26321, 0.22948], + [0.55496, 0.25837, 0.22857], + [0.54871, 0.25361, 0.22769], + [0.54243, 0.24891, 0.22689], + [0.53614, 0.24424, 0.22616], + [0.52984, 0.23968, 0.22548], + [0.52354, 0.2352, 0.22487], + [0.51724, 0.23076, 0.22436], + [0.51094, 0.22643, 0.22395], + [0.50467, 0.22217, 0.22363], + [0.49841, 0.21802, 0.22339], + [0.49217, 0.21397, 0.22325], + [0.48595, 0.21, 0.22321], + [0.47979, 0.20618, 0.22328], + [0.47364, 0.20242, 0.22345], + [0.46756, 0.1988, 0.22373], + [0.46152, 0.19532, 0.22413], + [0.45554, 0.19195, 0.22465], + [0.44962, 0.18873, 0.22534], + [0.44377, 0.18566, 0.22616], + [0.43799, 0.18266, 0.22708], + [0.43229, 0.17987, 0.22817], + [0.42665, 0.17723, 0.22938], + [0.42111, 0.17474, 0.23077], + [0.41567, 0.17238, 0.23232], + [0.41033, 0.17023, 0.23401], + [0.40507, 0.16822, 0.2359], + [0.39992, 0.1664, 0.23794], + [0.39489, 0.16475, 0.24014], + [0.38996, 0.16331, 0.24254], + [0.38515, 0.16203, 0.24512], + [0.38046, 0.16093, 0.24792], + [0.37589, 0.16, 0.25087], + [0.37143, 0.15932, 0.25403], + [0.36711, 0.15883, 0.25738], + [0.36292, 0.15853, 0.26092], + [0.35885, 0.15843, 0.26466], + [0.35494, 0.15853, 0.26862], + [0.35114, 0.15882, 0.27276], + [0.34748, 0.15931, 0.27711], + [0.34394, 0.15999, 0.28164], + [0.34056, 0.16094, 0.28636], + [0.33731, 0.16207, 0.29131], + [0.3342, 0.16338, 0.29642], + [0.33121, 0.16486, 0.3017], + [0.32837, 0.16658, 0.30719], + [0.32565, 0.16847, 0.31284], + [0.3231, 0.17056, 0.31867], + [0.32066, 0.17283, 0.32465], + [0.31834, 0.1753, 0.33079], + [0.31616, 0.17797, 0.3371], + [0.3141, 0.18074, 0.34354], + [0.31216, 0.18373, 0.35011], + [0.31038, 0.1869, 0.35682], + [0.3087, 0.19021, 0.36363], + [0.30712, 0.1937, 0.37056], + [0.3057, 0.19732, 0.3776], + [0.30435, 0.20106, 0.38473], + [0.30314, 0.205, 0.39195], + [0.30204, 0.20905, 0.39924], + [0.30106, 0.21323, 0.40661], + [0.30019, 0.21756, 0.41404], + [0.29944, 0.22198, 0.42151], + [0.29878, 0.22656, 0.42904], + [0.29822, 0.23122, 0.4366], + [0.29778, 0.23599, 0.44419], + [0.29745, 0.24085, 0.45179], + [0.29721, 0.24582, 0.45941], + [0.29708, 0.2509, 0.46703], + [0.29704, 0.25603, 0.47465], + [0.2971, 0.26127, 0.48225], + [0.29726, 0.26658, 0.48983], + [0.2975, 0.27194, 0.4974], + [0.29784, 0.27741, 0.50493], + [0.29828, 0.28292, 0.51242], + [0.29881, 0.28847, 0.51987], + [0.29943, 0.29408, 0.52728], + [0.30012, 0.29976, 0.53463], + [0.3009, 0.30548, 0.54191], + [0.30176, 0.31122, 0.54915], + [0.30271, 0.317, 0.5563], + [0.30373, 0.32283, 0.56339], + [0.30483, 0.32866, 0.5704], + [0.30601, 0.33454, 0.57733], + [0.30722, 0.34042, 0.58418], + [0.30853, 0.34631, 0.59095], + [0.30989, 0.35224, 0.59763], + [0.3113, 0.35817, 0.60423], + [0.31277, 0.3641, 0.61073], + [0.31431, 0.37005, 0.61715], + [0.3159, 0.376, 0.62347], + [0.31752, 0.38195, 0.62969], + [0.3192, 0.3879, 0.63583], + [0.32092, 0.39385, 0.64188], + [0.32268, 0.39979, 0.64783], + [0.32446, 0.40575, 0.6537], + [0.3263, 0.41168, 0.65948], + [0.32817, 0.41763, 0.66517], + [0.33008, 0.42355, 0.67079], + [0.33201, 0.4295, 0.67632], + [0.33398, 0.43544, 0.68176], + [0.33596, 0.44137, 0.68715], + [0.33798, 0.44731, 0.69246], + [0.34003, 0.45327, 0.69769], + [0.3421, 0.45923, 0.70288], + [0.34419, 0.4652, 0.70799], + [0.34631, 0.4712, 0.71306], + [0.34847, 0.4772, 0.71808], + [0.35064, 0.48323, 0.72305], + [0.35283, 0.48928, 0.72798], + [0.35506, 0.49537, 0.73288], + [0.3573, 0.50149, 0.73773], + [0.35955, 0.50763, 0.74256], + [0.36185, 0.51381, 0.74736], + [0.36414, 0.52001, 0.75213], + [0.36649, 0.52627, 0.75689], + [0.36884, 0.53256, 0.76162], + [0.37119, 0.53889, 0.76633], + [0.37359, 0.54525, 0.77103], + [0.376, 0.55166, 0.77571], + [0.37842, 0.55809, 0.78037], + [0.38087, 0.56458, 0.78503], + [0.38333, 0.5711, 0.78966], + [0.38579, 0.57766, 0.79429], + [0.38828, 0.58426, 0.7989], + [0.39078, 0.59088, 0.8035], + [0.39329, 0.59755, 0.8081], + [0.39582, 0.60426, 0.81268], + [0.39835, 0.61099, 0.81725], + [0.4009, 0.61774, 0.82182], + [0.40344, 0.62454, 0.82637], + [0.406, 0.63137, 0.83092], + [0.40856, 0.63822, 0.83546], + [0.41114, 0.6451, 0.83999], + [0.41372, 0.65202, 0.84451], + [0.41631, 0.65896, 0.84903], + [0.4189, 0.66593, 0.85354], + [0.42149, 0.67294, 0.85805], + [0.4241, 0.67996, 0.86256], + [0.42671, 0.68702, 0.86705], + [0.42932, 0.69411, 0.87156], + [0.43195, 0.70123, 0.87606], + [0.43457, 0.70839, 0.88056], + [0.4372, 0.71557, 0.88506], + [0.43983, 0.72278, 0.88956], + [0.44248, 0.73004, 0.89407], + [0.44512, 0.73732, 0.89858], + [0.44776, 0.74464, 0.9031], + [0.45042, 0.752, 0.90763], + [0.45308, 0.75939, 0.91216], + [0.45574, 0.76682, 0.9167], + [0.45841, 0.77429, 0.92124], + [0.46109, 0.78181, 0.9258], + [0.46377, 0.78936, 0.93036], + [0.46645, 0.79694, 0.93494], + [0.46914, 0.80458, 0.93952], + [0.47183, 0.81224, 0.94412], + [0.47453, 0.81995, 0.94872], + [0.47721, 0.8277, 0.95334], + [0.47992, 0.83549, 0.95796], + [0.48261, 0.84331, 0.96259], + [0.4853, 0.85117, 0.96722], + [0.48801, 0.85906, 0.97186], + [0.49071, 0.86699, 0.97651], + [0.49339, 0.87495, 0.98116], + [0.49607, 0.88294, 0.98581], + [0.49877, 0.89096, 0.99047], + [0.50144, 0.89901, 0.99512], + [0.50411, 0.90708, 0.99978]] + +_vanimo_data = [ + [1, 0.80346, 0.99215], + [0.99397, 0.79197, 0.98374], + [0.98791, 0.78052, 0.97535], + [0.98185, 0.7691, 0.96699], + [0.97578, 0.75774, 0.95867], + [0.96971, 0.74643, 0.95037], + [0.96363, 0.73517, 0.94211], + [0.95755, 0.72397, 0.93389], + [0.95147, 0.71284, 0.9257], + [0.94539, 0.70177, 0.91756], + [0.93931, 0.69077, 0.90945], + [0.93322, 0.67984, 0.90137], + [0.92713, 0.66899, 0.89334], + [0.92104, 0.65821, 0.88534], + [0.91495, 0.64751, 0.87738], + [0.90886, 0.63689, 0.86946], + [0.90276, 0.62634, 0.86158], + [0.89666, 0.61588, 0.85372], + [0.89055, 0.60551, 0.84591], + [0.88444, 0.59522, 0.83813], + [0.87831, 0.58503, 0.83039], + [0.87219, 0.57491, 0.82268], + [0.86605, 0.5649, 0.815], + [0.8599, 0.55499, 0.80736], + [0.85373, 0.54517, 0.79974], + [0.84756, 0.53544, 0.79216], + [0.84138, 0.52583, 0.78461], + [0.83517, 0.5163, 0.77709], + [0.82896, 0.5069, 0.76959], + [0.82272, 0.49761, 0.76212], + [0.81647, 0.48841, 0.75469], + [0.81018, 0.47934, 0.74728], + [0.80389, 0.47038, 0.7399], + [0.79757, 0.46154, 0.73255], + [0.79123, 0.45283, 0.72522], + [0.78487, 0.44424, 0.71792], + [0.77847, 0.43578, 0.71064], + [0.77206, 0.42745, 0.70339], + [0.76562, 0.41925, 0.69617], + [0.75914, 0.41118, 0.68897], + [0.75264, 0.40327, 0.68179], + [0.74612, 0.39549, 0.67465], + [0.73957, 0.38783, 0.66752], + [0.73297, 0.38034, 0.66041], + [0.72634, 0.37297, 0.65331], + [0.71967, 0.36575, 0.64623], + [0.71293, 0.35864, 0.63915], + [0.70615, 0.35166, 0.63206], + [0.69929, 0.34481, 0.62496], + [0.69236, 0.33804, 0.61782], + [0.68532, 0.33137, 0.61064], + [0.67817, 0.32479, 0.6034], + [0.67091, 0.3183, 0.59609], + [0.66351, 0.31184, 0.5887], + [0.65598, 0.30549, 0.58123], + [0.64828, 0.29917, 0.57366], + [0.64045, 0.29289, 0.56599], + [0.63245, 0.28667, 0.55822], + [0.6243, 0.28051, 0.55035], + [0.61598, 0.27442, 0.54237], + [0.60752, 0.26838, 0.53428], + [0.59889, 0.2624, 0.5261], + [0.59012, 0.25648, 0.51782], + [0.5812, 0.25063, 0.50944], + [0.57214, 0.24483, 0.50097], + [0.56294, 0.23914, 0.4924], + [0.55359, 0.23348, 0.48376], + [0.54413, 0.22795, 0.47505], + [0.53454, 0.22245, 0.46623], + [0.52483, 0.21706, 0.45736], + [0.51501, 0.21174, 0.44843], + [0.50508, 0.20651, 0.43942], + [0.49507, 0.20131, 0.43036], + [0.48495, 0.19628, 0.42125], + [0.47476, 0.19128, 0.4121], + [0.4645, 0.18639, 0.4029], + [0.45415, 0.18157, 0.39367], + [0.44376, 0.17688, 0.38441], + [0.43331, 0.17225, 0.37513], + [0.42282, 0.16773, 0.36585], + [0.41232, 0.16332, 0.35655], + [0.40178, 0.15897, 0.34726], + [0.39125, 0.15471, 0.33796], + [0.38071, 0.15058, 0.32869], + [0.37017, 0.14651, 0.31945], + [0.35969, 0.14258, 0.31025], + [0.34923, 0.13872, 0.30106], + [0.33883, 0.13499, 0.29196], + [0.32849, 0.13133, 0.28293], + [0.31824, 0.12778, 0.27396], + [0.30808, 0.12431, 0.26508], + [0.29805, 0.12097, 0.25631], + [0.28815, 0.11778, 0.24768], + [0.27841, 0.11462, 0.23916], + [0.26885, 0.11169, 0.23079], + [0.25946, 0.10877, 0.22259], + [0.25025, 0.10605, 0.21455], + [0.24131, 0.10341, 0.20673], + [0.23258, 0.10086, 0.19905], + [0.2241, 0.098494, 0.19163], + [0.21593, 0.096182, 0.18443], + [0.20799, 0.094098, 0.17748], + [0.20032, 0.092102, 0.17072], + [0.19299, 0.09021, 0.16425], + [0.18596, 0.088461, 0.15799], + [0.17918, 0.086861, 0.15197], + [0.17272, 0.08531, 0.14623], + [0.16658, 0.084017, 0.14075], + [0.1607, 0.082745, 0.13546], + [0.15515, 0.081683, 0.13049], + [0.1499, 0.080653, 0.1257], + [0.14493, 0.07978, 0.12112], + [0.1402, 0.079037, 0.11685], + [0.13578, 0.078426, 0.11282], + [0.13168, 0.077944, 0.10894], + [0.12782, 0.077586, 0.10529], + [0.12422, 0.077332, 0.1019], + [0.12091, 0.077161, 0.098724], + [0.11793, 0.077088, 0.095739], + [0.11512, 0.077124, 0.092921], + [0.11267, 0.077278, 0.090344], + [0.11042, 0.077557, 0.087858], + [0.10835, 0.077968, 0.085431], + [0.10665, 0.078516, 0.083233], + [0.105, 0.079207, 0.081185], + [0.10368, 0.080048, 0.079202], + [0.10245, 0.081036, 0.077408], + [0.10143, 0.082173, 0.075793], + [0.1006, 0.083343, 0.074344], + [0.099957, 0.084733, 0.073021], + [0.099492, 0.086174, 0.071799], + [0.099204, 0.087868, 0.070716], + [0.099092, 0.089631, 0.069813], + [0.099154, 0.091582, 0.069047], + [0.099384, 0.093597, 0.068337], + [0.099759, 0.095871, 0.067776], + [0.10029, 0.098368, 0.067351], + [0.10099, 0.101, 0.067056], + [0.10185, 0.1039, 0.066891], + [0.1029, 0.10702, 0.066853], + [0.10407, 0.11031, 0.066942], + [0.10543, 0.1138, 0.067155], + [0.10701, 0.1175, 0.067485], + [0.10866, 0.12142, 0.067929], + [0.11059, 0.12561, 0.06849], + [0.11265, 0.12998, 0.069162], + [0.11483, 0.13453, 0.069842], + [0.11725, 0.13923, 0.07061], + [0.11985, 0.14422, 0.071528], + [0.12259, 0.14937, 0.072403], + [0.12558, 0.15467, 0.073463], + [0.12867, 0.16015, 0.074429], + [0.13196, 0.16584, 0.075451], + [0.1354, 0.17169, 0.076499], + [0.13898, 0.17771, 0.077615], + [0.14273, 0.18382, 0.078814], + [0.14658, 0.1901, 0.080098], + [0.15058, 0.19654, 0.081473], + [0.15468, 0.20304, 0.08282], + [0.15891, 0.20968, 0.084315], + [0.16324, 0.21644, 0.085726], + [0.16764, 0.22326, 0.087378], + [0.17214, 0.23015, 0.088955], + [0.17673, 0.23717, 0.090617], + [0.18139, 0.24418, 0.092314], + [0.18615, 0.25132, 0.094071], + [0.19092, 0.25846, 0.095839], + [0.19578, 0.26567, 0.097702], + [0.20067, 0.2729, 0.099539], + [0.20564, 0.28016, 0.10144], + [0.21062, 0.28744, 0.10342], + [0.21565, 0.29475, 0.10534], + [0.22072, 0.30207, 0.10737], + [0.22579, 0.30942, 0.10942], + [0.23087, 0.31675, 0.11146], + [0.236, 0.32407, 0.11354], + [0.24112, 0.3314, 0.11563], + [0.24625, 0.33874, 0.11774], + [0.25142, 0.34605, 0.11988], + [0.25656, 0.35337, 0.12202], + [0.26171, 0.36065, 0.12422], + [0.26686, 0.36793, 0.12645], + [0.272, 0.37519, 0.12865], + [0.27717, 0.38242, 0.13092], + [0.28231, 0.38964, 0.13316], + [0.28741, 0.39682, 0.13541], + [0.29253, 0.40398, 0.13773], + [0.29763, 0.41111, 0.13998], + [0.30271, 0.4182, 0.14232], + [0.30778, 0.42527, 0.14466], + [0.31283, 0.43231, 0.14699], + [0.31787, 0.43929, 0.14937], + [0.32289, 0.44625, 0.15173], + [0.32787, 0.45318, 0.15414], + [0.33286, 0.46006, 0.1566], + [0.33781, 0.46693, 0.15904], + [0.34276, 0.47374, 0.16155], + [0.34769, 0.48054, 0.16407], + [0.3526, 0.48733, 0.16661], + [0.35753, 0.4941, 0.16923], + [0.36245, 0.50086, 0.17185], + [0.36738, 0.50764, 0.17458], + [0.37234, 0.51443, 0.17738], + [0.37735, 0.52125, 0.18022], + [0.38238, 0.52812, 0.18318], + [0.38746, 0.53505, 0.18626], + [0.39261, 0.54204, 0.18942], + [0.39783, 0.54911, 0.19272], + [0.40311, 0.55624, 0.19616], + [0.40846, 0.56348, 0.1997], + [0.4139, 0.57078, 0.20345], + [0.41942, 0.57819, 0.20734], + [0.42503, 0.5857, 0.2114], + [0.43071, 0.59329, 0.21565], + [0.43649, 0.60098, 0.22009], + [0.44237, 0.60878, 0.2247], + [0.44833, 0.61667, 0.22956], + [0.45439, 0.62465, 0.23468], + [0.46053, 0.63274, 0.23997], + [0.46679, 0.64092, 0.24553], + [0.47313, 0.64921, 0.25138], + [0.47959, 0.6576, 0.25745], + [0.48612, 0.66608, 0.26382], + [0.49277, 0.67466, 0.27047], + [0.49951, 0.68335, 0.2774], + [0.50636, 0.69213, 0.28464], + [0.51331, 0.70101, 0.2922], + [0.52035, 0.70998, 0.30008], + [0.5275, 0.71905, 0.30828], + [0.53474, 0.72821, 0.31682], + [0.54207, 0.73747, 0.32567], + [0.5495, 0.74682, 0.33491], + [0.55702, 0.75625, 0.34443], + [0.56461, 0.76577, 0.35434], + [0.5723, 0.77537, 0.36457], + [0.58006, 0.78506, 0.37515], + [0.58789, 0.79482, 0.38607], + [0.59581, 0.80465, 0.39734], + [0.60379, 0.81455, 0.40894], + [0.61182, 0.82453, 0.42086], + [0.61991, 0.83457, 0.43311], + [0.62805, 0.84467, 0.44566], + [0.63623, 0.85482, 0.45852], + [0.64445, 0.86503, 0.47168], + [0.6527, 0.8753, 0.48511], + [0.66099, 0.88562, 0.49882], + [0.6693, 0.89599, 0.51278], + [0.67763, 0.90641, 0.52699], + [0.68597, 0.91687, 0.54141], + [0.69432, 0.92738, 0.55605], + [0.70269, 0.93794, 0.5709], + [0.71107, 0.94855, 0.58593], + [0.71945, 0.9592, 0.60112], + [0.72782, 0.96989, 0.61646], + [0.7362, 0.98063, 0.63191], + [0.74458, 0.99141, 0.64748]] cmaps = { name: ListedColormap(data, name=name) for name, data in [ @@ -2068,4 +2841,7 @@ ('twilight', _twilight_data), ('twilight_shifted', _twilight_shifted_data), ('turbo', _turbo_data), + ('berlin', _berlin_data), + ('managua', _managua_data), + ('vanimo', _vanimo_data), ]} From 3a4d099790b399dc549bb69fd529b8387efc51b7 Mon Sep 17 00:00:00 2001 From: Kaustubh <97254178+Kaustbh@users.noreply.github.com> Date: Thu, 1 Aug 2024 21:31:27 +0530 Subject: [PATCH 0096/1230] Moved comment inside Co-authored-by: Elliott Sales de Andrade --- lib/matplotlib/widgets.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index e9f5c6f9eea8..683e0534f332 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -4000,9 +4000,8 @@ def onmove(self, event): # needs to process the move callback even if there is no button press. # _SelectorWidget.onmove include logic to ignore move event if # _eventpress is None. - - # Hide the cursor when interactive zoom/pan is active if self.ignore(event): + # Hide the cursor when interactive zoom/pan is active if not self.canvas.widgetlock.available(self) and self._xys: self._xys[-1] = (np.nan, np.nan) self._draw_polygon() From 7457ba43b8362bb43de954fc46897e9da7738e02 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 1 Aug 2024 13:57:56 -0500 Subject: [PATCH 0097/1230] Backport PR #28625: added typing_extensions.Self to _AxesBase.twinx --- lib/matplotlib/axes/_base.pyi | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axes/_base.pyi b/lib/matplotlib/axes/_base.pyi index 751dcd248a5c..8cd88a92cc09 100644 --- a/lib/matplotlib/axes/_base.pyi +++ b/lib/matplotlib/axes/_base.pyi @@ -4,6 +4,7 @@ import datetime from collections.abc import Callable, Iterable, Iterator, Sequence from matplotlib import cbook from matplotlib.artist import Artist +from matplotlib.axes import Axes from matplotlib.axis import XAxis, YAxis, Tick from matplotlib.backend_bases import RendererBase, MouseButton, MouseEvent from matplotlib.cbook import CallbackRegistry @@ -384,8 +385,8 @@ class _AxesBase(martist.Artist): bbox_extra_artists: Sequence[Artist] | None = ..., for_layout_only: bool = ... ) -> Bbox | None: ... - def twinx(self) -> _AxesBase: ... - def twiny(self) -> _AxesBase: ... + def twinx(self) -> Axes: ... + def twiny(self) -> Axes: ... def get_shared_x_axes(self) -> cbook.GrouperView: ... def get_shared_y_axes(self) -> cbook.GrouperView: ... def label_outer(self, remove_inner_ticks: bool = ...) -> None: ... From e0e49ef65a904695bdf301710983cc990943ccef Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 1 Aug 2024 22:15:20 +0200 Subject: [PATCH 0098/1230] DOC: Remove hint on PRs from origin/main --- doc/devel/development_workflow.rst | 7 ------- 1 file changed, 7 deletions(-) diff --git a/doc/devel/development_workflow.rst b/doc/devel/development_workflow.rst index 03d59ad097e4..a62971ac76d6 100644 --- a/doc/devel/development_workflow.rst +++ b/doc/devel/development_workflow.rst @@ -79,13 +79,6 @@ default, git will have a link to your fork of the GitHub repo, called git push origin my-new-feature -.. hint:: - - If you first opened the pull request from your ``main`` branch and then - converted it to a feature branch, you will need to close the original pull - request and open a new pull request from the renamed branch. See - `GitHub: working with branches - `_. .. _edit-flow: From 6ed77f6b6965408b6a07524e8fbcf77675593de2 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 1 Aug 2024 17:31:20 -0400 Subject: [PATCH 0099/1230] Backport PR #28634: Closed open div tag in color.ColorMap._repr_html_ --- lib/matplotlib/colors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 177557b371a6..5f40e7b0fb9a 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -960,6 +960,7 @@ def color_block(color): '' '
' f'over {color_block(self.get_over())}' + '
' '') def copy(self): From b7c34515b9b65978956f8eb9716f6fe0984626c9 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 2 Aug 2024 00:34:57 +0200 Subject: [PATCH 0100/1230] DOC: Simplify heatmap example Directly rotate ticks in `set_xticks` instead of setting the ticks and then changing them afterwards using `plt.setp(...)`. --- .../image_annotated_heatmap.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/galleries/examples/images_contours_and_fields/image_annotated_heatmap.py b/galleries/examples/images_contours_and_fields/image_annotated_heatmap.py index 23d9fd48dff8..ebd50359a29a 100644 --- a/galleries/examples/images_contours_and_fields/image_annotated_heatmap.py +++ b/galleries/examples/images_contours_and_fields/image_annotated_heatmap.py @@ -63,12 +63,9 @@ im = ax.imshow(harvest) # Show all ticks and label them with the respective list entries -ax.set_xticks(np.arange(len(farmers)), labels=farmers) -ax.set_yticks(np.arange(len(vegetables)), labels=vegetables) - -# Rotate the tick labels and set their alignment. -plt.setp(ax.get_xticklabels(), rotation=45, ha="right", - rotation_mode="anchor") +ax.set_xticks(range(len(farmers)), labels=farmers, + rotation=45, ha="right", rotation_mode="anchor") +ax.set_yticks(range(len(vegetables)), labels=vegetables) # Loop over data dimensions and create text annotations. for i in range(len(vegetables)): @@ -137,17 +134,14 @@ def heatmap(data, row_labels, col_labels, ax=None, cbar.ax.set_ylabel(cbarlabel, rotation=-90, va="bottom") # Show all ticks and label them with the respective list entries. - ax.set_xticks(np.arange(data.shape[1]), labels=col_labels) - ax.set_yticks(np.arange(data.shape[0]), labels=row_labels) + ax.set_xticks(range(data.shape[1]), labels=col_labels, + rotation=-30, ha="right", rotation_mode="anchor") + ax.set_yticks(range(data.shape[0]), labels=row_labels) # Let the horizontal axes labeling appear on top. ax.tick_params(top=True, bottom=False, labeltop=True, labelbottom=False) - # Rotate the tick labels and set their alignment. - plt.setp(ax.get_xticklabels(), rotation=-30, ha="right", - rotation_mode="anchor") - # Turn spines off and create white grid. ax.spines[:].set_visible(False) From f72565263f79f91d95ab4f5965634c4c6d2c092b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 1 Aug 2024 19:26:19 -0400 Subject: [PATCH 0101/1230] DOC: Fix matching for version switcher A released version should point to its version, not 'stable', since that doesn't appear in the version switcher. And devdocs should point to 'dev', since that's what it's called in the JSON. --- doc/conf.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 882370b7e255..7e8c58489618 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -514,10 +514,9 @@ def js_tag_with_cache_busting(js): f"https://matplotlib.org/devdocs/_static/switcher.json?{SHA}" ), "version_match": ( - # The start version to show. This must be in switcher.json. - # We either go to 'stable' or to 'devdocs' - 'stable' if matplotlib.__version_info__.releaselevel == 'final' - else 'devdocs') + matplotlib.__version__ + if matplotlib.__version_info__.releaselevel == 'final' + else 'dev') }, "navbar_end": ["theme-switcher", "version-switcher", "mpl_icon_links"], "navbar_persistent": ["search-button"], From 105533f20837e152625269a32e9e11cfe9d93ef9 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 2 Aug 2024 01:28:29 -0400 Subject: [PATCH 0102/1230] Backport PR #28644: DOC: Fix matching for version switcher --- doc/conf.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index f43806a8b4c0..56e09a24b53a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -496,10 +496,9 @@ def js_tag_with_cache_busting(js): f"https://matplotlib.org/devdocs/_static/switcher.json?{SHA}" ), "version_match": ( - # The start version to show. This must be in switcher.json. - # We either go to 'stable' or to 'devdocs' - 'stable' if matplotlib.__version_info__.releaselevel == 'final' - else 'devdocs') + matplotlib.__version__ + if matplotlib.__version_info__.releaselevel == 'final' + else 'dev') }, "navbar_end": ["theme-switcher", "version-switcher", "mpl_icon_links"], "navbar_persistent": ["search-button"], From ab258b73605635a0c6bc28d3e5e391232abd3292 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 2 Aug 2024 01:28:29 -0400 Subject: [PATCH 0103/1230] Backport PR #28644: DOC: Fix matching for version switcher --- doc/conf.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index f43806a8b4c0..56e09a24b53a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -496,10 +496,9 @@ def js_tag_with_cache_busting(js): f"https://matplotlib.org/devdocs/_static/switcher.json?{SHA}" ), "version_match": ( - # The start version to show. This must be in switcher.json. - # We either go to 'stable' or to 'devdocs' - 'stable' if matplotlib.__version_info__.releaselevel == 'final' - else 'devdocs') + matplotlib.__version__ + if matplotlib.__version_info__.releaselevel == 'final' + else 'dev') }, "navbar_end": ["theme-switcher", "version-switcher", "mpl_icon_links"], "navbar_persistent": ["search-button"], From 16827b82f0a2b9f71dfbebf3e8895f13a96e5c98 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 2 Aug 2024 12:27:11 -0400 Subject: [PATCH 0104/1230] FIX: improve formatting of image values in cases of singular norms closes #28648 --- lib/matplotlib/artist.py | 4 +++- lib/matplotlib/cbook.py | 4 ++++ lib/matplotlib/tests/test_image.py | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 981365d852be..24aaa349ee05 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -1365,7 +1365,9 @@ def format_cursor_data(self, data): delta = np.diff( self.norm.boundaries[neigh_idx:cur_idx + 2] ).max() - + elif self.norm.vmin == self.norm.vmax: + # singular norms, use delta of 10% of only value + delta = np.abs(self.norm.vmin * .1) else: # Midpoints of neighboring color intervals. neighbors = self.norm.inverse( diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index e4f60aac37a8..2411784af3ec 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2252,6 +2252,10 @@ def _g_sig_digits(value, delta): it is known with an error of *delta*. """ if delta == 0: + if value == 0: + # if both value and delta are 0, np.spacing below returns 5e-324 + # which results in rather silly results + return 3 # delta = 0 may occur when trying to format values over a tiny range; # in that case, replace it by the distance to the closest float. delta = abs(np.spacing(value)) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index dfacfccb3e0e..6de1754f9ed7 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -390,7 +390,8 @@ def test_cursor_data_nonuniform(xy, data): ([[.123, .987]], "[0.123]"), ([[np.nan, 1, 2]], "[]"), ([[1, 1+1e-15]], "[1.0000000000000000]"), - ([[-1, -1]], "[-1.0000000000000000]"), + ([[-1, -1]], "[-1.0]"), + ([[0, 0]], "[0.00]"), ]) def test_format_cursor_data(data, text): from matplotlib.backend_bases import MouseEvent From 1d65122767bf2e7c6838eecd0f13b73c3059f550 Mon Sep 17 00:00:00 2001 From: Caitlin Hathaway <103151440+caitlinhat@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:47:40 -0500 Subject: [PATCH 0105/1230] remove out of date todos on animation.py --- lib/matplotlib/animation.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 9108b727b50c..28b01fedf138 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -1,10 +1,5 @@ # TODO: -# * Documentation -- this will need a new section of the User's Guide. -# Both for Animations and just timers. -# - Also need to update -# https://scipy-cookbook.readthedocs.io/items/Matplotlib_Animations.html # * Blit -# * Currently broken with Qt4 for widgets that don't start on screen # * Still a few edge cases that aren't working correctly # * Can this integrate better with existing matplotlib animation artist flag? # - If animated removes from default draw(), perhaps we could use this to From 905a7bb3f134e4f3d7b350e1e87fb832f861abe2 Mon Sep 17 00:00:00 2001 From: scaccol Date: Fri, 2 Aug 2024 12:48:28 -0700 Subject: [PATCH 0106/1230] Fix docstring style inconsistincies --- lib/matplotlib/lines.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 72e74f4eb9c5..bc3d6caddc12 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -1541,21 +1541,15 @@ def draw(self, renderer): super().draw(renderer) def get_xy1(self): - """ - Return the *xy1* value of the line. - """ + """Return the *xy1* value of the line.""" return self._xy1 def get_xy2(self): - """ - Return the *xy2* value of the line. - """ + """Return the *xy2* value of the line.""" return self._xy2 def get_slope(self): - """ - Return the *slope* value of the line. - """ + """Return the *slope* value of the line.""" return self._slope def set_xy1(self, x, y): From a4e9e0749872946bcbd85390417ca1d2feb863b0 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 2 Aug 2024 00:10:53 +0200 Subject: [PATCH 0107/1230] DOC: Standardize example titles - part 2 Follow up to #28544. As there: Following recommendations from #28527, this improves example titles. Take this as an incremental improvement. I've changed what I saw t a glance when going through the examples once. Certainly, one could do further improvements, but that can be done in follow-ups. Co-authored-by: hannah --- galleries/examples/animation/pause_resume.py | 6 +++--- galleries/examples/axes_grid1/demo_axes_rgb.py | 6 +++--- .../demo_colorbar_with_inset_locator.py | 6 +++--- .../examples/axes_grid1/demo_imagegrid_aspect.py | 2 +- galleries/examples/color/custom_cmap.py | 6 +++--- .../image_annotated_heatmap.py | 6 +++--- .../examples/lines_bars_and_markers/bar_colors.py | 6 +++--- .../lines_bars_and_markers/fill_between_alpha.py | 6 +++--- .../lines_bars_and_markers/fill_between_demo.py | 6 +++--- .../lines_bars_and_markers/fill_betweenx_demo.py | 6 +++--- .../line_demo_dash_control.py | 6 +++--- .../lines_bars_and_markers/scatter_masked.py | 6 +++--- .../lines_bars_and_markers/scatter_with_legend.py | 6 +++--- .../examples/lines_bars_and_markers/timeline.py | 6 +++--- galleries/examples/misc/bbox_intersect.py | 6 +++--- galleries/examples/misc/demo_ribbon_box.py | 2 +- galleries/examples/misc/fig_x.py | 15 ++++++++------- galleries/examples/misc/fill_spiral.py | 2 +- .../pie_and_polar_charts/pie_and_donut_labels.py | 6 +++--- .../shapes_and_collections/line_collection.py | 6 +++--- .../examples/statistics/errorbars_and_boxes.py | 6 +++--- .../examples/statistics/histogram_cumulative.py | 6 +++--- .../multiple_histograms_side_by_side.py | 6 +++--- .../auto_subplots_adjust.py | 6 +++--- .../subplots_axes_and_figures/axhspan_demo.py | 6 +++--- .../demo_constrained_layout.py | 6 +++--- .../demo_tight_layout.py | 6 +++--- .../subplots_axes_and_figures/ganged_plots.py | 6 +++--- .../gridspec_and_subplots.py | 6 +++--- .../gridspec_multicolumn.py | 6 +++--- .../multiple_figs_demo.py | 6 +++--- .../share_axis_lims_views.py | 5 +++-- .../subplots_axes_and_figures/subplots_demo.py | 6 +++--- .../text_labels_and_annotations/autowrap.py | 10 +++++----- .../engineering_formatter.py | 6 +++--- .../text_labels_and_annotations/font_family_rc.py | 6 +++--- .../text_labels_and_annotations/rainbow_text.py | 6 +++--- .../text_labels_and_annotations/tex_demo.py | 6 +++--- .../usetex_fonteffects.py | 6 +++--- galleries/examples/ticks/centered_ticklabels.py | 6 +++--- .../examples/ticks/date_concise_formatter.py | 6 +++--- galleries/examples/ticks/ticklabels_rotation.py | 6 +++--- galleries/examples/units/basic_units.py | 2 +- .../embedding_in_gtk3_panzoom_sgskip.py | 8 ++++---- .../user_interfaces/embedding_in_gtk3_sgskip.py | 8 ++++---- .../embedding_in_gtk4_panzoom_sgskip.py | 8 ++++---- .../user_interfaces/embedding_in_gtk4_sgskip.py | 8 ++++---- .../user_interfaces/embedding_in_qt_sgskip.py | 6 +++--- .../user_interfaces/embedding_in_tk_sgskip.py | 8 ++++---- .../user_interfaces/embedding_in_wx2_sgskip.py | 6 +++--- .../user_interfaces/embedding_in_wx3_sgskip.py | 6 +++--- .../user_interfaces/embedding_in_wx4_sgskip.py | 6 +++--- .../user_interfaces/embedding_in_wx5_sgskip.py | 6 +++--- .../web_application_server_sgskip.py | 6 +++--- .../user_interfaces/wxcursor_demo_sgskip.py | 6 +++--- galleries/examples/widgets/slider_snap_demo.py | 6 +++--- 56 files changed, 172 insertions(+), 170 deletions(-) diff --git a/galleries/examples/animation/pause_resume.py b/galleries/examples/animation/pause_resume.py index 13de31f36f89..e62dd049e11f 100644 --- a/galleries/examples/animation/pause_resume.py +++ b/galleries/examples/animation/pause_resume.py @@ -1,7 +1,7 @@ """ -================================= -Pausing and resuming an animation -================================= +============================= +Pause and resume an animation +============================= This example showcases: diff --git a/galleries/examples/axes_grid1/demo_axes_rgb.py b/galleries/examples/axes_grid1/demo_axes_rgb.py index ecbe1b89fd72..2cdb41fc323b 100644 --- a/galleries/examples/axes_grid1/demo_axes_rgb.py +++ b/galleries/examples/axes_grid1/demo_axes_rgb.py @@ -1,7 +1,7 @@ """ -================================== -Showing RGB channels using RGBAxes -================================== +=============================== +Show RGB channels using RGBAxes +=============================== `~.axes_grid1.axes_rgb.RGBAxes` creates a layout of 4 Axes for displaying RGB channels: one large Axes for the RGB image and 3 smaller Axes for the R, G, B diff --git a/galleries/examples/axes_grid1/demo_colorbar_with_inset_locator.py b/galleries/examples/axes_grid1/demo_colorbar_with_inset_locator.py index d989fb44bbab..f62a0f58e3bc 100644 --- a/galleries/examples/axes_grid1/demo_colorbar_with_inset_locator.py +++ b/galleries/examples/axes_grid1/demo_colorbar_with_inset_locator.py @@ -1,9 +1,9 @@ """ .. _demo-colorbar-with-inset-locator: -============================================================== -Controlling the position and size of colorbars with Inset Axes -============================================================== +=========================================================== +Control the position and size of a colorbar with Inset Axes +=========================================================== This example shows how to control the position, height, and width of colorbars using `~mpl_toolkits.axes_grid1.inset_locator.inset_axes`. diff --git a/galleries/examples/axes_grid1/demo_imagegrid_aspect.py b/galleries/examples/axes_grid1/demo_imagegrid_aspect.py index 820a2e8e1d2d..55268c41c9b1 100644 --- a/galleries/examples/axes_grid1/demo_imagegrid_aspect.py +++ b/galleries/examples/axes_grid1/demo_imagegrid_aspect.py @@ -1,6 +1,6 @@ """ ========================================= -Setting a fixed aspect on ImageGrid cells +ImageGrid cells with a fixed aspect ratio ========================================= """ diff --git a/galleries/examples/color/custom_cmap.py b/galleries/examples/color/custom_cmap.py index 667dc3133819..0a73b0c3135a 100644 --- a/galleries/examples/color/custom_cmap.py +++ b/galleries/examples/color/custom_cmap.py @@ -1,7 +1,7 @@ """ -========================================= -Creating a colormap from a list of colors -========================================= +======================================= +Create a colormap from a list of colors +======================================= For more detail on creating and manipulating colormaps see :ref:`colormap-manipulation`. diff --git a/galleries/examples/images_contours_and_fields/image_annotated_heatmap.py b/galleries/examples/images_contours_and_fields/image_annotated_heatmap.py index 23d9fd48dff8..7bd9df2750f4 100644 --- a/galleries/examples/images_contours_and_fields/image_annotated_heatmap.py +++ b/galleries/examples/images_contours_and_fields/image_annotated_heatmap.py @@ -1,7 +1,7 @@ """ -=========================== -Creating annotated heatmaps -=========================== +================= +Annotated heatmap +================= It is often desirable to show data which depends on two independent variables as a color coded image plot. This is often referred to as a diff --git a/galleries/examples/lines_bars_and_markers/bar_colors.py b/galleries/examples/lines_bars_and_markers/bar_colors.py index 35e7a64ef605..f173b50c0672 100644 --- a/galleries/examples/lines_bars_and_markers/bar_colors.py +++ b/galleries/examples/lines_bars_and_markers/bar_colors.py @@ -1,7 +1,7 @@ """ -============== -Bar color demo -============== +==================================== +Bar chart with individual bar colors +==================================== This is an example showing how to control bar color and legend entries using the *color* and *label* parameters of `~matplotlib.pyplot.bar`. diff --git a/galleries/examples/lines_bars_and_markers/fill_between_alpha.py b/galleries/examples/lines_bars_and_markers/fill_between_alpha.py index 2887310378d1..1dadc4309e2e 100644 --- a/galleries/examples/lines_bars_and_markers/fill_between_alpha.py +++ b/galleries/examples/lines_bars_and_markers/fill_between_alpha.py @@ -1,7 +1,7 @@ """ -============================== -Fill Between with transparency -============================== +================================== +``fill_between`` with transparency +================================== The `~matplotlib.axes.Axes.fill_between` function generates a shaded region between a min and max boundary that is useful for illustrating ranges. diff --git a/galleries/examples/lines_bars_and_markers/fill_between_demo.py b/galleries/examples/lines_bars_and_markers/fill_between_demo.py index 656a8695ba18..5afdd722360f 100644 --- a/galleries/examples/lines_bars_and_markers/fill_between_demo.py +++ b/galleries/examples/lines_bars_and_markers/fill_between_demo.py @@ -1,7 +1,7 @@ """ -============================== -Filling the area between lines -============================== +=============================== +Fill the area between two lines +=============================== This example shows how to use `~.axes.Axes.fill_between` to color the area between two lines. diff --git a/galleries/examples/lines_bars_and_markers/fill_betweenx_demo.py b/galleries/examples/lines_bars_and_markers/fill_betweenx_demo.py index b311db42af85..472f42fdbfc4 100644 --- a/galleries/examples/lines_bars_and_markers/fill_betweenx_demo.py +++ b/galleries/examples/lines_bars_and_markers/fill_betweenx_demo.py @@ -1,7 +1,7 @@ """ -================== -Fill Betweenx Demo -================== +======================================== +Fill the area between two vertical lines +======================================== Using `~.Axes.fill_betweenx` to color along the horizontal direction between two curves. diff --git a/galleries/examples/lines_bars_and_markers/line_demo_dash_control.py b/galleries/examples/lines_bars_and_markers/line_demo_dash_control.py index c695bc51c176..5952809125de 100644 --- a/galleries/examples/lines_bars_and_markers/line_demo_dash_control.py +++ b/galleries/examples/lines_bars_and_markers/line_demo_dash_control.py @@ -1,7 +1,7 @@ """ -============================== -Customizing dashed line styles -============================== +=============================== +Dashed line style configuration +=============================== The dashing of a line is controlled via a dash sequence. It can be modified using `.Line2D.set_dashes`. diff --git a/galleries/examples/lines_bars_and_markers/scatter_masked.py b/galleries/examples/lines_bars_and_markers/scatter_masked.py index c8e603e6f3b0..2bf6e03a46d0 100644 --- a/galleries/examples/lines_bars_and_markers/scatter_masked.py +++ b/galleries/examples/lines_bars_and_markers/scatter_masked.py @@ -1,7 +1,7 @@ """ -========================== -Scatter with masked values -========================== +=============================== +Scatter plot with masked values +=============================== Mask some data points and add a line demarking masked regions. diff --git a/galleries/examples/lines_bars_and_markers/scatter_with_legend.py b/galleries/examples/lines_bars_and_markers/scatter_with_legend.py index 786ffff18807..5241e3ef1508 100644 --- a/galleries/examples/lines_bars_and_markers/scatter_with_legend.py +++ b/galleries/examples/lines_bars_and_markers/scatter_with_legend.py @@ -1,7 +1,7 @@ """ -=========================== -Scatter plots with a legend -=========================== +========================== +Scatter plot with a legend +========================== To create a scatter plot with a legend one may use a loop and create one `~.Axes.scatter` plot per item to appear in the legend and set the ``label`` diff --git a/galleries/examples/lines_bars_and_markers/timeline.py b/galleries/examples/lines_bars_and_markers/timeline.py index ef84515aedf1..b7f8ec57b1cc 100644 --- a/galleries/examples/lines_bars_and_markers/timeline.py +++ b/galleries/examples/lines_bars_and_markers/timeline.py @@ -1,7 +1,7 @@ """ -=============================================== -Creating a timeline with lines, dates, and text -=============================================== +==================================== +Timeline with lines, dates, and text +==================================== How to create a simple timeline using Matplotlib release dates. diff --git a/galleries/examples/misc/bbox_intersect.py b/galleries/examples/misc/bbox_intersect.py index c645cd34c155..9103705537d5 100644 --- a/galleries/examples/misc/bbox_intersect.py +++ b/galleries/examples/misc/bbox_intersect.py @@ -1,7 +1,7 @@ """ -=========================================== -Changing colors of lines intersecting a box -=========================================== +================================== +Identify whether artists intersect +================================== The lines intersecting the rectangle are colored in red, while the others are left as blue lines. This example showcases the `.intersects_bbox` function. diff --git a/galleries/examples/misc/demo_ribbon_box.py b/galleries/examples/misc/demo_ribbon_box.py index d5121ba6ff5c..5400a2a0063e 100644 --- a/galleries/examples/misc/demo_ribbon_box.py +++ b/galleries/examples/misc/demo_ribbon_box.py @@ -1,6 +1,6 @@ """ ========== -Ribbon Box +Ribbon box ========== """ diff --git a/galleries/examples/misc/fig_x.py b/galleries/examples/misc/fig_x.py index e2af3e766028..593a7e8f8aa5 100644 --- a/galleries/examples/misc/fig_x.py +++ b/galleries/examples/misc/fig_x.py @@ -1,9 +1,10 @@ """ -======================= -Adding lines to figures -======================= +============================== +Add lines directly to a figure +============================== -Adding lines to a figure without any Axes. +You can add artists such as a `.Line2D` directly to a figure. This is +typically useful for visual structuring. .. redirect-from:: /gallery/pyplots/fig_x """ @@ -12,9 +13,9 @@ import matplotlib.lines as lines -fig = plt.figure() -fig.add_artist(lines.Line2D([0, 1], [0, 1])) -fig.add_artist(lines.Line2D([0, 1], [1, 0])) +fig, axs = plt.subplots(2, 2, gridspec_kw={'hspace': 0.4, 'wspace': 0.4}) +fig.add_artist(lines.Line2D([0, 1], [0.47, 0.47], linewidth=3)) +fig.add_artist(lines.Line2D([0.5, 0.5], [1, 0], linewidth=3)) plt.show() # %% diff --git a/galleries/examples/misc/fill_spiral.py b/galleries/examples/misc/fill_spiral.py index e82f0203e39f..35b06886e985 100644 --- a/galleries/examples/misc/fill_spiral.py +++ b/galleries/examples/misc/fill_spiral.py @@ -1,6 +1,6 @@ """ =========== -Fill Spiral +Fill spiral =========== """ diff --git a/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py b/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py index ae9b805cf005..7f945d1056f4 100644 --- a/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py +++ b/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py @@ -1,7 +1,7 @@ """ -========================== -Labeling a pie and a donut -========================== +============================= +A pie and a donut with labels +============================= Welcome to the Matplotlib bakery. We will create a pie and a donut chart through the `pie method ` and diff --git a/galleries/examples/shapes_and_collections/line_collection.py b/galleries/examples/shapes_and_collections/line_collection.py index a27496f62e0e..d8b3fd655133 100644 --- a/galleries/examples/shapes_and_collections/line_collection.py +++ b/galleries/examples/shapes_and_collections/line_collection.py @@ -1,7 +1,7 @@ """ -============================================= -Plotting multiple lines with a LineCollection -============================================= +========================================== +Plot multiple lines using a LineCollection +========================================== Matplotlib can efficiently draw multiple lines at once using a `~.LineCollection`. """ diff --git a/galleries/examples/statistics/errorbars_and_boxes.py b/galleries/examples/statistics/errorbars_and_boxes.py index 54c8786096c7..886cd7a17c88 100644 --- a/galleries/examples/statistics/errorbars_and_boxes.py +++ b/galleries/examples/statistics/errorbars_and_boxes.py @@ -1,7 +1,7 @@ """ -==================================================== -Creating boxes from error bars using PatchCollection -==================================================== +================================================== +Create boxes from error bars using PatchCollection +================================================== In this example, we snazz up a pretty standard error bar plot by adding a rectangle patch defined by the limits of the bars in both the x- and diff --git a/galleries/examples/statistics/histogram_cumulative.py b/galleries/examples/statistics/histogram_cumulative.py index 9ce16568d126..8a2aaa5a707e 100644 --- a/galleries/examples/statistics/histogram_cumulative.py +++ b/galleries/examples/statistics/histogram_cumulative.py @@ -1,7 +1,7 @@ """ -================================= -Plotting cumulative distributions -================================= +======================== +Cumulative distributions +======================== This example shows how to plot the empirical cumulative distribution function (ECDF) of a sample. We also show the theoretical CDF. diff --git a/galleries/examples/statistics/multiple_histograms_side_by_side.py b/galleries/examples/statistics/multiple_histograms_side_by_side.py index 3c5766f8e546..ecb3623fb437 100644 --- a/galleries/examples/statistics/multiple_histograms_side_by_side.py +++ b/galleries/examples/statistics/multiple_histograms_side_by_side.py @@ -1,7 +1,7 @@ """ -========================================== -Producing multiple histograms side by side -========================================== +================================ +Multiple histograms side by side +================================ This example plots horizontal histograms of different samples along a categorical x-axis. Additionally, the histograms are plotted to diff --git a/galleries/examples/subplots_axes_and_figures/auto_subplots_adjust.py b/galleries/examples/subplots_axes_and_figures/auto_subplots_adjust.py index e0a8c76a0e61..983a47e4e42c 100644 --- a/galleries/examples/subplots_axes_and_figures/auto_subplots_adjust.py +++ b/galleries/examples/subplots_axes_and_figures/auto_subplots_adjust.py @@ -1,7 +1,7 @@ """ -=============================================== -Programmatically controlling subplot adjustment -=============================================== +=========================================== +Programmatically control subplot adjustment +=========================================== .. note:: diff --git a/galleries/examples/subplots_axes_and_figures/axhspan_demo.py b/galleries/examples/subplots_axes_and_figures/axhspan_demo.py index 934345ceca18..788030fcc5f3 100644 --- a/galleries/examples/subplots_axes_and_figures/axhspan_demo.py +++ b/galleries/examples/subplots_axes_and_figures/axhspan_demo.py @@ -1,7 +1,7 @@ """ -================================= -Drawing regions that span an Axes -================================= +============================== +Draw regions that span an Axes +============================== `~.Axes.axhspan` and `~.Axes.axvspan` draw rectangles that span the Axes in either the horizontal or vertical direction and are bounded in the other direction. They are diff --git a/galleries/examples/subplots_axes_and_figures/demo_constrained_layout.py b/galleries/examples/subplots_axes_and_figures/demo_constrained_layout.py index 9a67541e554e..67891cfed611 100644 --- a/galleries/examples/subplots_axes_and_figures/demo_constrained_layout.py +++ b/galleries/examples/subplots_axes_and_figures/demo_constrained_layout.py @@ -1,7 +1,7 @@ """ -===================================== -Resizing Axes with constrained layout -===================================== +=================================== +Resize Axes with constrained layout +=================================== *Constrained layout* attempts to resize subplots in a figure so that there are no overlaps between Axes objects and labels diff --git a/galleries/examples/subplots_axes_and_figures/demo_tight_layout.py b/galleries/examples/subplots_axes_and_figures/demo_tight_layout.py index 7ac3a7376d67..a8d7524697ea 100644 --- a/galleries/examples/subplots_axes_and_figures/demo_tight_layout.py +++ b/galleries/examples/subplots_axes_and_figures/demo_tight_layout.py @@ -1,7 +1,7 @@ """ -=============================== -Resizing Axes with tight layout -=============================== +============================= +Resize Axes with tight layout +============================= `~.Figure.tight_layout` attempts to resize subplots in a figure so that there are no overlaps between Axes objects and labels on the Axes. diff --git a/galleries/examples/subplots_axes_and_figures/ganged_plots.py b/galleries/examples/subplots_axes_and_figures/ganged_plots.py index e25bb16a15e5..d2f50fe2e986 100644 --- a/galleries/examples/subplots_axes_and_figures/ganged_plots.py +++ b/galleries/examples/subplots_axes_and_figures/ganged_plots.py @@ -1,7 +1,7 @@ """ -========================== -Creating adjacent subplots -========================== +================= +Adjacent subplots +================= To create plots that share a common axis (visually) you can set the hspace between the subplots to zero. Passing sharex=True when creating the subplots diff --git a/galleries/examples/subplots_axes_and_figures/gridspec_and_subplots.py b/galleries/examples/subplots_axes_and_figures/gridspec_and_subplots.py index 0535a7afdde4..cfe5b123e897 100644 --- a/galleries/examples/subplots_axes_and_figures/gridspec_and_subplots.py +++ b/galleries/examples/subplots_axes_and_figures/gridspec_and_subplots.py @@ -1,7 +1,7 @@ """ -================================================== -Combining two subplots using subplots and GridSpec -================================================== +================================================ +Combine two subplots using subplots and GridSpec +================================================ Sometimes we want to combine two subplots in an Axes layout created with `~.Figure.subplots`. We can get the `~.gridspec.GridSpec` from the Axes diff --git a/galleries/examples/subplots_axes_and_figures/gridspec_multicolumn.py b/galleries/examples/subplots_axes_and_figures/gridspec_multicolumn.py index 54c3d8fa63cc..a7fa34a10367 100644 --- a/galleries/examples/subplots_axes_and_figures/gridspec_multicolumn.py +++ b/galleries/examples/subplots_axes_and_figures/gridspec_multicolumn.py @@ -1,7 +1,7 @@ """ -======================================================= -Using Gridspec to make multi-column/row subplot layouts -======================================================= +============================================= +Gridspec for multi-column/row subplot layouts +============================================= `.GridSpec` is a flexible way to layout subplot grids. Here is an example with a 3x3 grid, and diff --git a/galleries/examples/subplots_axes_and_figures/multiple_figs_demo.py b/galleries/examples/subplots_axes_and_figures/multiple_figs_demo.py index d6b6a5ed48c6..fe3b2ab191a1 100644 --- a/galleries/examples/subplots_axes_and_figures/multiple_figs_demo.py +++ b/galleries/examples/subplots_axes_and_figures/multiple_figs_demo.py @@ -1,7 +1,7 @@ """ -=================================== -Managing multiple figures in pyplot -=================================== +================================= +Manage multiple figures in pyplot +================================= `matplotlib.pyplot` uses the concept of a *current figure* and *current Axes*. Figures are identified via a figure number that is passed to `~.pyplot.figure`. diff --git a/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py b/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py index 234a15660f2d..f8073b2c3c31 100644 --- a/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py +++ b/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py @@ -1,6 +1,7 @@ """ -Sharing axis limits and views -============================= +=========================== +Share axis limits and views +=========================== It's common to make two or more plots which share an axis, e.g., two subplots with time as a common axis. When you pan and zoom around on one, you want the diff --git a/galleries/examples/subplots_axes_and_figures/subplots_demo.py b/galleries/examples/subplots_axes_and_figures/subplots_demo.py index 229ecd34cc9f..afc71c795365 100644 --- a/galleries/examples/subplots_axes_and_figures/subplots_demo.py +++ b/galleries/examples/subplots_axes_and_figures/subplots_demo.py @@ -1,7 +1,7 @@ """ -================================================= -Creating multiple subplots using ``plt.subplots`` -================================================= +=============================================== +Create multiple subplots using ``plt.subplots`` +=============================================== `.pyplot.subplots` creates a figure and a grid of subplots with a single call, while providing reasonable control over how the individual plots are created. diff --git a/galleries/examples/text_labels_and_annotations/autowrap.py b/galleries/examples/text_labels_and_annotations/autowrap.py index e52dc919ee1b..ea65b0be9992 100644 --- a/galleries/examples/text_labels_and_annotations/autowrap.py +++ b/galleries/examples/text_labels_and_annotations/autowrap.py @@ -1,10 +1,10 @@ """ -================== -Auto-wrapping text -================== +============== +Auto-wrap text +============== -Matplotlib can wrap text automatically, but if it's too long, the text will be -displayed slightly outside of the boundaries of the axis anyways. +Matplotlib can wrap text automatically, but if it's too long, the text will +still be displayed slightly outside the boundaries of the axis. Note: Auto-wrapping does not work together with ``savefig(..., bbox_inches='tight')``. The 'tight' setting rescales the canvas diff --git a/galleries/examples/text_labels_and_annotations/engineering_formatter.py b/galleries/examples/text_labels_and_annotations/engineering_formatter.py index 573552b11a26..372297a81d57 100644 --- a/galleries/examples/text_labels_and_annotations/engineering_formatter.py +++ b/galleries/examples/text_labels_and_annotations/engineering_formatter.py @@ -1,7 +1,7 @@ """ -========================================= -Labeling ticks using engineering notation -========================================= +======================================= +Format ticks using engineering notation +======================================= Use of the engineering Formatter. """ diff --git a/galleries/examples/text_labels_and_annotations/font_family_rc.py b/galleries/examples/text_labels_and_annotations/font_family_rc.py index b3433dc9cdf1..bdf993b76a9e 100644 --- a/galleries/examples/text_labels_and_annotations/font_family_rc.py +++ b/galleries/examples/text_labels_and_annotations/font_family_rc.py @@ -1,7 +1,7 @@ """ -=========================== -Configuring the font family -=========================== +========================= +Configure the font family +========================= You can explicitly set which font family is picked up, either by specifying family names of fonts installed on user's system, or generic-families diff --git a/galleries/examples/text_labels_and_annotations/rainbow_text.py b/galleries/examples/text_labels_and_annotations/rainbow_text.py index 35cedb9bbd0b..4c14f8289cbc 100644 --- a/galleries/examples/text_labels_and_annotations/rainbow_text.py +++ b/galleries/examples/text_labels_and_annotations/rainbow_text.py @@ -1,7 +1,7 @@ """ -==================================================== -Concatenating text objects with different properties -==================================================== +================================================== +Concatenate text objects with different properties +================================================== The example strings together several Text objects with different properties (e.g., color or font), positioning each one after the other. The first Text diff --git a/galleries/examples/text_labels_and_annotations/tex_demo.py b/galleries/examples/text_labels_and_annotations/tex_demo.py index 5eba9a14c2b7..df040c5a866a 100644 --- a/galleries/examples/text_labels_and_annotations/tex_demo.py +++ b/galleries/examples/text_labels_and_annotations/tex_demo.py @@ -1,7 +1,7 @@ """ -================================== -Rendering math equations using TeX -================================== +=============================== +Render math equations using TeX +=============================== You can use TeX to render all of your Matplotlib text by setting :rc:`text.usetex` to True. This requires that you have TeX and the other diff --git a/galleries/examples/text_labels_and_annotations/usetex_fonteffects.py b/galleries/examples/text_labels_and_annotations/usetex_fonteffects.py index a289f3854ed7..ba1c944536cb 100644 --- a/galleries/examples/text_labels_and_annotations/usetex_fonteffects.py +++ b/galleries/examples/text_labels_and_annotations/usetex_fonteffects.py @@ -1,7 +1,7 @@ """ -================== -Usetex Fonteffects -================== +=================== +Usetex font effects +=================== This script demonstrates that font effects specified in your pdftex.map are now supported in usetex mode. diff --git a/galleries/examples/ticks/centered_ticklabels.py b/galleries/examples/ticks/centered_ticklabels.py index ab9e1b56c4e6..c3ccd67b0f5c 100644 --- a/galleries/examples/ticks/centered_ticklabels.py +++ b/galleries/examples/ticks/centered_ticklabels.py @@ -1,7 +1,7 @@ """ -============================== -Centering labels between ticks -============================== +=========================== +Center labels between ticks +=========================== Ticklabels are aligned relative to their associated tick. The alignment 'center', 'left', or 'right' can be controlled using the horizontal alignment diff --git a/galleries/examples/ticks/date_concise_formatter.py b/galleries/examples/ticks/date_concise_formatter.py index 540ebf0e56c1..ce5372aa9547 100644 --- a/galleries/examples/ticks/date_concise_formatter.py +++ b/galleries/examples/ticks/date_concise_formatter.py @@ -1,9 +1,9 @@ """ .. _date_concise_formatter: -================================================ -Formatting date ticks using ConciseDateFormatter -================================================ +============================================ +Format date ticks using ConciseDateFormatter +============================================ Finding good tick values and formatting the ticks for an axis that has date data is often a challenge. `~.dates.ConciseDateFormatter` is diff --git a/galleries/examples/ticks/ticklabels_rotation.py b/galleries/examples/ticks/ticklabels_rotation.py index 5e21b9a352f0..d337ca827cde 100644 --- a/galleries/examples/ticks/ticklabels_rotation.py +++ b/galleries/examples/ticks/ticklabels_rotation.py @@ -1,7 +1,7 @@ """ -==================== -Rotating tick labels -==================== +=================== +Rotated tick labels +=================== """ import matplotlib.pyplot as plt diff --git a/galleries/examples/units/basic_units.py b/galleries/examples/units/basic_units.py index d6f788c20fd9..3f64d145b65e 100644 --- a/galleries/examples/units/basic_units.py +++ b/galleries/examples/units/basic_units.py @@ -2,7 +2,7 @@ .. _basic_units: =========== -Basic Units +Basic units =========== """ diff --git a/galleries/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py b/galleries/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py index f6892a849a88..7c3b04041009 100644 --- a/galleries/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py @@ -1,7 +1,7 @@ """ -=========================================== -Embedding in GTK3 with a navigation toolbar -=========================================== +======================================= +Embed in GTK3 with a navigation toolbar +======================================= Demonstrate NavigationToolbar with GTK3 accessed via pygobject. """ @@ -22,7 +22,7 @@ win = Gtk.Window() win.connect("delete-event", Gtk.main_quit) win.set_default_size(400, 300) -win.set_title("Embedding in GTK3") +win.set_title("Embedded in GTK3") fig = Figure(figsize=(5, 4), dpi=100) ax = fig.add_subplot(1, 1, 1) diff --git a/galleries/examples/user_interfaces/embedding_in_gtk3_sgskip.py b/galleries/examples/user_interfaces/embedding_in_gtk3_sgskip.py index 170a88a58aff..51ceebb501e3 100644 --- a/galleries/examples/user_interfaces/embedding_in_gtk3_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_gtk3_sgskip.py @@ -1,7 +1,7 @@ """ -================= -Embedding in GTK3 -================= +============= +Embed in GTK3 +============= Demonstrate adding a FigureCanvasGTK3Agg widget to a Gtk.ScrolledWindow using GTK3 accessed via pygobject. @@ -21,7 +21,7 @@ win = Gtk.Window() win.connect("delete-event", Gtk.main_quit) win.set_default_size(400, 300) -win.set_title("Embedding in GTK3") +win.set_title("Embedded in GTK3") fig = Figure(figsize=(5, 4), dpi=100) ax = fig.add_subplot() diff --git a/galleries/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py b/galleries/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py index 3e8568091236..e42e59459198 100644 --- a/galleries/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py @@ -1,7 +1,7 @@ """ -=========================================== -Embedding in GTK4 with a navigation toolbar -=========================================== +======================================= +Embed in GTK4 with a navigation toolbar +======================================= Demonstrate NavigationToolbar with GTK4 accessed via pygobject. """ @@ -23,7 +23,7 @@ def on_activate(app): win = Gtk.ApplicationWindow(application=app) win.set_default_size(400, 300) - win.set_title("Embedding in GTK4") + win.set_title("Embedded in GTK4") fig = Figure(figsize=(5, 4), dpi=100) ax = fig.add_subplot(1, 1, 1) diff --git a/galleries/examples/user_interfaces/embedding_in_gtk4_sgskip.py b/galleries/examples/user_interfaces/embedding_in_gtk4_sgskip.py index 0e17473de5d3..197cd7971088 100644 --- a/galleries/examples/user_interfaces/embedding_in_gtk4_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_gtk4_sgskip.py @@ -1,7 +1,7 @@ """ -================= -Embedding in GTK4 -================= +============= +Embed in GTK4 +============= Demonstrate adding a FigureCanvasGTK4Agg widget to a Gtk.ScrolledWindow using GTK4 accessed via pygobject. @@ -22,7 +22,7 @@ def on_activate(app): win = Gtk.ApplicationWindow(application=app) win.set_default_size(400, 300) - win.set_title("Embedding in GTK4") + win.set_title("Embedded in GTK4") fig = Figure(figsize=(5, 4), dpi=100) ax = fig.add_subplot() diff --git a/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py b/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py index b79f582a65e4..f061efc79ec2 100644 --- a/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py @@ -1,7 +1,7 @@ """ -=============== -Embedding in Qt -=============== +=========== +Embed in Qt +=========== Simple Qt application embedding Matplotlib canvases. This program will work equally well using any Qt binding (PyQt6, PySide6, PyQt5, PySide2). The diff --git a/galleries/examples/user_interfaces/embedding_in_tk_sgskip.py b/galleries/examples/user_interfaces/embedding_in_tk_sgskip.py index e5c4aa4125b6..7474f40b4bac 100644 --- a/galleries/examples/user_interfaces/embedding_in_tk_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_tk_sgskip.py @@ -1,7 +1,7 @@ """ -=============== -Embedding in Tk -=============== +=========== +Embed in Tk +=========== """ @@ -16,7 +16,7 @@ from matplotlib.figure import Figure root = tkinter.Tk() -root.wm_title("Embedding in Tk") +root.wm_title("Embedded in Tk") fig = Figure(figsize=(5, 4), dpi=100) t = np.arange(0, 3, .01) diff --git a/galleries/examples/user_interfaces/embedding_in_wx2_sgskip.py b/galleries/examples/user_interfaces/embedding_in_wx2_sgskip.py index acfe1bc9d98f..634d8c511aa7 100644 --- a/galleries/examples/user_interfaces/embedding_in_wx2_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_wx2_sgskip.py @@ -1,7 +1,7 @@ """ -================== -Embedding in wx #2 -================== +============== +Embed in wx #2 +============== An example of how to use wxagg in an application with the new toolbar - comment out the add_toolbar line for no toolbar. diff --git a/galleries/examples/user_interfaces/embedding_in_wx3_sgskip.py b/galleries/examples/user_interfaces/embedding_in_wx3_sgskip.py index 40282699d872..ac1213be0576 100644 --- a/galleries/examples/user_interfaces/embedding_in_wx3_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_wx3_sgskip.py @@ -1,7 +1,7 @@ """ -================== -Embedding in wx #3 -================== +============== +Embed in wx #3 +============== Copyright (C) 2003-2004 Andrew Straw, Jeremy O'Donoghue and others diff --git a/galleries/examples/user_interfaces/embedding_in_wx4_sgskip.py b/galleries/examples/user_interfaces/embedding_in_wx4_sgskip.py index b9504ff25dee..062f1219adb5 100644 --- a/galleries/examples/user_interfaces/embedding_in_wx4_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_wx4_sgskip.py @@ -1,7 +1,7 @@ """ -================== -Embedding in wx #4 -================== +============== +Embed in wx #4 +============== An example of how to use wxagg in a wx application with a custom toolbar. """ diff --git a/galleries/examples/user_interfaces/embedding_in_wx5_sgskip.py b/galleries/examples/user_interfaces/embedding_in_wx5_sgskip.py index 80062782d9fa..f150e2106ead 100644 --- a/galleries/examples/user_interfaces/embedding_in_wx5_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_wx5_sgskip.py @@ -1,7 +1,7 @@ """ -================== -Embedding in wx #5 -================== +============== +Embed in wx #5 +============== """ diff --git a/galleries/examples/user_interfaces/web_application_server_sgskip.py b/galleries/examples/user_interfaces/web_application_server_sgskip.py index 950109015191..60c321e02eb9 100644 --- a/galleries/examples/user_interfaces/web_application_server_sgskip.py +++ b/galleries/examples/user_interfaces/web_application_server_sgskip.py @@ -1,7 +1,7 @@ """ -============================================= -Embedding in a web application server (Flask) -============================================= +========================================= +Embed in a web application server (Flask) +========================================= When using Matplotlib in a web server it is strongly recommended to not use pyplot (pyplot maintains references to the opened figures to make diff --git a/galleries/examples/user_interfaces/wxcursor_demo_sgskip.py b/galleries/examples/user_interfaces/wxcursor_demo_sgskip.py index 96c6d760dc5d..e2e7348f1c3c 100644 --- a/galleries/examples/user_interfaces/wxcursor_demo_sgskip.py +++ b/galleries/examples/user_interfaces/wxcursor_demo_sgskip.py @@ -1,7 +1,7 @@ """ -===================== -Adding a cursor in WX -===================== +================== +Add a cursor in WX +================== Example to draw a cursor and report the data coords in wx. """ diff --git a/galleries/examples/widgets/slider_snap_demo.py b/galleries/examples/widgets/slider_snap_demo.py index 23910415da8f..953ffaf63672 100644 --- a/galleries/examples/widgets/slider_snap_demo.py +++ b/galleries/examples/widgets/slider_snap_demo.py @@ -1,7 +1,7 @@ """ -=================================== -Snapping Sliders to Discrete Values -=================================== +=============================== +Snap sliders to discrete values +=============================== You can snap slider values to discrete values using the ``valstep`` argument. From 6f1bf3385103cc40f467688e675222451a571205 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sat, 3 Aug 2024 01:03:27 +0200 Subject: [PATCH 0108/1230] DOC: Remove long uninstructive examples - [Hatch filled histogram](https://matplotlib.org/stable/gallery/lines_bars_and_markers/filled_step.html) Hatching in histograms is fully supported through #28073. This is [now simple](https://matplotlib.org/devdocs/gallery/statistics/histogram_multihist.html#hatch) and does warrant a dedicated example. - [Percentiles in horizontal bar charts](https://matplotlib.org/stable/gallery/statistics/barchart_demo.html) This is a very complex example. But in the end, it's just a bar plot with annotations. While it is statistics-themed through the use of perecntiles, the plot itself has no statistics-related characteristics. --- doc/users/prev_whats_new/whats_new_1.5.rst | 5 - .../lines_bars_and_markers/filled_step.py | 237 ------------------ .../examples/statistics/barchart_demo.py | 111 -------- .../statistics/histogram_multihist.py | 3 + 4 files changed, 3 insertions(+), 353 deletions(-) delete mode 100644 galleries/examples/lines_bars_and_markers/filled_step.py delete mode 100644 galleries/examples/statistics/barchart_demo.py diff --git a/doc/users/prev_whats_new/whats_new_1.5.rst b/doc/users/prev_whats_new/whats_new_1.5.rst index dd8e204aa957..039f65e2eba6 100644 --- a/doc/users/prev_whats_new/whats_new_1.5.rst +++ b/doc/users/prev_whats_new/whats_new_1.5.rst @@ -368,11 +368,6 @@ kwargs names is not ideal, but `.Axes.fill_between` already has a This is particularly useful for plotting pre-binned histograms. -.. figure:: ../../gallery/lines_bars_and_markers/images/sphx_glr_filled_step_001.png - :target: ../../gallery/lines_bars_and_markers/filled_step.html - :align: center - :scale: 50 - Square Plot ``````````` diff --git a/galleries/examples/lines_bars_and_markers/filled_step.py b/galleries/examples/lines_bars_and_markers/filled_step.py deleted file mode 100644 index 65a7d31a425a..000000000000 --- a/galleries/examples/lines_bars_and_markers/filled_step.py +++ /dev/null @@ -1,237 +0,0 @@ -""" -========================= -Hatch-filled histograms -========================= - -Hatching capabilities for plotting histograms. -""" - -from functools import partial -import itertools - -from cycler import cycler - -import matplotlib.pyplot as plt -import numpy as np - -import matplotlib.ticker as mticker - - -def filled_hist(ax, edges, values, bottoms=None, orientation='v', - **kwargs): - """ - Draw a histogram as a stepped patch. - - Parameters - ---------- - ax : Axes - The Axes to plot to. - - edges : array - A length n+1 array giving the left edges of each bin and the - right edge of the last bin. - - values : array - A length n array of bin counts or values - - bottoms : float or array, optional - A length n array of the bottom of the bars. If None, zero is used. - - orientation : {'v', 'h'} - Orientation of the histogram. 'v' (default) has - the bars increasing in the positive y-direction. - - **kwargs - Extra keyword arguments are passed through to `.fill_between`. - - Returns - ------- - ret : PolyCollection - Artist added to the Axes - """ - print(orientation) - if orientation not in 'hv': - raise ValueError(f"orientation must be in {{'h', 'v'}} " - f"not {orientation}") - - kwargs.setdefault('step', 'post') - kwargs.setdefault('alpha', 0.7) - edges = np.asarray(edges) - values = np.asarray(values) - if len(edges) - 1 != len(values): - raise ValueError(f'Must provide one more bin edge than value not: ' - f'{len(edges)=} {len(values)=}') - - if bottoms is None: - bottoms = 0 - bottoms = np.broadcast_to(bottoms, values.shape) - - values = np.append(values, values[-1]) - bottoms = np.append(bottoms, bottoms[-1]) - if orientation == 'h': - return ax.fill_betweenx(edges, values, bottoms, - **kwargs) - elif orientation == 'v': - return ax.fill_between(edges, values, bottoms, - **kwargs) - else: - raise AssertionError("you should never be here") - - -def stack_hist(ax, stacked_data, sty_cycle, bottoms=None, - hist_func=None, labels=None, - plot_func=None, plot_kwargs=None): - """ - Parameters - ---------- - ax : axes.Axes - The Axes to add artists to. - - stacked_data : array or Mapping - A (M, N) shaped array. The first dimension will be iterated over to - compute histograms row-wise - - sty_cycle : Cycler or operable of dict - Style to apply to each set - - bottoms : array, default: 0 - The initial positions of the bottoms. - - hist_func : callable, optional - Must have signature `bin_vals, bin_edges = f(data)`. - `bin_edges` expected to be one longer than `bin_vals` - - labels : list of str, optional - The label for each set. - - If not given and stacked data is an array defaults to 'default set {n}' - - If *stacked_data* is a mapping, and *labels* is None, default to the - keys. - - If *stacked_data* is a mapping and *labels* is given then only the - columns listed will be plotted. - - plot_func : callable, optional - Function to call to draw the histogram must have signature: - - ret = plot_func(ax, edges, top, bottoms=bottoms, - label=label, **kwargs) - - plot_kwargs : dict, optional - Any extra keyword arguments to pass through to the plotting function. - This will be the same for all calls to the plotting function and will - override the values in *sty_cycle*. - - Returns - ------- - arts : dict - Dictionary of artists keyed on their labels - """ - # deal with default binning function - if hist_func is None: - hist_func = np.histogram - - # deal with default plotting function - if plot_func is None: - plot_func = filled_hist - - # deal with default - if plot_kwargs is None: - plot_kwargs = {} - print(plot_kwargs) - try: - l_keys = stacked_data.keys() - label_data = True - if labels is None: - labels = l_keys - - except AttributeError: - label_data = False - if labels is None: - labels = itertools.repeat(None) - - if label_data: - loop_iter = enumerate((stacked_data[lab], lab, s) - for lab, s in zip(labels, sty_cycle)) - else: - loop_iter = enumerate(zip(stacked_data, labels, sty_cycle)) - - arts = {} - for j, (data, label, sty) in loop_iter: - if label is None: - label = f'dflt set {j}' - label = sty.pop('label', label) - vals, edges = hist_func(data) - if bottoms is None: - bottoms = np.zeros_like(vals) - top = bottoms + vals - print(sty) - sty.update(plot_kwargs) - print(sty) - ret = plot_func(ax, edges, top, bottoms=bottoms, - label=label, **sty) - bottoms = top - arts[label] = ret - ax.legend(fontsize=10) - return arts - - -# set up histogram function to fixed bins -edges = np.linspace(-3, 3, 20, endpoint=True) -hist_func = partial(np.histogram, bins=edges) - -# set up style cycles -color_cycle = cycler(facecolor=plt.rcParams['axes.prop_cycle'][:4]) -label_cycle = cycler(label=[f'set {n}' for n in range(4)]) -hatch_cycle = cycler(hatch=['/', '*', '+', '|']) - -# Fixing random state for reproducibility -np.random.seed(19680801) - -stack_data = np.random.randn(4, 12250) -dict_data = dict(zip((c['label'] for c in label_cycle), stack_data)) - -# %% -# Work with plain arrays - -fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4.5), tight_layout=True) -arts = stack_hist(ax1, stack_data, color_cycle + label_cycle + hatch_cycle, - hist_func=hist_func) - -arts = stack_hist(ax2, stack_data, color_cycle, - hist_func=hist_func, - plot_kwargs=dict(edgecolor='w', orientation='h')) -ax1.set_ylabel('counts') -ax1.set_xlabel('x') -ax2.set_xlabel('counts') -ax2.set_ylabel('x') - -# %% -# Work with labeled data - -fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4.5), - tight_layout=True, sharey=True) - -arts = stack_hist(ax1, dict_data, color_cycle + hatch_cycle, - hist_func=hist_func) - -arts = stack_hist(ax2, dict_data, color_cycle + hatch_cycle, - hist_func=hist_func, labels=['set 0', 'set 3']) -ax1.xaxis.set_major_locator(mticker.MaxNLocator(5)) -ax1.set_xlabel('counts') -ax1.set_ylabel('x') -ax2.set_ylabel('x') - -plt.show() - -# %% -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.axes.Axes.fill_betweenx` / `matplotlib.pyplot.fill_betweenx` -# - `matplotlib.axes.Axes.fill_between` / `matplotlib.pyplot.fill_between` -# - `matplotlib.axis.Axis.set_major_locator` diff --git a/galleries/examples/statistics/barchart_demo.py b/galleries/examples/statistics/barchart_demo.py deleted file mode 100644 index ad33f442b844..000000000000 --- a/galleries/examples/statistics/barchart_demo.py +++ /dev/null @@ -1,111 +0,0 @@ -""" -=================================== -Percentiles as horizontal bar chart -=================================== - -Bar charts are useful for visualizing counts, or summary statistics -with error bars. Also see the :doc:`/gallery/lines_bars_and_markers/barchart` -or the :doc:`/gallery/lines_bars_and_markers/barh` example for simpler versions -of those features. - -This example comes from an application in which grade school gym -teachers wanted to be able to show parents how their child did across -a handful of fitness tests, and importantly, relative to how other -children did. To extract the plotting code for demo purposes, we'll -just make up some data for little Johnny Doe. -""" - -from collections import namedtuple - -import matplotlib.pyplot as plt -import numpy as np - -Student = namedtuple('Student', ['name', 'grade', 'gender']) -Score = namedtuple('Score', ['value', 'unit', 'percentile']) - - -def to_ordinal(num): - """Convert an integer to an ordinal string, e.g. 2 -> '2nd'.""" - suffixes = {str(i): v - for i, v in enumerate(['th', 'st', 'nd', 'rd', 'th', - 'th', 'th', 'th', 'th', 'th'])} - v = str(num) - # special case early teens - if v in {'11', '12', '13'}: - return v + 'th' - return v + suffixes[v[-1]] - - -def format_score(score): - """ - Create score labels for the right y-axis as the test name followed by the - measurement unit (if any), split over two lines. - """ - return f'{score.value}\n{score.unit}' if score.unit else str(score.value) - - -def plot_student_results(student, scores_by_test, cohort_size): - fig, ax1 = plt.subplots(figsize=(9, 7), layout='constrained') - fig.canvas.manager.set_window_title('Eldorado K-8 Fitness Chart') - - ax1.set_title(student.name) - ax1.set_xlabel( - 'Percentile Ranking Across {grade} Grade {gender}s\n' - 'Cohort Size: {cohort_size}'.format( - grade=to_ordinal(student.grade), - gender=student.gender.title(), - cohort_size=cohort_size)) - - test_names = list(scores_by_test.keys()) - percentiles = [score.percentile for score in scores_by_test.values()] - - rects = ax1.barh(test_names, percentiles, align='center', height=0.5) - # Partition the percentile values to be able to draw large numbers in - # white within the bar, and small numbers in black outside the bar. - large_percentiles = [to_ordinal(p) if p > 40 else '' for p in percentiles] - small_percentiles = [to_ordinal(p) if p <= 40 else '' for p in percentiles] - ax1.bar_label(rects, small_percentiles, - padding=5, color='black', fontweight='bold') - ax1.bar_label(rects, large_percentiles, - padding=-32, color='white', fontweight='bold') - - ax1.set_xlim([0, 100]) - ax1.set_xticks([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]) - ax1.xaxis.grid(True, linestyle='--', which='major', - color='grey', alpha=.25) - ax1.axvline(50, color='grey', alpha=0.25) # median position - - # Set the right-hand Y-axis ticks and labels - ax2 = ax1.twinx() - # Set equal limits on both yaxis so that the ticks line up - ax2.set_ylim(ax1.get_ylim()) - # Set the tick locations and labels - ax2.set_yticks( - np.arange(len(scores_by_test)), - labels=[format_score(score) for score in scores_by_test.values()]) - - ax2.set_ylabel('Test Scores') - - -student = Student(name='Johnny Doe', grade=2, gender='Boy') -scores_by_test = { - 'Pacer Test': Score(7, 'laps', percentile=37), - 'Flexed Arm\n Hang': Score(48, 'sec', percentile=95), - 'Mile Run': Score('12:52', 'min:sec', percentile=73), - 'Agility': Score(17, 'sec', percentile=60), - 'Push Ups': Score(14, '', percentile=16), -} - -plot_student_results(student, scores_by_test, cohort_size=62) -plt.show() - -# %% -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` -# - `matplotlib.axes.Axes.bar_label` / `matplotlib.pyplot.bar_label` -# - `matplotlib.axes.Axes.twinx` / `matplotlib.pyplot.twinx` diff --git a/galleries/examples/statistics/histogram_multihist.py b/galleries/examples/statistics/histogram_multihist.py index b9a9c5f0bf26..62b3c4178ced 100644 --- a/galleries/examples/statistics/histogram_multihist.py +++ b/galleries/examples/statistics/histogram_multihist.py @@ -14,6 +14,9 @@ shape of a histogram. The Astropy docs have a great section on how to select these parameters: http://docs.astropy.org/en/stable/visualization/histogram.html + +.. redirect-from:: /gallery/lines_bars_and_markers/filled_step + """ # %% import matplotlib.pyplot as plt From 627d71dc334088aedec88238d2e8b587841512d9 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 2 Aug 2024 19:22:28 -0400 Subject: [PATCH 0109/1230] DOC: Inline pgf userdemo examples They are not too useful as examples, since they need to save `.pgf` files, which are not scraped nor exposed in any way from the generated docs. --- galleries/examples/userdemo/pgf_fonts.py | 30 -------- .../examples/userdemo/pgf_preamble_sgskip.py | 34 -------- galleries/examples/userdemo/pgf_texsystem.py | 30 -------- galleries/users_explain/text/pgf.py | 77 ++++++++++++++++--- 4 files changed, 67 insertions(+), 104 deletions(-) delete mode 100644 galleries/examples/userdemo/pgf_fonts.py delete mode 100644 galleries/examples/userdemo/pgf_preamble_sgskip.py delete mode 100644 galleries/examples/userdemo/pgf_texsystem.py diff --git a/galleries/examples/userdemo/pgf_fonts.py b/galleries/examples/userdemo/pgf_fonts.py deleted file mode 100644 index 9d5f5594b81b..000000000000 --- a/galleries/examples/userdemo/pgf_fonts.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -========= -PGF fonts -========= -""" - -import matplotlib.pyplot as plt - -plt.rcParams.update({ - "font.family": "serif", - # Use LaTeX default serif font. - "font.serif": [], - # Use specific cursive fonts. - "font.cursive": ["Comic Neue", "Comic Sans MS"], -}) - -fig, ax = plt.subplots(figsize=(4.5, 2.5)) - -ax.plot(range(5)) - -ax.text(0.5, 3., "serif") -ax.text(0.5, 2., "monospace", family="monospace") -ax.text(2.5, 2., "sans-serif", family="DejaVu Sans") # Use specific sans font. -ax.text(2.5, 1., "comic", family="cursive") -ax.set_xlabel("µ is not $\\mu$") - -fig.tight_layout(pad=.5) - -fig.savefig("pgf_fonts.pdf") -fig.savefig("pgf_fonts.png") diff --git a/galleries/examples/userdemo/pgf_preamble_sgskip.py b/galleries/examples/userdemo/pgf_preamble_sgskip.py deleted file mode 100644 index b32fa972c31f..000000000000 --- a/galleries/examples/userdemo/pgf_preamble_sgskip.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -============ -PGF preamble -============ -""" - -import matplotlib as mpl - -mpl.use("pgf") -import matplotlib.pyplot as plt - -plt.rcParams.update({ - "font.family": "serif", # use serif/main font for text elements - "text.usetex": True, # use inline math for ticks - "pgf.rcfonts": False, # don't setup fonts from rc parameters - "pgf.preamble": "\n".join([ - r"\usepackage{url}", # load additional packages - r"\usepackage{unicode-math}", # unicode math setup - r"\setmainfont{DejaVu Serif}", # serif font via preamble - ]) -}) - -fig, ax = plt.subplots(figsize=(4.5, 2.5)) - -ax.plot(range(5)) - -ax.set_xlabel("unicode text: я, ψ, €, ü") -ax.set_ylabel(r"\url{https://matplotlib.org}") -ax.legend(["unicode math: $λ=∑_i^∞ μ_i^2$"]) - -fig.tight_layout(pad=.5) - -fig.savefig("pgf_preamble.pdf") -fig.savefig("pgf_preamble.png") diff --git a/galleries/examples/userdemo/pgf_texsystem.py b/galleries/examples/userdemo/pgf_texsystem.py deleted file mode 100644 index 0d8e326803ea..000000000000 --- a/galleries/examples/userdemo/pgf_texsystem.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -============= -PGF texsystem -============= -""" - -import matplotlib.pyplot as plt - -plt.rcParams.update({ - "pgf.texsystem": "pdflatex", - "pgf.preamble": "\n".join([ - r"\usepackage[utf8x]{inputenc}", - r"\usepackage[T1]{fontenc}", - r"\usepackage{cmbright}", - ]), -}) - -fig, ax = plt.subplots(figsize=(4.5, 2.5)) - -ax.plot(range(5)) - -ax.text(0.5, 3., "serif", family="serif") -ax.text(0.5, 2., "monospace", family="monospace") -ax.text(2.5, 2., "sans-serif", family="sans-serif") -ax.set_xlabel(r"µ is not $\mu$") - -fig.tight_layout(pad=.5) - -fig.savefig("pgf_texsystem.pdf") -fig.savefig("pgf_texsystem.png") diff --git a/galleries/users_explain/text/pgf.py b/galleries/users_explain/text/pgf.py index 8683101032b5..fd7693cf55e3 100644 --- a/galleries/users_explain/text/pgf.py +++ b/galleries/users_explain/text/pgf.py @@ -91,6 +91,8 @@ pdf.savefig(fig2) +.. redirect-from:: /gallery/userdemo/pgf_fonts + Font specification ================== @@ -107,9 +109,29 @@ When saving to ``.pgf``, the font configuration Matplotlib used for the layout of the figure is included in the header of the text file. -.. literalinclude:: /gallery/userdemo/pgf_fonts.py - :end-before: fig.savefig +.. code-block:: python + + import matplotlib.pyplot as plt + + plt.rcParams.update({ + "font.family": "serif", + # Use LaTeX default serif font. + "font.serif": [], + # Use specific cursive fonts. + "font.cursive": ["Comic Neue", "Comic Sans MS"], + }) + fig, ax = plt.subplots(figsize=(4.5, 2.5)) + + ax.plot(range(5)) + + ax.text(0.5, 3., "serif") + ax.text(0.5, 2., "monospace", family="monospace") + ax.text(2.5, 2., "sans-serif", family="DejaVu Sans") # Use specific sans font. + ax.text(2.5, 1., "comic", family="cursive") + ax.set_xlabel("µ is not $\\mu$") + +.. redirect-from:: /gallery/userdemo/pgf_preamble_sgskip .. _pgf-preamble: @@ -122,16 +144,33 @@ if you want to do the font configuration yourself instead of using the fonts specified in the rc parameters, make sure to disable :rc:`pgf.rcfonts`. -.. only:: html +.. code-block:: python - .. literalinclude:: /gallery/userdemo/pgf_preamble_sgskip.py - :end-before: fig.savefig + import matplotlib as mpl -.. only:: latex + mpl.use("pgf") + import matplotlib.pyplot as plt - .. literalinclude:: /gallery/userdemo/pgf_preamble_sgskip.py - :end-before: import matplotlib.pyplot as plt + plt.rcParams.update({ + "font.family": "serif", # use serif/main font for text elements + "text.usetex": True, # use inline math for ticks + "pgf.rcfonts": False, # don't setup fonts from rc parameters + "pgf.preamble": "\n".join([ + r"\usepackage{url}", # load additional packages + r"\usepackage{unicode-math}", # unicode math setup + r"\setmainfont{DejaVu Serif}", # serif font via preamble + ]) + }) + fig, ax = plt.subplots(figsize=(4.5, 2.5)) + + ax.plot(range(5)) + + ax.set_xlabel("unicode text: я, ψ, €, ü") + ax.set_ylabel(r"\url{https://matplotlib.org}") + ax.legend(["unicode math: $λ=∑_i^∞ μ_i^2$"]) + +.. redirect-from:: /gallery/userdemo/pgf_texsystem .. _pgf-texsystem: @@ -143,9 +182,27 @@ Please note that when selecting pdflatex, the fonts and Unicode handling must be configured in the preamble. -.. literalinclude:: /gallery/userdemo/pgf_texsystem.py - :end-before: fig.savefig +.. code-block:: python + + import matplotlib.pyplot as plt + + plt.rcParams.update({ + "pgf.texsystem": "pdflatex", + "pgf.preamble": "\n".join([ + r"\usepackage[utf8x]{inputenc}", + r"\usepackage[T1]{fontenc}", + r"\usepackage{cmbright}", + ]), + }) + + fig, ax = plt.subplots(figsize=(4.5, 2.5)) + + ax.plot(range(5)) + ax.text(0.5, 3., "serif", family="serif") + ax.text(0.5, 2., "monospace", family="monospace") + ax.text(2.5, 2., "sans-serif", family="sans-serif") + ax.set_xlabel(r"µ is not $\mu$") .. _pgf-troubleshooting: From d8db2837e57a4e881bcddb5d8c0dff7f9c01acaa Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 3 Aug 2024 03:28:22 -0400 Subject: [PATCH 0110/1230] DOC: Inline annotation user demos `annotate_explain.py` and `connectionstyle_demo.py` can be inlined where they are used in the Annotations tutorial. `custom_boxstyle01.py` is half written in the "Defining custom box styles" section, so just insert the second half. `annotate_text_arrow.py` is better explained in the "Annotating with boxed text" section of the Annotations tutorial. `simple_annotate01.py` repeats all of the tutorial, but without any explanatory text. --- .../examples/userdemo/annotate_explain.py | 83 --------- .../examples/userdemo/annotate_text_arrow.py | 43 ----- .../examples/userdemo/connectionstyle_demo.py | 63 ------- .../examples/userdemo/custom_boxstyle01.py | 128 ------------- .../examples/userdemo/simple_annotate01.py | 90 --------- galleries/users_explain/text/annotations.py | 171 ++++++++++++++++-- 6 files changed, 156 insertions(+), 422 deletions(-) delete mode 100644 galleries/examples/userdemo/annotate_explain.py delete mode 100644 galleries/examples/userdemo/annotate_text_arrow.py delete mode 100644 galleries/examples/userdemo/connectionstyle_demo.py delete mode 100644 galleries/examples/userdemo/custom_boxstyle01.py delete mode 100644 galleries/examples/userdemo/simple_annotate01.py diff --git a/galleries/examples/userdemo/annotate_explain.py b/galleries/examples/userdemo/annotate_explain.py deleted file mode 100644 index 8f20b5406bd7..000000000000 --- a/galleries/examples/userdemo/annotate_explain.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -================ -Annotate Explain -================ - -""" - -import matplotlib.pyplot as plt - -import matplotlib.patches as mpatches - -fig, axs = plt.subplots(2, 2) -x1, y1 = 0.3, 0.3 -x2, y2 = 0.7, 0.7 - -ax = axs.flat[0] -ax.plot([x1, x2], [y1, y2], ".") -el = mpatches.Ellipse((x1, y1), 0.3, 0.4, angle=30, alpha=0.2) -ax.add_artist(el) -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="-", - color="0.5", - patchB=None, - shrinkB=0, - connectionstyle="arc3,rad=0.3", - ), - ) -ax.text(.05, .95, "connect", transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[1] -ax.plot([x1, x2], [y1, y2], ".") -el = mpatches.Ellipse((x1, y1), 0.3, 0.4, angle=30, alpha=0.2) -ax.add_artist(el) -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="-", - color="0.5", - patchB=el, - shrinkB=0, - connectionstyle="arc3,rad=0.3", - ), - ) -ax.text(.05, .95, "clip", transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[2] -ax.plot([x1, x2], [y1, y2], ".") -el = mpatches.Ellipse((x1, y1), 0.3, 0.4, angle=30, alpha=0.2) -ax.add_artist(el) -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="-", - color="0.5", - patchB=el, - shrinkB=5, - connectionstyle="arc3,rad=0.3", - ), - ) -ax.text(.05, .95, "shrink", transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[3] -ax.plot([x1, x2], [y1, y2], ".") -el = mpatches.Ellipse((x1, y1), 0.3, 0.4, angle=30, alpha=0.2) -ax.add_artist(el) -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="fancy", - color="0.5", - patchB=el, - shrinkB=5, - connectionstyle="arc3,rad=0.3", - ), - ) -ax.text(.05, .95, "mutate", transform=ax.transAxes, ha="left", va="top") - -for ax in axs.flat: - ax.set(xlim=(0, 1), ylim=(0, 1), xticks=[], yticks=[], aspect=1) - -plt.show() diff --git a/galleries/examples/userdemo/annotate_text_arrow.py b/galleries/examples/userdemo/annotate_text_arrow.py deleted file mode 100644 index 2495c7687bd7..000000000000 --- a/galleries/examples/userdemo/annotate_text_arrow.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -=================== -Annotate Text Arrow -=================== - -""" - -import matplotlib.pyplot as plt -import numpy as np - -# Fixing random state for reproducibility -np.random.seed(19680801) - -fig, ax = plt.subplots(figsize=(5, 5)) -ax.set_aspect(1) - -x1 = -1 + np.random.randn(100) -y1 = -1 + np.random.randn(100) -x2 = 1. + np.random.randn(100) -y2 = 1. + np.random.randn(100) - -ax.scatter(x1, y1, color="r") -ax.scatter(x2, y2, color="g") - -bbox_props = dict(boxstyle="round", fc="w", ec="0.5", alpha=0.9) -ax.text(-2, -2, "Sample A", ha="center", va="center", size=20, - bbox=bbox_props) -ax.text(2, 2, "Sample B", ha="center", va="center", size=20, - bbox=bbox_props) - - -bbox_props = dict(boxstyle="rarrow", fc=(0.8, 0.9, 0.9), ec="b", lw=2) -t = ax.text(0, 0, "Direction", ha="center", va="center", rotation=45, - size=15, - bbox=bbox_props) - -bb = t.get_bbox_patch() -bb.set_boxstyle("rarrow", pad=0.6) - -ax.set_xlim(-4, 4) -ax.set_ylim(-4, 4) - -plt.show() diff --git a/galleries/examples/userdemo/connectionstyle_demo.py b/galleries/examples/userdemo/connectionstyle_demo.py deleted file mode 100644 index e34c63a5708b..000000000000 --- a/galleries/examples/userdemo/connectionstyle_demo.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -================================= -Connection styles for annotations -================================= - -When creating an annotation using `~.Axes.annotate`, the arrow shape can be -controlled via the *connectionstyle* parameter of *arrowprops*. For further -details see the description of `.FancyArrowPatch`. -""" - -import matplotlib.pyplot as plt - - -def demo_con_style(ax, connectionstyle): - x1, y1 = 0.3, 0.2 - x2, y2 = 0.8, 0.6 - - ax.plot([x1, x2], [y1, y2], ".") - ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="->", color="0.5", - shrinkA=5, shrinkB=5, - patchA=None, patchB=None, - connectionstyle=connectionstyle, - ), - ) - - ax.text(.05, .95, connectionstyle.replace(",", ",\n"), - transform=ax.transAxes, ha="left", va="top") - - -fig, axs = plt.subplots(3, 5, figsize=(7, 6.3), layout="constrained") -demo_con_style(axs[0, 0], "angle3,angleA=90,angleB=0") -demo_con_style(axs[1, 0], "angle3,angleA=0,angleB=90") -demo_con_style(axs[0, 1], "arc3,rad=0.") -demo_con_style(axs[1, 1], "arc3,rad=0.3") -demo_con_style(axs[2, 1], "arc3,rad=-0.3") -demo_con_style(axs[0, 2], "angle,angleA=-90,angleB=180,rad=0") -demo_con_style(axs[1, 2], "angle,angleA=-90,angleB=180,rad=5") -demo_con_style(axs[2, 2], "angle,angleA=-90,angleB=10,rad=5") -demo_con_style(axs[0, 3], "arc,angleA=-90,angleB=0,armA=30,armB=30,rad=0") -demo_con_style(axs[1, 3], "arc,angleA=-90,angleB=0,armA=30,armB=30,rad=5") -demo_con_style(axs[2, 3], "arc,angleA=-90,angleB=0,armA=0,armB=40,rad=0") -demo_con_style(axs[0, 4], "bar,fraction=0.3") -demo_con_style(axs[1, 4], "bar,fraction=-0.3") -demo_con_style(axs[2, 4], "bar,angle=180,fraction=-0.2") - -for ax in axs.flat: - ax.set(xlim=(0, 1), ylim=(0, 1.25), xticks=[], yticks=[], aspect=1.25) -fig.get_layout_engine().set(wspace=0, hspace=0, w_pad=0, h_pad=0) - -plt.show() - -# %% -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.axes.Axes.annotate` -# - `matplotlib.patches.FancyArrowPatch` diff --git a/galleries/examples/userdemo/custom_boxstyle01.py b/galleries/examples/userdemo/custom_boxstyle01.py deleted file mode 100644 index 71668cc6cc31..000000000000 --- a/galleries/examples/userdemo/custom_boxstyle01.py +++ /dev/null @@ -1,128 +0,0 @@ -r""" -================= -Custom box styles -================= - -This example demonstrates the implementation of a custom `.BoxStyle`. -Custom `.ConnectionStyle`\s and `.ArrowStyle`\s can be similarly defined. -""" - -import matplotlib.pyplot as plt - -from matplotlib.patches import BoxStyle -from matplotlib.path import Path - -# %% -# Custom box styles can be implemented as a function that takes arguments -# specifying both a rectangular box and the amount of "mutation", and -# returns the "mutated" path. The specific signature is the one of -# ``custom_box_style`` below. -# -# Here, we return a new path which adds an "arrow" shape on the left of the -# box. -# -# The custom box style can then be used by passing -# ``bbox=dict(boxstyle=custom_box_style, ...)`` to `.Axes.text`. - - -def custom_box_style(x0, y0, width, height, mutation_size): - """ - Given the location and size of the box, return the path of the box around - it. - - Rotation is automatically taken care of. - - Parameters - ---------- - x0, y0, width, height : float - Box location and size. - mutation_size : float - Mutation reference scale, typically the text font size. - """ - # padding - mypad = 0.3 - pad = mutation_size * mypad - # width and height with padding added. - width = width + 2 * pad - height = height + 2 * pad - # boundary of the padded box - x0, y0 = x0 - pad, y0 - pad - x1, y1 = x0 + width, y0 + height - # return the new path - return Path([(x0, y0), - (x1, y0), (x1, y1), (x0, y1), - (x0-pad, (y0+y1)/2), (x0, y0), - (x0, y0)], - closed=True) - - -fig, ax = plt.subplots(figsize=(3, 3)) -ax.text(0.5, 0.5, "Test", size=30, va="center", ha="center", rotation=30, - bbox=dict(boxstyle=custom_box_style, alpha=0.2)) - - -# %% -# Likewise, custom box styles can be implemented as classes that implement -# ``__call__``. -# -# The classes can then be registered into the ``BoxStyle._style_list`` dict, -# which allows specifying the box style as a string, -# ``bbox=dict(boxstyle="registered_name,param=value,...", ...)``. -# Note that this registration relies on internal APIs and is therefore not -# officially supported. - - -class MyStyle: - """A simple box.""" - - def __init__(self, pad=0.3): - """ - The arguments must be floats and have default values. - - Parameters - ---------- - pad : float - amount of padding - """ - self.pad = pad - super().__init__() - - def __call__(self, x0, y0, width, height, mutation_size): - """ - Given the location and size of the box, return the path of the box - around it. - - Rotation is automatically taken care of. - - Parameters - ---------- - x0, y0, width, height : float - Box location and size. - mutation_size : float - Reference scale for the mutation, typically the text font size. - """ - # padding - pad = mutation_size * self.pad - # width and height with padding added - width = width + 2.*pad - height = height + 2.*pad - # boundary of the padded box - x0, y0 = x0 - pad, y0 - pad - x1, y1 = x0 + width, y0 + height - # return the new path - return Path([(x0, y0), - (x1, y0), (x1, y1), (x0, y1), - (x0-pad, (y0+y1)/2.), (x0, y0), - (x0, y0)], - closed=True) - - -BoxStyle._style_list["angled"] = MyStyle # Register the custom style. - -fig, ax = plt.subplots(figsize=(3, 3)) -ax.text(0.5, 0.5, "Test", size=30, va="center", ha="center", rotation=30, - bbox=dict(boxstyle="angled,pad=0.5", alpha=0.2)) - -del BoxStyle._style_list["angled"] # Unregister it. - -plt.show() diff --git a/galleries/examples/userdemo/simple_annotate01.py b/galleries/examples/userdemo/simple_annotate01.py deleted file mode 100644 index cb3b6cb7e2c8..000000000000 --- a/galleries/examples/userdemo/simple_annotate01.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -================= -Simple Annotate01 -================= - -""" - -import matplotlib.pyplot as plt - -import matplotlib.patches as mpatches - -fig, axs = plt.subplots(2, 4) -x1, y1 = 0.3, 0.3 -x2, y2 = 0.7, 0.7 - -ax = axs.flat[0] -ax.plot([x1, x2], [y1, y2], "o") -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="->")) -ax.text(.05, .95, "A $->$ B", - transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[2] -ax.plot([x1, x2], [y1, y2], "o") -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=0.3", - shrinkB=5)) -ax.text(.05, .95, "shrinkB=5", - transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[3] -ax.plot([x1, x2], [y1, y2], "o") -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=0.3")) -ax.text(.05, .95, "connectionstyle=arc3", - transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[4] -ax.plot([x1, x2], [y1, y2], "o") -el = mpatches.Ellipse((x1, y1), 0.3, 0.4, angle=30, alpha=0.5) -ax.add_artist(el) -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=0.2")) - -ax = axs.flat[5] -ax.plot([x1, x2], [y1, y2], "o") -el = mpatches.Ellipse((x1, y1), 0.3, 0.4, angle=30, alpha=0.5) -ax.add_artist(el) -ax.annotate("", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=0.2", - patchB=el)) -ax.text(.05, .95, "patchB", - transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[6] -ax.plot([x1], [y1], "o") -ax.annotate("Test", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - ha="center", va="center", - bbox=dict(boxstyle="round", fc="w"), - arrowprops=dict(arrowstyle="->")) -ax.text(.05, .95, "annotate", - transform=ax.transAxes, ha="left", va="top") - -ax = axs.flat[7] -ax.plot([x1], [y1], "o") -ax.annotate("Test", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='data', - ha="center", va="center", - bbox=dict(boxstyle="round", fc="w", ), - arrowprops=dict(arrowstyle="->", relpos=(0., 0.))) -ax.text(.05, .95, "relpos=(0, 0)", - transform=ax.transAxes, ha="left", va="top") - -for ax in axs.flat: - ax.set(xlim=(0, 1), ylim=(0, 1), xticks=[], yticks=[], aspect=1) - -plt.show() diff --git a/galleries/users_explain/text/annotations.py b/galleries/users_explain/text/annotations.py index 89787c4a6336..5cfb16c12715 100644 --- a/galleries/users_explain/text/annotations.py +++ b/galleries/users_explain/text/annotations.py @@ -1,13 +1,16 @@ r""" +.. redirect-from:: /gallery/userdemo/anchored_box04 +.. redirect-from:: /gallery/userdemo/annotate_explain .. redirect-from:: /gallery/userdemo/annotate_simple01 .. redirect-from:: /gallery/userdemo/annotate_simple02 .. redirect-from:: /gallery/userdemo/annotate_simple03 .. redirect-from:: /gallery/userdemo/annotate_simple04 -.. redirect-from:: /gallery/userdemo/anchored_box04 .. redirect-from:: /gallery/userdemo/annotate_simple_coord01 .. redirect-from:: /gallery/userdemo/annotate_simple_coord02 .. redirect-from:: /gallery/userdemo/annotate_simple_coord03 +.. redirect-from:: /gallery/userdemo/annotate_text_arrow .. redirect-from:: /gallery/userdemo/connect_simple01 +.. redirect-from:: /gallery/userdemo/connectionstyle_demo .. redirect-from:: /tutorials/text/annotations .. _annotations: @@ -265,23 +268,30 @@ # Defining custom box styles # ^^^^^^^^^^^^^^^^^^^^^^^^^^ # -# You can use a custom box style. The value for the ``boxstyle`` can be a -# callable object in the following forms: +# Custom box styles can be implemented as a function that takes arguments specifying +# both a rectangular box and the amount of "mutation", and returns the "mutated" path. +# The specific signature is the one of ``custom_box_style`` below. +# +# Here, we return a new path which adds an "arrow" shape on the left of the box. +# +# The custom box style can then be used by passing +# ``bbox=dict(boxstyle=custom_box_style, ...)`` to `.Axes.text`. from matplotlib.path import Path def custom_box_style(x0, y0, width, height, mutation_size): """ - Given the location and size of the box, return the path of the box around - it. Rotation is automatically taken care of. + Given the location and size of the box, return the path of the box around it. + + Rotation is automatically taken care of. Parameters ---------- x0, y0, width, height : float Box location and size. mutation_size : float - Mutation reference scale, typically the text font size. + Mutation reference scale, typically the text font size. """ # padding mypad = 0.3 @@ -302,9 +312,71 @@ def custom_box_style(x0, y0, width, height, mutation_size): bbox=dict(boxstyle=custom_box_style, alpha=0.2)) # %% -# See also :doc:`/gallery/userdemo/custom_boxstyle01`. Similarly, you can define a -# custom `.ConnectionStyle` and a custom `.ArrowStyle`. View the source code at -# `.patches` to learn how each class is defined. +# Likewise, custom box styles can be implemented as classes that implement +# ``__call__``. +# +# The classes can then be registered into the ``BoxStyle._style_list`` dict, +# which allows specifying the box style as a string, +# ``bbox=dict(boxstyle="registered_name,param=value,...", ...)``. +# Note that this registration relies on internal APIs and is therefore not +# officially supported. + +from matplotlib.patches import BoxStyle + + +class MyStyle: + """A simple box.""" + + def __init__(self, pad=0.3): + """ + The arguments must be floats and have default values. + + Parameters + ---------- + pad : float + amount of padding + """ + self.pad = pad + super().__init__() + + def __call__(self, x0, y0, width, height, mutation_size): + """ + Given the location and size of the box, return the path of the box around it. + + Rotation is automatically taken care of. + + Parameters + ---------- + x0, y0, width, height : float + Box location and size. + mutation_size : float + Reference scale for the mutation, typically the text font size. + """ + # padding + pad = mutation_size * self.pad + # width and height with padding added + width = width + 2 * pad + height = height + 2 * pad + # boundary of the padded box + x0, y0 = x0 - pad, y0 - pad + x1, y1 = x0 + width, y0 + height + # return the new path + return Path([(x0, y0), (x1, y0), (x1, y1), (x0, y1), + (x0-pad, (y0+y1)/2), (x0, y0), (x0, y0)], + closed=True) + + +BoxStyle._style_list["angled"] = MyStyle # Register the custom style. + +fig, ax = plt.subplots(figsize=(3, 3)) +ax.text(0.5, 0.5, "Test", size=30, va="center", ha="center", rotation=30, + bbox=dict(boxstyle="angled,pad=0.5", alpha=0.2)) + +del BoxStyle._style_list["angled"] # Unregister it. + +# %% +# Similarly, you can define a custom `.ConnectionStyle` and a custom `.ArrowStyle`. View +# the source code at `.patches` to learn how each class is defined. # # .. _annotation_with_custom_arrow: # @@ -332,9 +404,40 @@ def custom_box_style(x0, y0, width, height, mutation_size): # 4. The path is transmuted to an arrow patch, as specified by the *arrowstyle* # parameter. # -# .. figure:: /gallery/userdemo/images/sphx_glr_annotate_explain_001.png -# :target: /gallery/userdemo/annotate_explain.html -# :align: center +# .. plot:: +# :show-source-link: False +# +# import matplotlib.patches as mpatches +# +# x1, y1 = 0.3, 0.3 +# x2, y2 = 0.7, 0.7 +# arrowprops = { +# "1. connect with connectionstyle": +# dict(arrowstyle="-", patchB=False, shrinkB=0), +# "2. clip against patchB": dict(arrowstyle="-", patchB=True, shrinkB=0), +# "3. shrink by shrinkB": dict(arrowstyle="-", patchB=True, shrinkB=5), +# "4. mutate with arrowstyle": dict(arrowstyle="fancy", patchB=True, shrinkB=5), +# } +# +# fig, axs = plt.subplots(2, 2, figsize=(6, 6), layout='compressed') +# for ax, (name, props) in zip(axs.flat, arrowprops.items()): +# ax.plot([x1, x2], [y1, y2], ".") +# +# el = mpatches.Ellipse((x1, y1), 0.3, 0.4, angle=30, alpha=0.2) +# ax.add_artist(el) +# +# props["patchB"] = el if props["patchB"] else None +# +# ax.annotate( +# "", +# xy=(x1, y1), xycoords='data', +# xytext=(x2, y2), textcoords='data', +# arrowprops={"color": "0.5", "connectionstyle": "arc3,rad=0.3", **props}) +# ax.text(.05, .95, name, transform=ax.transAxes, ha="left", va="top") +# +# ax.set(xlim=(0, 1), ylim=(0, 1), xticks=[], yticks=[], aspect=1) +# +# fig.get_layout_engine().set(wspace=0, hspace=0, w_pad=0, h_pad=0) # # The creation of the connecting path between two points is controlled by # ``connectionstyle`` key and the following styles are available: @@ -358,9 +461,47 @@ def custom_box_style(x0, y0, width, height, mutation_size): # example below. (Warning: The behavior of the ``bar`` style is currently not # well-defined and may be changed in the future). # -# .. figure:: /gallery/userdemo/images/sphx_glr_connectionstyle_demo_001.png -# :target: /gallery/userdemo/connectionstyle_demo.html -# :align: center +# .. plot:: +# :caption: Connection styles for annotations +# +# def demo_con_style(ax, connectionstyle): +# x1, y1 = 0.3, 0.2 +# x2, y2 = 0.8, 0.6 +# +# ax.plot([x1, x2], [y1, y2], ".") +# ax.annotate("", +# xy=(x1, y1), xycoords='data', +# xytext=(x2, y2), textcoords='data', +# arrowprops=dict(arrowstyle="->", color="0.5", +# shrinkA=5, shrinkB=5, +# patchA=None, patchB=None, +# connectionstyle=connectionstyle, +# ), +# ) +# +# ax.text(.05, .95, connectionstyle.replace(",", ",\n"), +# transform=ax.transAxes, ha="left", va="top") +# +# ax.set(xlim=(0, 1), ylim=(0, 1.25), xticks=[], yticks=[], aspect=1.25) +# +# fig, axs = plt.subplots(3, 5, figsize=(7, 6.3), layout="compressed") +# demo_con_style(axs[0, 0], "angle3,angleA=90,angleB=0") +# demo_con_style(axs[1, 0], "angle3,angleA=0,angleB=90") +# demo_con_style(axs[0, 1], "arc3,rad=0.") +# demo_con_style(axs[1, 1], "arc3,rad=0.3") +# demo_con_style(axs[2, 1], "arc3,rad=-0.3") +# demo_con_style(axs[0, 2], "angle,angleA=-90,angleB=180,rad=0") +# demo_con_style(axs[1, 2], "angle,angleA=-90,angleB=180,rad=5") +# demo_con_style(axs[2, 2], "angle,angleA=-90,angleB=10,rad=5") +# demo_con_style(axs[0, 3], "arc,angleA=-90,angleB=0,armA=30,armB=30,rad=0") +# demo_con_style(axs[1, 3], "arc,angleA=-90,angleB=0,armA=30,armB=30,rad=5") +# demo_con_style(axs[2, 3], "arc,angleA=-90,angleB=0,armA=0,armB=40,rad=0") +# demo_con_style(axs[0, 4], "bar,fraction=0.3") +# demo_con_style(axs[1, 4], "bar,fraction=-0.3") +# demo_con_style(axs[2, 4], "bar,angle=180,fraction=-0.2") +# +# axs[2, 0].remove() +# fig.get_layout_engine().set(wspace=0, hspace=0, w_pad=0, h_pad=0) # # The connecting path (after clipping and shrinking) is then mutated to # an arrow patch, according to the given ``arrowstyle``: From e7aba708ec6c8d834bc250bafa4e405d2736af38 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 3 Aug 2024 05:42:17 -0400 Subject: [PATCH 0111/1230] Backport PR #28649: FIX: improve formatting of image values in cases of singular norms --- lib/matplotlib/artist.py | 4 +++- lib/matplotlib/cbook.py | 4 ++++ lib/matplotlib/tests/test_image.py | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index d5b8631e95df..735c2eb59cf5 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -1346,7 +1346,9 @@ def format_cursor_data(self, data): delta = np.diff( self.norm.boundaries[neigh_idx:cur_idx + 2] ).max() - + elif self.norm.vmin == self.norm.vmax: + # singular norms, use delta of 10% of only value + delta = np.abs(self.norm.vmin * .1) else: # Midpoints of neighboring color intervals. neighbors = self.norm.inverse( diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index a156ac200abf..f5a4199cf9ad 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2252,6 +2252,10 @@ def _g_sig_digits(value, delta): it is known with an error of *delta*. """ if delta == 0: + if value == 0: + # if both value and delta are 0, np.spacing below returns 5e-324 + # which results in rather silly results + return 3 # delta = 0 may occur when trying to format values over a tiny range; # in that case, replace it by the distance to the closest float. delta = abs(np.spacing(value)) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index a043d3aec983..0c032fa5367a 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -411,7 +411,8 @@ def test_cursor_data_nonuniform(xy, data): ([[.123, .987]], "[0.123]"), ([[np.nan, 1, 2]], "[]"), ([[1, 1+1e-15]], "[1.0000000000000000]"), - ([[-1, -1]], "[-1.0000000000000000]"), + ([[-1, -1]], "[-1.0]"), + ([[0, 0]], "[0.00]"), ]) def test_format_cursor_data(data, text): from matplotlib.backend_bases import MouseEvent From b86e35e4d2d6c89461f396654eb08ba54577bade Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Thu, 18 Jul 2024 11:58:04 +0300 Subject: [PATCH 0112/1230] Qt embedding example: Separate drawing and data retrieval timers In the previous version of this example, if the timer interval would have been simply decreased to 1ms, the GUI could have appeared as stuck on some platforms / (slow) machines, because the event loop didn't find the time to respond to external events such as the user dragging the window etc. This change does 3 things: - Puts the data to plot in the self.{x,y}data attributes. - Separates the timer that updates the self.{x,y}data from the timer that updates the canvas - Explains much better the reasoning for the timers' intervals choices in comments, as well as explaining why the timers are attributed to self, although they are not used by other methods of the class. - Use the asynchronous draw_idle() function to further guarantee that the drawing won't be blocking. Co-authored-by: Elliott Sales de Andrade --- .../user_interfaces/embedding_in_qt_sgskip.py | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py b/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py index b79f582a65e4..56f076d3d338 100644 --- a/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py @@ -44,18 +44,34 @@ def __init__(self): self._static_ax.plot(t, np.tan(t), ".") self._dynamic_ax = dynamic_canvas.figure.subplots() - t = np.linspace(0, 10, 101) # Set up a Line2D. - self._line, = self._dynamic_ax.plot(t, np.sin(t + time.time())) - self._timer = dynamic_canvas.new_timer(50) - self._timer.add_callback(self._update_canvas) - self._timer.start() + self.xdata = np.linspace(0, 10, 101) + self._update_data() + self._line, = self._dynamic_ax.plot(self.xdata, self.ydata) + # The below two timers must be attributes of self, so that the garbage + # collector won't clean them after we finish with __init__... - def _update_canvas(self): - t = np.linspace(0, 10, 101) + # The data retrieval may be fast as possible (Using QRunnable could be + # even faster). + self.data_timer = dynamic_canvas.new_timer(1) + self.data_timer.add_callback(self._update_data) + self.data_timer.start() + # Drawing at 50Hz should be fast enough for the GUI to feel smooth, and + # not too fast for the GUI to be overloaded with events that need to be + # processed while the GUI element is changed. + self.drawing_timer = dynamic_canvas.new_timer(20) + self.drawing_timer.add_callback(self._update_canvas) + self.drawing_timer.start() + + def _update_data(self): # Shift the sinusoid as a function of time. - self._line.set_data(t, np.sin(t + time.time())) - self._line.figure.canvas.draw() + self.ydata = np.sin(self.xdata + time.time()) + + def _update_canvas(self): + self._line.set_data(self.xdata, self.ydata) + # It should be safe to use the synchronous draw() method for most drawing + # frequencies, but it is safer to use draw_idle(). + self._line.figure.canvas.draw_idle() if __name__ == "__main__": From e68bfdbea55b60c566de04d41a2b1d95544ea833 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 31 Jul 2024 06:34:27 +0200 Subject: [PATCH 0113/1230] Backport PR #28546: DOC: Clarify/simplify example of multiple images with one colorbar --- .../images_contours_and_fields/multi_image.py | 88 +++++++++++-------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/galleries/examples/images_contours_and_fields/multi_image.py b/galleries/examples/images_contours_and_fields/multi_image.py index 5634a32abeb9..8be048055dec 100644 --- a/galleries/examples/images_contours_and_fields/multi_image.py +++ b/galleries/examples/images_contours_and_fields/multi_image.py @@ -1,9 +1,19 @@ """ -=============== -Multiple images -=============== +================================= +Multiple images with one colorbar +================================= -Make a set of images with a single colormap, norm, and colorbar. +Use a single colorbar for multiple images. + +Currently, a colorbar can only be connected to one image. The connection +guarantees that the data coloring is consistent with the colormap scale +(i.e. the color of value *x* in the colormap is used for coloring a data +value *x* in the image). + +If we want one colorbar to be representative for multiple images, we have +to explicitly ensure consistent data coloring by using the same data +normalization for all the images. We ensure this by explicitly creating a +``norm`` object that we pass to all the image plotting methods. """ import matplotlib.pyplot as plt @@ -12,47 +22,53 @@ from matplotlib import colors np.random.seed(19680801) -Nr = 3 -Nc = 2 -fig, axs = plt.subplots(Nr, Nc) +datasets = [ + (i+1)/10 * np.random.rand(10, 20) + for i in range(4) +] + +fig, axs = plt.subplots(2, 2) fig.suptitle('Multiple images') -images = [] -for i in range(Nr): - for j in range(Nc): - # Generate data with a range that varies from one plot to the next. - data = ((1 + i + j) / 10) * np.random.rand(10, 20) - images.append(axs[i, j].imshow(data)) - axs[i, j].label_outer() +# create a single norm to be shared across all images +norm = colors.Normalize(vmin=np.min(datasets), vmax=np.max(datasets)) -# Find the min and max of all colors for use in setting the color scale. -vmin = min(image.get_array().min() for image in images) -vmax = max(image.get_array().max() for image in images) -norm = colors.Normalize(vmin=vmin, vmax=vmax) -for im in images: - im.set_norm(norm) +images = [] +for ax, data in zip(axs.flat, datasets): + images.append(ax.imshow(data, norm=norm)) fig.colorbar(images[0], ax=axs, orientation='horizontal', fraction=.1) - -# Make images respond to changes in the norm of other images (e.g. via the -# "edit axis, curves and images parameters" GUI on Qt), but be careful not to -# recurse infinitely! -def update(changed_image): - for im in images: - if (changed_image.get_cmap() != im.get_cmap() - or changed_image.get_clim() != im.get_clim()): - im.set_cmap(changed_image.get_cmap()) - im.set_clim(changed_image.get_clim()) - - -for im in images: - im.callbacks.connect('changed', update) - plt.show() # %% +# The colors are now kept consistent across all images when changing the +# scaling, e.g. through zooming in the colorbar or via the "edit axis, +# curves and images parameters" GUI of the Qt backend. This is sufficient +# for most practical use cases. +# +# Advanced: Additionally sync the colormap +# ---------------------------------------- +# +# Sharing a common norm object guarantees synchronized scaling because scale +# changes modify the norm object in-place and thus propagate to all images +# that use this norm. This approach does not help with synchronizing colormaps +# because changing the colormap of an image (e.g. through the "edit axis, +# curves and images parameters" GUI of the Qt backend) results in the image +# referencing the new colormap object. Thus, the other images are not updated. +# +# To update the other images, sync the +# colormaps using the following code:: +# +# def sync_cmaps(changed_image): +# for im in images: +# if changed_image.get_cmap() != im.get_cmap(): +# im.set_cmap(changed_image.get_cmap()) +# +# for im in images: +# im.callbacks.connect('changed', sync_cmaps) +# # # .. admonition:: References # @@ -63,6 +79,4 @@ def update(changed_image): # - `matplotlib.figure.Figure.colorbar` / `matplotlib.pyplot.colorbar` # - `matplotlib.colors.Normalize` # - `matplotlib.cm.ScalarMappable.set_cmap` -# - `matplotlib.cm.ScalarMappable.set_norm` -# - `matplotlib.cm.ScalarMappable.set_clim` # - `matplotlib.cbook.CallbackRegistry.connect` From 6c7dbc06ebc50d4833189605b4fdc6a30ea8f283 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 31 Jul 2024 06:34:27 +0200 Subject: [PATCH 0114/1230] Backport PR #28546: DOC: Clarify/simplify example of multiple images with one colorbar --- .../images_contours_and_fields/multi_image.py | 88 +++++++++++-------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/galleries/examples/images_contours_and_fields/multi_image.py b/galleries/examples/images_contours_and_fields/multi_image.py index 5634a32abeb9..8be048055dec 100644 --- a/galleries/examples/images_contours_and_fields/multi_image.py +++ b/galleries/examples/images_contours_and_fields/multi_image.py @@ -1,9 +1,19 @@ """ -=============== -Multiple images -=============== +================================= +Multiple images with one colorbar +================================= -Make a set of images with a single colormap, norm, and colorbar. +Use a single colorbar for multiple images. + +Currently, a colorbar can only be connected to one image. The connection +guarantees that the data coloring is consistent with the colormap scale +(i.e. the color of value *x* in the colormap is used for coloring a data +value *x* in the image). + +If we want one colorbar to be representative for multiple images, we have +to explicitly ensure consistent data coloring by using the same data +normalization for all the images. We ensure this by explicitly creating a +``norm`` object that we pass to all the image plotting methods. """ import matplotlib.pyplot as plt @@ -12,47 +22,53 @@ from matplotlib import colors np.random.seed(19680801) -Nr = 3 -Nc = 2 -fig, axs = plt.subplots(Nr, Nc) +datasets = [ + (i+1)/10 * np.random.rand(10, 20) + for i in range(4) +] + +fig, axs = plt.subplots(2, 2) fig.suptitle('Multiple images') -images = [] -for i in range(Nr): - for j in range(Nc): - # Generate data with a range that varies from one plot to the next. - data = ((1 + i + j) / 10) * np.random.rand(10, 20) - images.append(axs[i, j].imshow(data)) - axs[i, j].label_outer() +# create a single norm to be shared across all images +norm = colors.Normalize(vmin=np.min(datasets), vmax=np.max(datasets)) -# Find the min and max of all colors for use in setting the color scale. -vmin = min(image.get_array().min() for image in images) -vmax = max(image.get_array().max() for image in images) -norm = colors.Normalize(vmin=vmin, vmax=vmax) -for im in images: - im.set_norm(norm) +images = [] +for ax, data in zip(axs.flat, datasets): + images.append(ax.imshow(data, norm=norm)) fig.colorbar(images[0], ax=axs, orientation='horizontal', fraction=.1) - -# Make images respond to changes in the norm of other images (e.g. via the -# "edit axis, curves and images parameters" GUI on Qt), but be careful not to -# recurse infinitely! -def update(changed_image): - for im in images: - if (changed_image.get_cmap() != im.get_cmap() - or changed_image.get_clim() != im.get_clim()): - im.set_cmap(changed_image.get_cmap()) - im.set_clim(changed_image.get_clim()) - - -for im in images: - im.callbacks.connect('changed', update) - plt.show() # %% +# The colors are now kept consistent across all images when changing the +# scaling, e.g. through zooming in the colorbar or via the "edit axis, +# curves and images parameters" GUI of the Qt backend. This is sufficient +# for most practical use cases. +# +# Advanced: Additionally sync the colormap +# ---------------------------------------- +# +# Sharing a common norm object guarantees synchronized scaling because scale +# changes modify the norm object in-place and thus propagate to all images +# that use this norm. This approach does not help with synchronizing colormaps +# because changing the colormap of an image (e.g. through the "edit axis, +# curves and images parameters" GUI of the Qt backend) results in the image +# referencing the new colormap object. Thus, the other images are not updated. +# +# To update the other images, sync the +# colormaps using the following code:: +# +# def sync_cmaps(changed_image): +# for im in images: +# if changed_image.get_cmap() != im.get_cmap(): +# im.set_cmap(changed_image.get_cmap()) +# +# for im in images: +# im.callbacks.connect('changed', sync_cmaps) +# # # .. admonition:: References # @@ -63,6 +79,4 @@ def update(changed_image): # - `matplotlib.figure.Figure.colorbar` / `matplotlib.pyplot.colorbar` # - `matplotlib.colors.Normalize` # - `matplotlib.cm.ScalarMappable.set_cmap` -# - `matplotlib.cm.ScalarMappable.set_norm` -# - `matplotlib.cm.ScalarMappable.set_clim` # - `matplotlib.cbook.CallbackRegistry.connect` From a46ecad34240487861bdf36980eb79c9adb850bd Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:46:08 +0200 Subject: [PATCH 0115/1230] Rename _update_data to _update_ydata --- .../examples/user_interfaces/embedding_in_qt_sgskip.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py b/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py index 56f076d3d338..854ae798e284 100644 --- a/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py @@ -46,7 +46,7 @@ def __init__(self): self._dynamic_ax = dynamic_canvas.figure.subplots() # Set up a Line2D. self.xdata = np.linspace(0, 10, 101) - self._update_data() + self._update_ydata() self._line, = self._dynamic_ax.plot(self.xdata, self.ydata) # The below two timers must be attributes of self, so that the garbage # collector won't clean them after we finish with __init__... @@ -54,7 +54,7 @@ def __init__(self): # The data retrieval may be fast as possible (Using QRunnable could be # even faster). self.data_timer = dynamic_canvas.new_timer(1) - self.data_timer.add_callback(self._update_data) + self.data_timer.add_callback(self._update_ydata) self.data_timer.start() # Drawing at 50Hz should be fast enough for the GUI to feel smooth, and # not too fast for the GUI to be overloaded with events that need to be @@ -63,7 +63,7 @@ def __init__(self): self.drawing_timer.add_callback(self._update_canvas) self.drawing_timer.start() - def _update_data(self): + def _update_ydata(self): # Shift the sinusoid as a function of time. self.ydata = np.sin(self.xdata + time.time()) From 5b5759058b344534a43202ac911606c2183c823b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 19:23:16 +0000 Subject: [PATCH 0116/1230] Bump the actions group with 2 updates Bumps the actions group with 2 updates: [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) and [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance). Updates `pypa/cibuildwheel` from 2.19.2 to 2.20.0 - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/7e5a838a63ac8128d71ab2dfd99e4634dd1bca09...bd033a44476646b606efccdd5eed92d5ea1d77ad) Updates `actions/attest-build-provenance` from 1.3.3 to 1.4.0 - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/5e9cb68e95676991667494a6a4e59b8a2f13e1d0...210c1913531870065f03ce1f9440dd87bc0938cd) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: actions/attest-build-provenance dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/cibuildwheel.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 90526af740ba..ddb102b8ffd1 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -135,7 +135,7 @@ jobs: path: dist/ - name: Build wheels for CPython 3.13 - uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 + uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -156,7 +156,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.12 - uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 + uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -164,7 +164,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.11 - uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 + uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -172,7 +172,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.10 - uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 + uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -180,7 +180,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for PyPy - uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 + uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -216,7 +216,7 @@ jobs: run: ls dist - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 + uses: actions/attest-build-provenance@210c1913531870065f03ce1f9440dd87bc0938cd # v1.4.0 with: subject-path: dist/matplotlib-* From d3457e2ea230a8f59905183522f586b5b9784cf7 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 5 Aug 2024 15:48:06 -0400 Subject: [PATCH 0117/1230] API: deprecate unused helper in patch._Styles --- doc/api/next_api_changes/deprecations/28670-TAC.rst | 6 ++++++ lib/matplotlib/patches.py | 5 +++++ 2 files changed, 11 insertions(+) create mode 100644 doc/api/next_api_changes/deprecations/28670-TAC.rst diff --git a/doc/api/next_api_changes/deprecations/28670-TAC.rst b/doc/api/next_api_changes/deprecations/28670-TAC.rst new file mode 100644 index 000000000000..50ada7201437 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/28670-TAC.rst @@ -0,0 +1,6 @@ +Drerecated ``matplotlib.patches._Styles`` and subclasses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This class method is never used internally. Due to the internal check in the +method it only accepts subclasses of a private baseclass embedded in the host +class which makes it unlikely that it has been used externally. diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 2899952634a9..bc75e6923879 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -2347,6 +2347,11 @@ def pprint_styles(cls): return textwrap.indent(rst_table, prefix=' ' * 4) @classmethod + @_api.deprecated( + '3.10.0', + message="This method is never used internally.", + alternative="No replacement. Please open an issue if you use this." + ) def register(cls, name, style): """Register a new style.""" if not issubclass(style, cls._Base): From cde1fa5c3045d446b0d9e5cf46e350589fb73655 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 6 Aug 2024 14:34:47 -0400 Subject: [PATCH 0118/1230] DOC: correct heading Co-authored-by: Kyle Sunden --- doc/api/next_api_changes/deprecations/28670-TAC.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/next_api_changes/deprecations/28670-TAC.rst b/doc/api/next_api_changes/deprecations/28670-TAC.rst index 50ada7201437..e970abf69d54 100644 --- a/doc/api/next_api_changes/deprecations/28670-TAC.rst +++ b/doc/api/next_api_changes/deprecations/28670-TAC.rst @@ -1,5 +1,5 @@ -Drerecated ``matplotlib.patches._Styles`` and subclasses -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Deprecated ``register`` on ``matplotlib.patches._Styles`` and subclasses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This class method is never used internally. Due to the internal check in the method it only accepts subclasses of a private baseclass embedded in the host From 1f9b487afde562362b944864adeafc40e6336461 Mon Sep 17 00:00:00 2001 From: Caitlin Hathaway <103151440+caitlinhat@users.noreply.github.com> Date: Wed, 7 Aug 2024 07:38:03 -0500 Subject: [PATCH 0119/1230] remove all Todo's in animation.py --- lib/matplotlib/animation.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 28b01fedf138..00b16d240740 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -1,18 +1,3 @@ -# TODO: -# * Blit -# * Still a few edge cases that aren't working correctly -# * Can this integrate better with existing matplotlib animation artist flag? -# - If animated removes from default draw(), perhaps we could use this to -# simplify initial draw. -# * Example -# * Frameless animation - pure procedural with no loop -# * Need example that uses something like inotify or subprocess -# * Complex syncing examples -# * Movies -# * Can blit be enabled for movies? -# * Need to consider event sources to allow clicking through multiple figures - - import abc import base64 import contextlib From 2a8d1fc7a4a0303c534b6452c752c79122ecf926 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 7 Aug 2024 16:05:43 +0200 Subject: [PATCH 0120/1230] Backport PR #28650: remove out of date todos on animation.py --- lib/matplotlib/animation.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 1efb72cb52e6..5a4764f1a79f 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -1,23 +1,3 @@ -# TODO: -# * Documentation -- this will need a new section of the User's Guide. -# Both for Animations and just timers. -# - Also need to update -# https://scipy-cookbook.readthedocs.io/items/Matplotlib_Animations.html -# * Blit -# * Currently broken with Qt4 for widgets that don't start on screen -# * Still a few edge cases that aren't working correctly -# * Can this integrate better with existing matplotlib animation artist flag? -# - If animated removes from default draw(), perhaps we could use this to -# simplify initial draw. -# * Example -# * Frameless animation - pure procedural with no loop -# * Need example that uses something like inotify or subprocess -# * Complex syncing examples -# * Movies -# * Can blit be enabled for movies? -# * Need to consider event sources to allow clicking through multiple figures - - import abc import base64 import contextlib From 1bb9c02173f86efee085c9e771a61a449be6e02d Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Wed, 7 Aug 2024 15:48:15 -0500 Subject: [PATCH 0121/1230] Backport PR #28577: Copy all internals from initial Tick to lazy ones --- lib/matplotlib/axis.py | 33 ++++++++++++++++++++----------- lib/matplotlib/tests/test_axes.py | 22 +++++++++++++++++++++ 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 3afc98fac60b..98f7db89b09f 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -33,12 +33,6 @@ _gridline_param_names = ['grid_' + name for name in _line_param_names + _line_param_aliases] -_MARKER_DICT = { - 'out': (mlines.TICKDOWN, mlines.TICKUP), - 'in': (mlines.TICKUP, mlines.TICKDOWN), - 'inout': ('|', '|'), -} - class Tick(martist.Artist): """ @@ -204,18 +198,21 @@ def _set_labelrotation(self, labelrotation): _api.check_in_list(['auto', 'default'], labelrotation=mode) self._labelrotation = (mode, angle) + @property + def _pad(self): + return self._base_pad + self.get_tick_padding() + def _apply_tickdir(self, tickdir): """Set tick direction. Valid values are 'out', 'in', 'inout'.""" - # This method is responsible for updating `_pad`, and, in subclasses, - # for setting the tick{1,2}line markers as well. From the user - # perspective this should always be called through _apply_params, which - # further updates ticklabel positions using the new pads. + # This method is responsible for verifying input and, in subclasses, for setting + # the tick{1,2}line markers. From the user perspective this should always be + # called through _apply_params, which further updates ticklabel positions using + # the new pads. if tickdir is None: tickdir = mpl.rcParams[f'{self.__name__}.direction'] else: _api.check_in_list(['in', 'out', 'inout'], tickdir=tickdir) self._tickdir = tickdir - self._pad = self._base_pad + self.get_tick_padding() def get_tickdir(self): return self._tickdir @@ -425,7 +422,11 @@ def _get_text2_transform(self): def _apply_tickdir(self, tickdir): # docstring inherited super()._apply_tickdir(tickdir) - mark1, mark2 = _MARKER_DICT[self._tickdir] + mark1, mark2 = { + 'out': (mlines.TICKDOWN, mlines.TICKUP), + 'in': (mlines.TICKUP, mlines.TICKDOWN), + 'inout': ('|', '|'), + }[self._tickdir] self.tick1line.set_marker(mark1) self.tick2line.set_marker(mark2) @@ -1617,6 +1618,14 @@ def _copy_tick_props(self, src, dest): dest.tick1line.update_from(src.tick1line) dest.tick2line.update_from(src.tick2line) dest.gridline.update_from(src.gridline) + dest.update_from(src) + dest._loc = src._loc + dest._size = src._size + dest._width = src._width + dest._base_pad = src._base_pad + dest._labelrotation = src._labelrotation + dest._zorder = src._zorder + dest._tickdir = src._tickdir def get_label_text(self): """Get the text of the label.""" diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index f18e05dc2f1e..3ec9923c0840 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -5631,6 +5631,28 @@ def test_reset_ticks(fig_test, fig_ref): ax.yaxis.reset_ticks() +@mpl.style.context('mpl20') +def test_context_ticks(): + with plt.rc_context({ + 'xtick.direction': 'in', 'xtick.major.size': 30, 'xtick.major.width': 5, + 'xtick.color': 'C0', 'xtick.major.pad': 12, + 'xtick.bottom': True, 'xtick.top': True, + 'xtick.labelsize': 14, 'xtick.labelcolor': 'C1'}): + fig, ax = plt.subplots() + # Draw outside the context so that all-but-first tick are generated with the normal + # mpl20 style in place. + fig.draw_without_rendering() + + first_tick = ax.xaxis.majorTicks[0] + for tick in ax.xaxis.majorTicks[1:]: + assert tick._size == first_tick._size + assert tick._width == first_tick._width + assert tick._base_pad == first_tick._base_pad + assert tick._labelrotation == first_tick._labelrotation + assert tick._zorder == first_tick._zorder + assert tick._tickdir == first_tick._tickdir + + def test_vline_limit(): fig = plt.figure() ax = fig.gca() From db478989b0320bafc94db1509d6fc8e5bb8e09e4 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 7 Aug 2024 16:51:13 -0400 Subject: [PATCH 0122/1230] WIN: Fix capsule check for SetForegroundWindow Looking at pybind11 again, the `py::capsule::name` method returns a `const char *`, and comparing that with a literal using `==` is unspecified behaviour. Seemingly, this is fine on MSVC, but MinGW gcc warns about it. --- src/_c_internal_utils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_c_internal_utils.cpp b/src/_c_internal_utils.cpp index e118183ecc8b..0fb3bc185c8b 100644 --- a/src/_c_internal_utils.cpp +++ b/src/_c_internal_utils.cpp @@ -125,7 +125,7 @@ static void mpl_SetForegroundWindow(py::capsule UNUSED_ON_NON_WINDOWS(handle_p)) { #ifdef _WIN32 - if (handle_p.name() != "HWND") { + if (strcmp(handle_p.name(), "HWND") != 0) { throw std::runtime_error("Handle must be a value returned from Win32_GetForegroundWindow"); } HWND handle = static_cast(handle_p.get_pointer()); From 24b24898f1cfddf421ea59864cbd75af1d6a5491 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 7 Aug 2024 16:58:09 -0400 Subject: [PATCH 0123/1230] WIN: Only define _WIN32_WINNT if not new enough This warns with MinGW, since it already defines `_WIN32_WINNT` to 0x0a00. --- src/_c_internal_utils.cpp | 9 ++++++++- src/_tkagg.cpp | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/_c_internal_utils.cpp b/src/_c_internal_utils.cpp index 0fb3bc185c8b..d1ae620c3b8e 100644 --- a/src/_c_internal_utils.cpp +++ b/src/_c_internal_utils.cpp @@ -7,7 +7,14 @@ #define WIN32_LEAN_AND_MEAN // Windows 10, for latest HiDPI API support. #define WINVER 0x0A00 -#define _WIN32_WINNT 0x0A00 +#if defined(_WIN32_WINNT) +#if _WIN32_WINNT < WINVER +#undef _WIN32_WINNT +#define _WIN32_WINNT WINVER +#endif +#else +#define _WIN32_WINNT WINVER +#endif #endif #include #ifdef __linux__ diff --git a/src/_tkagg.cpp b/src/_tkagg.cpp index e35502fe23ff..bfc2253188fd 100644 --- a/src/_tkagg.cpp +++ b/src/_tkagg.cpp @@ -19,7 +19,14 @@ #define WIN32_LEAN_AND_MEAN // Windows 8.1 #define WINVER 0x0603 -#define _WIN32_WINNT 0x0603 +#if defined(_WIN32_WINNT) +#if _WIN32_WINNT < WINVER +#undef _WIN32_WINNT +#define _WIN32_WINNT WINVER +#endif +#else +#define _WIN32_WINNT WINVER +#endif #endif #include From f4e9ea548a13c1a5a02b0a2831059ce71cbdfaf7 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 1 Aug 2024 03:06:49 -0400 Subject: [PATCH 0124/1230] DOC: Tell sphinx-gallery to link mpl_toolkits from our build Otherwise, it will try to find it with intersphinx, and fail. --- doc/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 7e8c58489618..ea1e75c20af5 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -276,7 +276,8 @@ def tutorials_download_error(record): 'matplotlib_animations': True, 'min_reported_time': 1, 'plot_gallery': 'True', # sphinx-gallery/913 - 'reference_url': {'matplotlib': None}, + 'reference_url': {'matplotlib': None, 'mpl_toolkits': None}, + 'prefer_full_module': {r'mpl_toolkits\.'}, 'remove_config_comments': True, 'reset_modules': ('matplotlib', clear_basic_units), 'subsection_order': gallery_order_sectionorder, From fe1043ec37862348372c69080f59e9b7a63ba0de Mon Sep 17 00:00:00 2001 From: hannah Date: Wed, 7 Aug 2024 20:27:27 -0400 Subject: [PATCH 0125/1230] Doc: add axes titles to axhspan/axvspan Because there are multiple axes here, titling the axes w. the function it's demoing helps highlight what's going on --- galleries/examples/subplots_axes_and_figures/axhspan_demo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/galleries/examples/subplots_axes_and_figures/axhspan_demo.py b/galleries/examples/subplots_axes_and_figures/axhspan_demo.py index 788030fcc5f3..995552d2e826 100644 --- a/galleries/examples/subplots_axes_and_figures/axhspan_demo.py +++ b/galleries/examples/subplots_axes_and_figures/axhspan_demo.py @@ -17,7 +17,7 @@ s = 2.9 * np.convolve(np.random.randn(500), np.ones(30) / 30, mode='valid') ax1.plot(s) ax1.axhspan(-1, 1, alpha=0.1) -ax1.set_ylim(-1.5, 1.5) +ax1.set(ylim = (-1.5, 1.5), title = "axhspan") mu = 8 @@ -29,6 +29,7 @@ ax2.axvspan(mu+sigma, mu+2*sigma, color='0.95') ax2.axvline(mu, color='darkgrey', linestyle='--') ax2.plot(x, y) +ax2.set(title = "axvspan") plt.show() From d75d748e6c4e1a08fbdf8ff78f8fdb953d8907b6 Mon Sep 17 00:00:00 2001 From: hannah Date: Wed, 7 Aug 2024 23:15:51 -0400 Subject: [PATCH 0126/1230] fix linting on phone --- galleries/examples/subplots_axes_and_figures/axhspan_demo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/galleries/examples/subplots_axes_and_figures/axhspan_demo.py b/galleries/examples/subplots_axes_and_figures/axhspan_demo.py index 995552d2e826..5544618016d6 100644 --- a/galleries/examples/subplots_axes_and_figures/axhspan_demo.py +++ b/galleries/examples/subplots_axes_and_figures/axhspan_demo.py @@ -17,7 +17,7 @@ s = 2.9 * np.convolve(np.random.randn(500), np.ones(30) / 30, mode='valid') ax1.plot(s) ax1.axhspan(-1, 1, alpha=0.1) -ax1.set(ylim = (-1.5, 1.5), title = "axhspan") +ax1.set(ylim=(-1.5, 1.5), title="axhspan") mu = 8 @@ -29,7 +29,7 @@ ax2.axvspan(mu+sigma, mu+2*sigma, color='0.95') ax2.axvline(mu, color='darkgrey', linestyle='--') ax2.plot(x, y) -ax2.set(title = "axvspan") +ax2.set(title="axvspan") plt.show() From d669d1b50e7465cec34267f9969e57c35b02cd1e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 8 Aug 2024 02:25:10 -0400 Subject: [PATCH 0127/1230] WIN: Fix signedness comparison warning --- src/_c_internal_utils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_c_internal_utils.cpp b/src/_c_internal_utils.cpp index d1ae620c3b8e..74bb97904f89 100644 --- a/src/_c_internal_utils.cpp +++ b/src/_c_internal_utils.cpp @@ -165,7 +165,7 @@ mpl_SetProcessDpiAwareness_max(void) DPI_AWARENESS_CONTEXT_SYSTEM_AWARE}; // Win10 if (IsValidDpiAwarenessContextPtr != NULL && SetProcessDpiAwarenessContextPtr != NULL) { - for (int i = 0; i < sizeof(ctxs) / sizeof(DPI_AWARENESS_CONTEXT); ++i) { + for (size_t i = 0; i < sizeof(ctxs) / sizeof(DPI_AWARENESS_CONTEXT); ++i) { if (IsValidDpiAwarenessContextPtr(ctxs[i])) { SetProcessDpiAwarenessContextPtr(ctxs[i]); break; From c57960c89497163dd80f13244c00a429a5469c62 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 8 Aug 2024 11:31:47 -0500 Subject: [PATCH 0128/1230] Backport PR #28682: Fix warnings from mingw compilers --- src/_c_internal_utils.cpp | 13 ++++++++++--- src/_tkagg.cpp | 9 ++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/_c_internal_utils.cpp b/src/_c_internal_utils.cpp index e118183ecc8b..74bb97904f89 100644 --- a/src/_c_internal_utils.cpp +++ b/src/_c_internal_utils.cpp @@ -7,7 +7,14 @@ #define WIN32_LEAN_AND_MEAN // Windows 10, for latest HiDPI API support. #define WINVER 0x0A00 -#define _WIN32_WINNT 0x0A00 +#if defined(_WIN32_WINNT) +#if _WIN32_WINNT < WINVER +#undef _WIN32_WINNT +#define _WIN32_WINNT WINVER +#endif +#else +#define _WIN32_WINNT WINVER +#endif #endif #include #ifdef __linux__ @@ -125,7 +132,7 @@ static void mpl_SetForegroundWindow(py::capsule UNUSED_ON_NON_WINDOWS(handle_p)) { #ifdef _WIN32 - if (handle_p.name() != "HWND") { + if (strcmp(handle_p.name(), "HWND") != 0) { throw std::runtime_error("Handle must be a value returned from Win32_GetForegroundWindow"); } HWND handle = static_cast(handle_p.get_pointer()); @@ -158,7 +165,7 @@ mpl_SetProcessDpiAwareness_max(void) DPI_AWARENESS_CONTEXT_SYSTEM_AWARE}; // Win10 if (IsValidDpiAwarenessContextPtr != NULL && SetProcessDpiAwarenessContextPtr != NULL) { - for (int i = 0; i < sizeof(ctxs) / sizeof(DPI_AWARENESS_CONTEXT); ++i) { + for (size_t i = 0; i < sizeof(ctxs) / sizeof(DPI_AWARENESS_CONTEXT); ++i) { if (IsValidDpiAwarenessContextPtr(ctxs[i])) { SetProcessDpiAwarenessContextPtr(ctxs[i]); break; diff --git a/src/_tkagg.cpp b/src/_tkagg.cpp index e35502fe23ff..bfc2253188fd 100644 --- a/src/_tkagg.cpp +++ b/src/_tkagg.cpp @@ -19,7 +19,14 @@ #define WIN32_LEAN_AND_MEAN // Windows 8.1 #define WINVER 0x0603 -#define _WIN32_WINNT 0x0603 +#if defined(_WIN32_WINNT) +#if _WIN32_WINNT < WINVER +#undef _WIN32_WINNT +#define _WIN32_WINNT WINVER +#endif +#else +#define _WIN32_WINNT WINVER +#endif #endif #include From e40125ac7f17de880ebc8177e82a16c194779f2a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 25 Jul 2024 15:39:40 -0400 Subject: [PATCH 0129/1230] Backport PR #28293 and #28668: Enable 3.13 wheels and bump cibuildwheel This is the commit message #1: > Merge pull request #28293 from QuLogic/py313 > > BLD: Enable building Python 3.13 wheels for nightlies (cherry picked from commit 725ee995000985b2ee24d1b21cd777e0811272c8) This is the commit message #2: > Merge pull request #28668 from matplotlib/dependabot/github_actions/actions-167bd8b160 > > Bump the actions group with 2 updates (cherry picked from commit fd42e7d63577ef88694913268fe5a5ffd8539431) --- .github/workflows/cibuildwheel.yml | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index ef819ea5a438..4e8ea0ab5bf6 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -134,8 +134,28 @@ jobs: name: cibw-sdist path: dist/ + - name: Build wheels for CPython 3.13 + uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 + with: + package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} + env: + CIBW_BUILD: "cp313-* cp313t-*" + # No free-threading wheels for NumPy; musllinux skipped for main builds also. + CIBW_SKIP: "cp313t-win_amd64 *-musllinux_aarch64" + CIBW_BUILD_FRONTEND: + "pip; args: --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" + CIBW_FREE_THREADED_SUPPORT: true + # No free-threading wheels available for aarch64 on Pillow. + CIBW_TEST_SKIP: "cp313t-manylinux_aarch64" + # We need pre-releases to get the nightly wheels. + CIBW_BEFORE_TEST: >- + pip install --pre + --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple + contourpy numpy pillow + CIBW_ARCHS: ${{ matrix.cibw_archs }} + - name: Build wheels for CPython 3.12 - uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 + uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -143,7 +163,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.11 - uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 + uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -151,7 +171,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.10 - uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 + uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -167,7 +187,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for PyPy - uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 + uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -203,7 +223,7 @@ jobs: run: ls dist - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3 + uses: actions/attest-build-provenance@210c1913531870065f03ce1f9440dd87bc0938cd # v1.4.0 with: subject-path: dist/matplotlib-* From 465401ed3000baff801e8f14592754cf80ce25d9 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:15:32 +0200 Subject: [PATCH 0130/1230] Backport PR #28632: DOC: Tell sphinx-gallery to link mpl_toolkits from our build --- doc/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 56e09a24b53a..843766a804ea 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -263,7 +263,8 @@ def _check_dependencies(): 'matplotlib_animations': True, 'min_reported_time': 1, 'plot_gallery': 'True', # sphinx-gallery/913 - 'reference_url': {'matplotlib': None}, + 'reference_url': {'matplotlib': None, 'mpl_toolkits': None}, + 'prefer_full_module': {r'mpl_toolkits\.'}, 'remove_config_comments': True, 'reset_modules': ('matplotlib', clear_basic_units), 'subsection_order': gallery_order_sectionorder, From d88a582fb14accd95e80a25a567b1d1bb08561d0 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 9 Aug 2024 12:17:28 -0500 Subject: [PATCH 0131/1230] Backport PR #27797: DOC: Use video files for saving animations --- doc/conf.py | 7 ++++++- requirements/doc/doc-requirements.txt | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 843766a804ea..8036edec9989 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -194,6 +194,11 @@ def _check_dependencies(): subsectionorder as gallery_order_subsectionorder) from sphinxext.util import clear_basic_units, matplotlib_reduced_latex_scraper +if parse_version(sphinx_gallery.__version__) >= parse_version('0.17.0'): + sg_matplotlib_animations = (True, 'mp4') +else: + sg_matplotlib_animations = True + # The following import is only necessary to monkey patch the signature later on from sphinx_gallery import gen_rst @@ -260,7 +265,7 @@ def _check_dependencies(): 'image_scrapers': (matplotlib_reduced_latex_scraper, ), 'image_srcset': ["2x"], 'junit': '../test-results/sphinx-gallery/junit.xml' if CIRCLECI else '', - 'matplotlib_animations': True, + 'matplotlib_animations': sg_matplotlib_animations, 'min_reported_time': 1, 'plot_gallery': 'True', # sphinx-gallery/913 'reference_url': {'matplotlib': None, 'mpl_toolkits': None}, diff --git a/requirements/doc/doc-requirements.txt b/requirements/doc/doc-requirements.txt index 87bc483b15c0..ee74d02f7146 100644 --- a/requirements/doc/doc-requirements.txt +++ b/requirements/doc/doc-requirements.txt @@ -18,6 +18,7 @@ pydata-sphinx-theme~=0.15.0 mpl-sphinx-theme~=3.9.0 pyyaml sphinxcontrib-svg2pdfconverter>=1.1.0 +sphinxcontrib-video>=0.2.1 sphinx-copybutton sphinx-design sphinx-gallery>=0.12.0 From 8a62afa5dd0fd8e9fa4d30f8960f38d882728212 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 8 Aug 2024 15:34:39 -0400 Subject: [PATCH 0132/1230] BLD: Include MSVCP140 runtime statically This should prevent conflicts with other wheels that use the runtime at a different version. --- .github/workflows/cibuildwheel.yml | 10 +++++++++- doc/api/next_api_changes/development/28687-ES.rst | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 doc/api/next_api_changes/development/28687-ES.rst diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 4e8ea0ab5bf6..9de63b14c4fd 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -100,7 +100,15 @@ jobs: CIBW_AFTER_BUILD: >- twine check {wheel} && python {package}/ci/check_wheel_licenses.py {wheel} - CIBW_CONFIG_SETTINGS: setup-args="--vsenv" + # On Windows, we explicitly request MSVC compilers (as GitHub Action runners have + # MinGW on PATH that would be picked otherwise), switch to a static build for + # runtimes, but use dynamic linking for `VCRUNTIME140.dll`, `VCRUNTIME140_1.dll`, + # and the UCRT. This avoids requiring specific versions of `MSVCP140.dll`, while + # keeping shared state with the rest of the Python process/extensions. + CIBW_CONFIG_SETTINGS_WINDOWS: >- + setup-args="--vsenv" + setup-args="-Db_vscrt=mt" + setup-args="-Dcpp_link_args=['ucrt.lib','vcruntime.lib','/nodefaultlib:libucrt.lib','/nodefaultlib:libvcruntime.lib']" CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 CIBW_SKIP: "*-musllinux_aarch64" CIBW_TEST_COMMAND: >- diff --git a/doc/api/next_api_changes/development/28687-ES.rst b/doc/api/next_api_changes/development/28687-ES.rst new file mode 100644 index 000000000000..339dafdd05d0 --- /dev/null +++ b/doc/api/next_api_changes/development/28687-ES.rst @@ -0,0 +1,10 @@ +Windows wheel runtime bundling made static +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In 3.7.0, the MSVC runtime DLL was bundled in wheels to enable importing Matplotlib on +systems that do not have it installed. However, this could cause inconsistencies with +other wheels that did the same, and trigger random crashes depending on import order. See +`this issue `_ for further +details. + +Since 3.9.2, wheels now bundle the MSVC runtime DLL statically to avoid such issues. From 41afa7d6d84b193a60da20a819bcc54d144e496b Mon Sep 17 00:00:00 2001 From: James Spencer Date: Mon, 12 Aug 2024 11:31:15 +0100 Subject: [PATCH 0133/1230] Avoid division-by-zero in Sketch::Sketch --- src/path_converters.h | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/path_converters.h b/src/path_converters.h index db732e126c3f..6d242e74415b 100644 --- a/src/path_converters.h +++ b/src/path_converters.h @@ -5,6 +5,7 @@ #include #include +#include #include "agg_clip_liang_barsky.h" #include "mplutils.h" @@ -1019,8 +1020,18 @@ class Sketch { rewind(0); const double d_M_PI = 3.14159265358979323846; - m_p_scale = (2.0 * d_M_PI) / (m_length * m_randomness); - m_log_randomness = 2.0 * log(m_randomness); + // Set derived values to zero if m_length or m_randomness are zero to + // avoid divide-by-zero errors when a sketch is created but not used. + if (m_length <= std::numeric_limits::epsilon() || m_randomness <= std::numeric_limits::epsilon()) { + m_p_scale = 0.0; + } else { + m_p_scale = (2.0 * d_M_PI) / (m_length * m_randomness); + } + if (m_randomness <= std::numeric_limits::epsilon()) { + m_log_randomness = 0.0; + } else { + m_log_randomness = 2.0 * log(m_randomness); + } } unsigned vertex(double *x, double *y) From 27cc94b424d8986b52c00ae9ab3ea9048fced794 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 19:57:00 +0000 Subject: [PATCH 0134/1230] Bump actions/attest-build-provenance in the actions group Bumps the actions group with 1 update: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance). Updates `actions/attest-build-provenance` from 1.4.0 to 1.4.1 - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/210c1913531870065f03ce1f9440dd87bc0938cd...310b0a4a3b0b78ef57ecda988ee04b132db73ef8) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/cibuildwheel.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index ddb102b8ffd1..48e9d4358f9c 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -216,7 +216,7 @@ jobs: run: ls dist - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@210c1913531870065f03ce1f9440dd87bc0938cd # v1.4.0 + uses: actions/attest-build-provenance@310b0a4a3b0b78ef57ecda988ee04b132db73ef8 # v1.4.1 with: subject-path: dist/matplotlib-* From 056f307c1eaceb7615594e02b03336ec047ef02d Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 12 Aug 2024 19:25:14 -0400 Subject: [PATCH 0135/1230] DOC: Create release notes for 3.9.2 --- .../api_changes_3.9.2.rst} | 6 + doc/users/github_stats.rst | 236 ++++++------------ .../prev_whats_new/github_stats_3.9.1.rst | 192 ++++++++++++++ doc/users/release_notes.rst | 2 + 4 files changed, 270 insertions(+), 166 deletions(-) rename doc/api/{next_api_changes/development/28687-ES.rst => prev_api_changes/api_changes_3.9.2.rst} (88%) create mode 100644 doc/users/prev_whats_new/github_stats_3.9.1.rst diff --git a/doc/api/next_api_changes/development/28687-ES.rst b/doc/api/prev_api_changes/api_changes_3.9.2.rst similarity index 88% rename from doc/api/next_api_changes/development/28687-ES.rst rename to doc/api/prev_api_changes/api_changes_3.9.2.rst index 339dafdd05d0..4c2a69634502 100644 --- a/doc/api/next_api_changes/development/28687-ES.rst +++ b/doc/api/prev_api_changes/api_changes_3.9.2.rst @@ -1,3 +1,9 @@ +API Changes for 3.9.2 +===================== + +Development +----------- + Windows wheel runtime bundling made static ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/users/github_stats.rst b/doc/users/github_stats.rst index 0c8f29687afb..00c3e5d656a1 100644 --- a/doc/users/github_stats.rst +++ b/doc/users/github_stats.rst @@ -1,195 +1,99 @@ .. _github-stats: -GitHub statistics for 3.9.1 (Jul 04, 2024) +GitHub statistics for 3.9.2 (Aug 12, 2024) ========================================== -GitHub statistics for 2024/05/15 (tag: v3.9.0) - 2024/07/04 +GitHub statistics for 2024/07/04 (tag: v3.9.1) - 2024/08/12 These lists are automatically generated, and may be incomplete or contain duplicates. -We closed 30 issues and merged 111 pull requests. -The full list can be seen `on GitHub `__ +We closed 9 issues and merged 45 pull requests. +The full list can be seen `on GitHub `__ -The following 29 authors contributed 184 commits. +The following 20 authors contributed 67 commits. -* Antony Lee -* Brigitta Sipőcz -* Christian Mattsson -* dale +* Adam J. Stewart +* Anthony Lee +* Caitlin Hathaway +* ClarkeAC * dependabot[bot] * Elliott Sales de Andrade -* Eytan Adler +* Filippo Balzaretti * Greg Lucas -* haaris * hannah * Ian Thomas -* Illviljan -* K900 +* Jody Klymak * Kyle Sunden -* Lumberbot (aka Jack) -* malhar2460 -* Matthew Feickert -* Melissa Weber Mendonça -* MischaMegens2 * Oscar Gustafsson +* Randolf Scholz +* Refael Ackermann * Ruth Comer * Scott Shambaugh -* simond07 -* SjoerdB93 -* Takumasa N -* Takumasa N. -* Takumasa Nakamura +* Sean Smith * Thomas A Caswell * Tim Hoffmann GitHub issues and pull requests: -Pull Requests (111): +Pull Requests (45): -* :ghpull:`28507`: Backport PR #28430 on branch v3.9.x (Fix pickling of AxesWidgets.) -* :ghpull:`28506`: Backport PR #28451 on branch v3.9.x (Fix GTK cairo backends) -* :ghpull:`28430`: Fix pickling of AxesWidgets. -* :ghpull:`25861`: Fix Hidpi scaling for GTK4Cairo -* :ghpull:`28451`: Fix GTK cairo backends -* :ghpull:`28499`: Backport PR #28498 on branch v3.9.x (Don't fail if we can't query system fonts on macOS) -* :ghpull:`28498`: Don't fail if we can't query system fonts on macOS -* :ghpull:`28491`: Backport PR #28487 on branch v3.9.x (Fix autoscaling with axhspan) -* :ghpull:`28490`: Backport PR #28486 on branch v3.9.x (Fix CompositeGenericTransform.contains_branch_seperately) -* :ghpull:`28487`: Fix autoscaling with axhspan -* :ghpull:`28486`: Fix CompositeGenericTransform.contains_branch_seperately -* :ghpull:`28483`: Backport PR #28393 on branch v3.9.x (Make sticky edges only apply if the sticky edge is the most extreme limit point) -* :ghpull:`28482`: Backport PR #28473 on branch v3.9.x (Do not lowercase module:// backends) -* :ghpull:`28393`: Make sticky edges only apply if the sticky edge is the most extreme limit point -* :ghpull:`28473`: Do not lowercase module:// backends -* :ghpull:`28480`: Backport PR #28474 on branch v3.9.x (Fix typing and docs for containers) -* :ghpull:`28479`: Backport PR #28397 (FIX: stale root Figure when adding/updating subfigures) -* :ghpull:`28474`: Fix typing and docs for containers -* :ghpull:`28472`: Backport PR #28289 on branch v3.9.x (Promote mpltype Sphinx role to a public extension) -* :ghpull:`28471`: Backport PR #28342 on branch v3.9.x (DOC: Document the parameter *position* of apply_aspect() as internal) -* :ghpull:`28470`: Backport PR #28398 on branch v3.9.x (Add GIL Release to flush_events in macosx backend) -* :ghpull:`28469`: Backport PR #28355 on branch v3.9.x (MNT: Re-add matplotlib.cm.get_cmap) -* :ghpull:`28397`: FIX: stale root Figure when adding/updating subfigures -* :ghpull:`28289`: Promote mpltype Sphinx role to a public extension -* :ghpull:`28342`: DOC: Document the parameter *position* of apply_aspect() as internal -* :ghpull:`28398`: Add GIL Release to flush_events in macosx backend -* :ghpull:`28355`: MNT: Re-add matplotlib.cm.get_cmap -* :ghpull:`28468`: Backport PR #28465 on branch v3.9.x (Fix pickling of SubFigures) -* :ghpull:`28465`: Fix pickling of SubFigures -* :ghpull:`28462`: Backport PR #28440 on branch v3.9.x (DOC: Add note about simplification of to_polygons) -* :ghpull:`28460`: Backport PR #28459 on branch v3.9.x (DOC: Document kwargs scope for tick setter functions) -* :ghpull:`28461`: Backport PR #28458 on branch v3.9.x (Correct numpy dtype comparisons in image_resample) -* :ghpull:`28440`: DOC: Add note about simplification of to_polygons -* :ghpull:`28458`: Correct numpy dtype comparisons in image_resample -* :ghpull:`28459`: DOC: Document kwargs scope for tick setter functions -* :ghpull:`28450`: Backport of 28371 and 28411 -* :ghpull:`28446`: Backport PR #28403 on branch v3.9.x (FIX: Autoscale support in add_collection3d for Line3DCollection and Poly3DCollection -* :ghpull:`28445`: Backport PR #28403 on branch v3.9.x (FIX: Autoscale support in add_collection3d for Line3DCollection and Poly3DCollection) -* :ghpull:`28438`: Backport PR #28436 on branch v3.9.x (Fix ``is_color_like`` for 2-tuple of strings and fix ``to_rgba`` for ``(nth_color, alpha)``) -* :ghpull:`28403`: FIX: Autoscale support in add_collection3d for Line3DCollection and Poly3DCollection -* :ghpull:`28443`: Backport PR #28441 on branch v3.9.x (MNT: Update basic units example to work with numpy 2.0) -* :ghpull:`28441`: MNT: Update basic units example to work with numpy 2.0 -* :ghpull:`28436`: Fix ``is_color_like`` for 2-tuple of strings and fix ``to_rgba`` for ``(nth_color, alpha)`` -* :ghpull:`28426`: Backport PR #28425 on branch v3.9.x (Fix Circle yaml line length) -* :ghpull:`28427`: Fix circleci yaml -* :ghpull:`28425`: Fix Circle yaml line length -* :ghpull:`28422`: Backport PR #28401 on branch v3.9.x (FIX: Fix text wrapping) -* :ghpull:`28424`: Backport PR #28423 on branch v3.9.x (Update return type for Axes.axhspan and Axes.axvspan) -* :ghpull:`28423`: Update return type for Axes.axhspan and Axes.axvspan -* :ghpull:`28401`: FIX: Fix text wrapping -* :ghpull:`28419`: Backport PR #28414 on branch v3.9.x (Clean up obsolete widget code) -* :ghpull:`28411`: Bump the actions group with 3 updates -* :ghpull:`28414`: Clean up obsolete widget code -* :ghpull:`28415`: Backport PR #28413 on branch v3.9.x (CI: update action that got moved org) -* :ghpull:`28413`: CI: update action that got moved org -* :ghpull:`28392`: Backport PR #28388 on branch v3.9.x (Allow duplicate (name, value) entry points for backends) -* :ghpull:`28362`: Backport PR #28337 on branch v3.9.x (Bump the actions group across 1 directory with 3 updates) -* :ghpull:`28388`: Allow duplicate (name, value) entry points for backends -* :ghpull:`28389`: Backport PR #28380 on branch v3.9.x (Remove outdated docstring section in RendererBase.draw_text.) -* :ghpull:`28380`: Remove outdated docstring section in RendererBase.draw_text. -* :ghpull:`28385`: Backport PR #28377 on branch v3.9.x (DOC: Clarify scope of wrap.) -* :ghpull:`28377`: DOC: Clarify scope of wrap. -* :ghpull:`28368`: Backport PR #28359 on branch v3.9.x (Document that axes unsharing is impossible.) -* :ghpull:`28359`: Document that axes unsharing is impossible. -* :ghpull:`28337`: Bump the actions group across 1 directory with 3 updates -* :ghpull:`28351`: Backport PR #28307 on branch v3.9.x (DOC: New color line by value example) -* :ghpull:`28307`: DOC: New color line by value example -* :ghpull:`28339`: Backport PR #28336 on branch v3.9.x (DOC: Add version warning banner for docs versions different from stable) -* :ghpull:`28336`: DOC: Add version warning banner for docs versions different from stable -* :ghpull:`28334`: Backport PR #28332 on branch v3.9.x (Call IPython.enable_gui when install repl displayhook) -* :ghpull:`28332`: Call IPython.enable_gui when install repl displayhook -* :ghpull:`28331`: Backport PR #28329 on branch v3.9.x (DOC: Add example for 3D intersecting planes) -* :ghpull:`28329`: DOC: Add example for 3D intersecting planes -* :ghpull:`28327`: Backport PR #28292 on branch v3.9.x (Resolve MaxNLocator IndexError when no large steps) -* :ghpull:`28292`: Resolve MaxNLocator IndexError when no large steps -* :ghpull:`28326`: Backport PR #28041 on branch v3.9.x ([BUG]: Shift box_aspect according to vertical_axis) -* :ghpull:`28041`: [BUG]: Shift box_aspect according to vertical_axis -* :ghpull:`28320`: Backport PR #27001 on branch v3.9.x ([TYP] Add overload of ``pyplot.subplots``) -* :ghpull:`27001`: [TYP] Add overload of ``pyplot.subplots`` -* :ghpull:`28318`: Backport PR #28273 on branch v3.9.x (CI: Add GitHub artifact attestations to package distribution) -* :ghpull:`28273`: CI: Add GitHub artifact attestations to package distribution -* :ghpull:`28305`: Backport PR #28303 on branch v3.9.x (Removed drawedges repeated definition from function doc string) -* :ghpull:`28303`: Removed drawedges repeated definition from function doc string -* :ghpull:`28299`: Backport PR #28297 on branch v3.9.x (Solved #28296 Added missing comma) -* :ghpull:`28297`: Solved #28296 Added missing comma -* :ghpull:`28294`: Backport PR #28261 on branch v3.9.x (Correct roll angle units, issue #28256) -* :ghpull:`28261`: Correct roll angle units, issue #28256 -* :ghpull:`28283`: Backport PR #28280 on branch v3.9.x (DOC: Add an example for 2D images in 3D plots) -* :ghpull:`28280`: DOC: Add an example for 2D images in 3D plots -* :ghpull:`28278`: Backport PR #28272 on branch v3.9.x (BLD: Move macos builders from 11 to 12) -* :ghpull:`28277`: Backport PR #28274 on branch v3.9.x (ci: Remove deprecated codeql option) -* :ghpull:`28272`: BLD: Move macos builders from 11 to 12 -* :ghpull:`28274`: ci: Remove deprecated codeql option -* :ghpull:`28270`: Backport PR #28269 on branch v3.9.x (Handle GetForegroundWindow() returning NULL.) -* :ghpull:`28269`: Handle GetForegroundWindow() returning NULL. -* :ghpull:`28266`: Backport PR #28257 on branch v3.9.x (Clean up some Meson-related leftovers) -* :ghpull:`28257`: Clean up some Meson-related leftovers -* :ghpull:`28255`: Backport PR #28254 on branch v3.9.x ([DOC] plot type heading consistency) -* :ghpull:`28254`: [DOC] plot type heading consistency -* :ghpull:`28253`: Backport PR #28252 on branch v3.9.x (DOC: Flip the imshow plot types example to match the other examples) -* :ghpull:`28252`: DOC: Flip the imshow plot types example to match the other examples -* :ghpull:`28247`: Backport PR #28230 on branch v3.9.x (Add extra imports to improve typing) -* :ghpull:`28230`: Add extra imports to improve typing -* :ghpull:`28246`: Backport PR #28243 on branch v3.9.x (DOC: Add more 3D plot types) -* :ghpull:`28243`: DOC: Add more 3D plot types -* :ghpull:`28241`: Backport PR #28219 on branch v3.9.x (Bump the actions group with 2 updates) -* :ghpull:`28219`: Bump the actions group with 2 updates -* :ghpull:`28237`: Backport PR #28233 on branch v3.9.x (CI: Fix font install on macOS/Homebrew) -* :ghpull:`28236`: Backport PR #28231 on branch v3.9.x (DOC: we do not need the blit call in on_draw) -* :ghpull:`28233`: CI: Fix font install on macOS/Homebrew -* :ghpull:`28231`: DOC: we do not need the blit call in on_draw +* :ghpull:`28687`: BLD: Include MSVCP140 runtime statically +* :ghpull:`28679`: Run delvewheel with path to required msvcp140.dll +* :ghpull:`28695`: Backport PR #27797 on branch v3.9.x (DOC: Use video files for saving animations) +* :ghpull:`28688`: Backport PR #28293 and #28668: Enable 3.13 wheels and bump cibuildwheel +* :ghpull:`27797`: DOC: Use video files for saving animations +* :ghpull:`28692`: Backport PR #28632 on branch v3.9.x (DOC: Tell sphinx-gallery to link mpl_toolkits from our build) +* :ghpull:`28632`: DOC: Tell sphinx-gallery to link mpl_toolkits from our build +* :ghpull:`28668`: Bump the actions group with 2 updates +* :ghpull:`28686`: Backport PR #28682 on branch v3.9.x (Fix warnings from mingw compilers) +* :ghpull:`28682`: Fix warnings from mingw compilers +* :ghpull:`28676`: Backport PR #28577 on branch v3.9.x (Copy all internals from initial Tick to lazy ones) +* :ghpull:`28577`: Copy all internals from initial Tick to lazy ones +* :ghpull:`28674`: Backport PR #28650 on branch v3.9.x (remove out of date todos on animation.py) +* :ghpull:`28650`: remove out of date todos on animation.py +* :ghpull:`28656`: Backport PR #28649 on branch v3.9.x (FIX: improve formatting of image values in cases of singular norms) +* :ghpull:`28665`: Backport PR #28546 on branch v3.9.x (DOC: Clarify/simplify example of multiple images with one colorbar) +* :ghpull:`28649`: FIX: improve formatting of image values in cases of singular norms +* :ghpull:`28635`: BLD: windows wheels +* :ghpull:`28645`: Backport PR #28644 on branch v3.9.x (DOC: Fix matching for version switcher) +* :ghpull:`28640`: Backport PR #28634 on branch v3.9.x (Closed open div tag in color.ColorMap._repr_html_) +* :ghpull:`28634`: Closed open div tag in color.ColorMap._repr_html_ +* :ghpull:`28636`: Backport PR #28625 on branch v3.9.x (added typing_extensions.Self to _AxesBase.twinx) +* :ghpull:`28625`: added typing_extensions.Self to _AxesBase.twinx +* :ghpull:`28622`: Backport PR #28621 on branch v3.9.x (TYP: Fix a typo in animation.pyi) +* :ghpull:`28621`: TYP: Fix a typo in animation.pyi +* :ghpull:`28605`: Backport PR #28604 on branch v3.9.x (cycler signature update.) +* :ghpull:`28604`: cycler signature update. +* :ghpull:`28598`: Pin PyQt6 back on Ubuntu 20.04 +* :ghpull:`28596`: Backport PR #28518 on branch v3.9.x ([TYP] Fix overload of ``pyplot.subplots``) +* :ghpull:`28518`: [TYP] Fix overload of ``pyplot.subplots`` +* :ghpull:`28591`: Backport PR #28580 on branch v3.9.x (Bump actions/attest-build-provenance from 1.3.2 to 1.3.3 in the actions group) +* :ghpull:`28580`: Bump actions/attest-build-provenance from 1.3.2 to 1.3.3 in the actions group +* :ghpull:`28586`: Backport PR #28582 on branch v3.9.x (FIX: make sticky edge tolerance relative to data range) +* :ghpull:`28582`: FIX: make sticky edge tolerance relative to data range +* :ghpull:`28572`: Backport PR #28571 on branch v3.9.x (DOC: Add version directive to hatch parameter in stackplot) +* :ghpull:`28571`: DOC: Add version directive to hatch parameter in stackplot +* :ghpull:`28564`: Backport PR #28534 on branch v3.9.x ([BLD] Fix WSL build warning) +* :ghpull:`28563`: Backport PR #28526 on branch v3.9.x (Bump pypa/cibuildwheel from 2.19.1 to 2.19.2 in the actions group) +* :ghpull:`28534`: [BLD] Fix WSL build warning +* :ghpull:`28526`: Bump pypa/cibuildwheel from 2.19.1 to 2.19.2 in the actions group +* :ghpull:`28552`: Backport PR #28541 on branch v3.9.x (MNT: be more careful about disk I/O failures when writing font cache) +* :ghpull:`28541`: MNT: be more careful about disk I/O failures when writing font cache +* :ghpull:`28524`: Backport PR #28523 on branch v3.9.x (Fix value error when set widget size to zero while using FigureCanvasQT ) +* :ghpull:`28523`: Fix value error when set widget size to zero while using FigureCanvasQT +* :ghpull:`28519`: Backport PR #28517 on branch v3.9.x (DOC: better cross referencing for animations) -Issues (30): +Issues (9): -* :ghissue:`22482`: [ENH]: pickle (or save) matplotlib figure with insteractive slider -* :ghissue:`25847`: [Bug]: Graph gets cut off with scaled resolution using gtk4cairo backend -* :ghissue:`28341`: [Bug]: Incorrect X-axis scaling with date values -* :ghissue:`28383`: [Bug]: axvspan no longer participating in limit calculations -* :ghissue:`28223`: [Bug]: Inconsistent Visualization of Intervals in ax.barh for Different Duration Widths -* :ghissue:`28432`: [Bug]: Backend name specified as module gets lowercased since 3.9 -* :ghissue:`28467`: [Bug]: Incorrect type stub for ``ErrorbarContainer``'s ``lines`` attribute. -* :ghissue:`28384`: [Bug]: subfigure artists not drawn interactively -* :ghissue:`28234`: [Bug]: mpltype custom role breaks sphinx build for third-party projects that have intersphinx links to matplotlib -* :ghissue:`28464`: [Bug]: figure with subfigures cannot be pickled -* :ghissue:`28448`: [Bug]: Making an RGB image from pickled data throws error -* :ghissue:`23317`: [Bug]: ``add_collection3d`` does not update view limits -* :ghissue:`17130`: autoscale_view is not working with Line3DCollection -* :ghissue:`28434`: [Bug]: Setting exactly 2 colors with tuple in ``plot`` method gives confusing error -* :ghissue:`28417`: [Doc]: axhspan and axvspan now return Rectangles, not Polygons. -* :ghissue:`28378`: [ENH]: Switch text wrapping boundary to subfigure -* :ghissue:`28404`: [Doc]: matplotlib.widgets.CheckButtons no longer has .rectangles attribute, needs removed. -* :ghissue:`28367`: [Bug]: Backend entry points can be erroneously duplicated -* :ghissue:`28358`: [Bug]: Labels don't get wrapped when set_yticks() is used in subplots -* :ghissue:`28374`: [Bug]: rcParam ``tk.window_focus: True`` is causes crash on Linux in version 3.9.0. -* :ghissue:`28324`: [Bug]: show(block=False) freezes -* :ghissue:`28239`: [Doc]: Gallery example showing 3D slice planes -* :ghissue:`27603`: [Bug]: _raw_ticker() istep -* :ghissue:`24328`: [Bug]: class Axes3D.set_box_aspect() sets wrong aspect ratios when Axes3D.view_init( vertical_axis='y') is enabled. -* :ghissue:`28221`: [Doc]: drawedges attribute described twice in matplotlib.colorbar documentation -* :ghissue:`28296`: [Doc]: Missing comma -* :ghissue:`28256`: [Bug]: axes3d.py's _on_move() converts the roll angle to radians, but then passes it to view_init() as if it were still in degrees -* :ghissue:`28267`: [Bug]: for Python 3.11.9 gor error ValueError: PyCapsule_New called with null pointer -* :ghissue:`28022`: [Bug]: Type of Axes is unknown pyright -* :ghissue:`28002`: Segfault from path editor example with QtAgg +* :ghissue:`28551`: [Bug]: Possible issue with Matplotlib 3.9.1 wheel on Windows only +* :ghissue:`28250`: [Doc]: Sphinx gallery links mispointed for Axes3D methods +* :ghissue:`28574`: [Bug]: Nondeterministic behavior with subplot spacing and constrained layout +* :ghissue:`28626`: [Doc]: Remove old TODO's from animation.py +* :ghissue:`28648`: [Bug]: format_image_data on an image of only zeros produses a large number of zeros +* :ghissue:`28624`: [Bug]: Bad type hint in ``_AxesBase.twinx()`` +* :ghissue:`28567`: [Bug]: sticky edge related changes for datetime plots +* :ghissue:`28533`: [Doc]: Stackplot hatch functionality has version dependencies +* :ghissue:`28538`: [Bug]: Permission denied when importing matplotlib.pyplot Previous GitHub statistics diff --git a/doc/users/prev_whats_new/github_stats_3.9.1.rst b/doc/users/prev_whats_new/github_stats_3.9.1.rst new file mode 100644 index 000000000000..1bd7860546cb --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.9.1.rst @@ -0,0 +1,192 @@ +.. _github-stats-3-9-1: + +GitHub statistics for 3.9.1 (Jul 04, 2024) +========================================== + +GitHub statistics for 2024/05/15 (tag: v3.9.0) - 2024/07/04 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 30 issues and merged 111 pull requests. +The full list can be seen `on GitHub `__ + +The following 29 authors contributed 184 commits. + +* Antony Lee +* Brigitta Sipőcz +* Christian Mattsson +* dale +* dependabot[bot] +* Elliott Sales de Andrade +* Eytan Adler +* Greg Lucas +* haaris +* hannah +* Ian Thomas +* Illviljan +* K900 +* Kyle Sunden +* Lumberbot (aka Jack) +* malhar2460 +* Matthew Feickert +* Melissa Weber Mendonça +* MischaMegens2 +* Oscar Gustafsson +* Ruth Comer +* Scott Shambaugh +* simond07 +* SjoerdB93 +* Takumasa N +* Takumasa N. +* Takumasa Nakamura +* Thomas A Caswell +* Tim Hoffmann + +GitHub issues and pull requests: + +Pull Requests (111): + +* :ghpull:`28507`: Backport PR #28430 on branch v3.9.x (Fix pickling of AxesWidgets.) +* :ghpull:`28506`: Backport PR #28451 on branch v3.9.x (Fix GTK cairo backends) +* :ghpull:`28430`: Fix pickling of AxesWidgets. +* :ghpull:`25861`: Fix Hidpi scaling for GTK4Cairo +* :ghpull:`28451`: Fix GTK cairo backends +* :ghpull:`28499`: Backport PR #28498 on branch v3.9.x (Don't fail if we can't query system fonts on macOS) +* :ghpull:`28498`: Don't fail if we can't query system fonts on macOS +* :ghpull:`28491`: Backport PR #28487 on branch v3.9.x (Fix autoscaling with axhspan) +* :ghpull:`28490`: Backport PR #28486 on branch v3.9.x (Fix CompositeGenericTransform.contains_branch_seperately) +* :ghpull:`28487`: Fix autoscaling with axhspan +* :ghpull:`28486`: Fix CompositeGenericTransform.contains_branch_seperately +* :ghpull:`28483`: Backport PR #28393 on branch v3.9.x (Make sticky edges only apply if the sticky edge is the most extreme limit point) +* :ghpull:`28482`: Backport PR #28473 on branch v3.9.x (Do not lowercase module:// backends) +* :ghpull:`28393`: Make sticky edges only apply if the sticky edge is the most extreme limit point +* :ghpull:`28473`: Do not lowercase module:// backends +* :ghpull:`28480`: Backport PR #28474 on branch v3.9.x (Fix typing and docs for containers) +* :ghpull:`28479`: Backport PR #28397 (FIX: stale root Figure when adding/updating subfigures) +* :ghpull:`28474`: Fix typing and docs for containers +* :ghpull:`28472`: Backport PR #28289 on branch v3.9.x (Promote mpltype Sphinx role to a public extension) +* :ghpull:`28471`: Backport PR #28342 on branch v3.9.x (DOC: Document the parameter *position* of apply_aspect() as internal) +* :ghpull:`28470`: Backport PR #28398 on branch v3.9.x (Add GIL Release to flush_events in macosx backend) +* :ghpull:`28469`: Backport PR #28355 on branch v3.9.x (MNT: Re-add matplotlib.cm.get_cmap) +* :ghpull:`28397`: FIX: stale root Figure when adding/updating subfigures +* :ghpull:`28289`: Promote mpltype Sphinx role to a public extension +* :ghpull:`28342`: DOC: Document the parameter *position* of apply_aspect() as internal +* :ghpull:`28398`: Add GIL Release to flush_events in macosx backend +* :ghpull:`28355`: MNT: Re-add matplotlib.cm.get_cmap +* :ghpull:`28468`: Backport PR #28465 on branch v3.9.x (Fix pickling of SubFigures) +* :ghpull:`28465`: Fix pickling of SubFigures +* :ghpull:`28462`: Backport PR #28440 on branch v3.9.x (DOC: Add note about simplification of to_polygons) +* :ghpull:`28460`: Backport PR #28459 on branch v3.9.x (DOC: Document kwargs scope for tick setter functions) +* :ghpull:`28461`: Backport PR #28458 on branch v3.9.x (Correct numpy dtype comparisons in image_resample) +* :ghpull:`28440`: DOC: Add note about simplification of to_polygons +* :ghpull:`28458`: Correct numpy dtype comparisons in image_resample +* :ghpull:`28459`: DOC: Document kwargs scope for tick setter functions +* :ghpull:`28450`: Backport of 28371 and 28411 +* :ghpull:`28446`: Backport PR #28403 on branch v3.9.x (FIX: Autoscale support in add_collection3d for Line3DCollection and Poly3DCollection +* :ghpull:`28445`: Backport PR #28403 on branch v3.9.x (FIX: Autoscale support in add_collection3d for Line3DCollection and Poly3DCollection) +* :ghpull:`28438`: Backport PR #28436 on branch v3.9.x (Fix ``is_color_like`` for 2-tuple of strings and fix ``to_rgba`` for ``(nth_color, alpha)``) +* :ghpull:`28403`: FIX: Autoscale support in add_collection3d for Line3DCollection and Poly3DCollection +* :ghpull:`28443`: Backport PR #28441 on branch v3.9.x (MNT: Update basic units example to work with numpy 2.0) +* :ghpull:`28441`: MNT: Update basic units example to work with numpy 2.0 +* :ghpull:`28436`: Fix ``is_color_like`` for 2-tuple of strings and fix ``to_rgba`` for ``(nth_color, alpha)`` +* :ghpull:`28426`: Backport PR #28425 on branch v3.9.x (Fix Circle yaml line length) +* :ghpull:`28427`: Fix circleci yaml +* :ghpull:`28425`: Fix Circle yaml line length +* :ghpull:`28422`: Backport PR #28401 on branch v3.9.x (FIX: Fix text wrapping) +* :ghpull:`28424`: Backport PR #28423 on branch v3.9.x (Update return type for Axes.axhspan and Axes.axvspan) +* :ghpull:`28423`: Update return type for Axes.axhspan and Axes.axvspan +* :ghpull:`28401`: FIX: Fix text wrapping +* :ghpull:`28419`: Backport PR #28414 on branch v3.9.x (Clean up obsolete widget code) +* :ghpull:`28411`: Bump the actions group with 3 updates +* :ghpull:`28414`: Clean up obsolete widget code +* :ghpull:`28415`: Backport PR #28413 on branch v3.9.x (CI: update action that got moved org) +* :ghpull:`28413`: CI: update action that got moved org +* :ghpull:`28392`: Backport PR #28388 on branch v3.9.x (Allow duplicate (name, value) entry points for backends) +* :ghpull:`28362`: Backport PR #28337 on branch v3.9.x (Bump the actions group across 1 directory with 3 updates) +* :ghpull:`28388`: Allow duplicate (name, value) entry points for backends +* :ghpull:`28389`: Backport PR #28380 on branch v3.9.x (Remove outdated docstring section in RendererBase.draw_text.) +* :ghpull:`28380`: Remove outdated docstring section in RendererBase.draw_text. +* :ghpull:`28385`: Backport PR #28377 on branch v3.9.x (DOC: Clarify scope of wrap.) +* :ghpull:`28377`: DOC: Clarify scope of wrap. +* :ghpull:`28368`: Backport PR #28359 on branch v3.9.x (Document that axes unsharing is impossible.) +* :ghpull:`28359`: Document that axes unsharing is impossible. +* :ghpull:`28337`: Bump the actions group across 1 directory with 3 updates +* :ghpull:`28351`: Backport PR #28307 on branch v3.9.x (DOC: New color line by value example) +* :ghpull:`28307`: DOC: New color line by value example +* :ghpull:`28339`: Backport PR #28336 on branch v3.9.x (DOC: Add version warning banner for docs versions different from stable) +* :ghpull:`28336`: DOC: Add version warning banner for docs versions different from stable +* :ghpull:`28334`: Backport PR #28332 on branch v3.9.x (Call IPython.enable_gui when install repl displayhook) +* :ghpull:`28332`: Call IPython.enable_gui when install repl displayhook +* :ghpull:`28331`: Backport PR #28329 on branch v3.9.x (DOC: Add example for 3D intersecting planes) +* :ghpull:`28329`: DOC: Add example for 3D intersecting planes +* :ghpull:`28327`: Backport PR #28292 on branch v3.9.x (Resolve MaxNLocator IndexError when no large steps) +* :ghpull:`28292`: Resolve MaxNLocator IndexError when no large steps +* :ghpull:`28326`: Backport PR #28041 on branch v3.9.x ([BUG]: Shift box_aspect according to vertical_axis) +* :ghpull:`28041`: [BUG]: Shift box_aspect according to vertical_axis +* :ghpull:`28320`: Backport PR #27001 on branch v3.9.x ([TYP] Add overload of ``pyplot.subplots``) +* :ghpull:`27001`: [TYP] Add overload of ``pyplot.subplots`` +* :ghpull:`28318`: Backport PR #28273 on branch v3.9.x (CI: Add GitHub artifact attestations to package distribution) +* :ghpull:`28273`: CI: Add GitHub artifact attestations to package distribution +* :ghpull:`28305`: Backport PR #28303 on branch v3.9.x (Removed drawedges repeated definition from function doc string) +* :ghpull:`28303`: Removed drawedges repeated definition from function doc string +* :ghpull:`28299`: Backport PR #28297 on branch v3.9.x (Solved #28296 Added missing comma) +* :ghpull:`28297`: Solved #28296 Added missing comma +* :ghpull:`28294`: Backport PR #28261 on branch v3.9.x (Correct roll angle units, issue #28256) +* :ghpull:`28261`: Correct roll angle units, issue #28256 +* :ghpull:`28283`: Backport PR #28280 on branch v3.9.x (DOC: Add an example for 2D images in 3D plots) +* :ghpull:`28280`: DOC: Add an example for 2D images in 3D plots +* :ghpull:`28278`: Backport PR #28272 on branch v3.9.x (BLD: Move macos builders from 11 to 12) +* :ghpull:`28277`: Backport PR #28274 on branch v3.9.x (ci: Remove deprecated codeql option) +* :ghpull:`28272`: BLD: Move macos builders from 11 to 12 +* :ghpull:`28274`: ci: Remove deprecated codeql option +* :ghpull:`28270`: Backport PR #28269 on branch v3.9.x (Handle GetForegroundWindow() returning NULL.) +* :ghpull:`28269`: Handle GetForegroundWindow() returning NULL. +* :ghpull:`28266`: Backport PR #28257 on branch v3.9.x (Clean up some Meson-related leftovers) +* :ghpull:`28257`: Clean up some Meson-related leftovers +* :ghpull:`28255`: Backport PR #28254 on branch v3.9.x ([DOC] plot type heading consistency) +* :ghpull:`28254`: [DOC] plot type heading consistency +* :ghpull:`28253`: Backport PR #28252 on branch v3.9.x (DOC: Flip the imshow plot types example to match the other examples) +* :ghpull:`28252`: DOC: Flip the imshow plot types example to match the other examples +* :ghpull:`28247`: Backport PR #28230 on branch v3.9.x (Add extra imports to improve typing) +* :ghpull:`28230`: Add extra imports to improve typing +* :ghpull:`28246`: Backport PR #28243 on branch v3.9.x (DOC: Add more 3D plot types) +* :ghpull:`28243`: DOC: Add more 3D plot types +* :ghpull:`28241`: Backport PR #28219 on branch v3.9.x (Bump the actions group with 2 updates) +* :ghpull:`28219`: Bump the actions group with 2 updates +* :ghpull:`28237`: Backport PR #28233 on branch v3.9.x (CI: Fix font install on macOS/Homebrew) +* :ghpull:`28236`: Backport PR #28231 on branch v3.9.x (DOC: we do not need the blit call in on_draw) +* :ghpull:`28233`: CI: Fix font install on macOS/Homebrew +* :ghpull:`28231`: DOC: we do not need the blit call in on_draw + +Issues (30): + +* :ghissue:`22482`: [ENH]: pickle (or save) matplotlib figure with insteractive slider +* :ghissue:`25847`: [Bug]: Graph gets cut off with scaled resolution using gtk4cairo backend +* :ghissue:`28341`: [Bug]: Incorrect X-axis scaling with date values +* :ghissue:`28383`: [Bug]: axvspan no longer participating in limit calculations +* :ghissue:`28223`: [Bug]: Inconsistent Visualization of Intervals in ax.barh for Different Duration Widths +* :ghissue:`28432`: [Bug]: Backend name specified as module gets lowercased since 3.9 +* :ghissue:`28467`: [Bug]: Incorrect type stub for ``ErrorbarContainer``'s ``lines`` attribute. +* :ghissue:`28384`: [Bug]: subfigure artists not drawn interactively +* :ghissue:`28234`: [Bug]: mpltype custom role breaks sphinx build for third-party projects that have intersphinx links to matplotlib +* :ghissue:`28464`: [Bug]: figure with subfigures cannot be pickled +* :ghissue:`28448`: [Bug]: Making an RGB image from pickled data throws error +* :ghissue:`23317`: [Bug]: ``add_collection3d`` does not update view limits +* :ghissue:`17130`: autoscale_view is not working with Line3DCollection +* :ghissue:`28434`: [Bug]: Setting exactly 2 colors with tuple in ``plot`` method gives confusing error +* :ghissue:`28417`: [Doc]: axhspan and axvspan now return Rectangles, not Polygons. +* :ghissue:`28378`: [ENH]: Switch text wrapping boundary to subfigure +* :ghissue:`28404`: [Doc]: matplotlib.widgets.CheckButtons no longer has .rectangles attribute, needs removed. +* :ghissue:`28367`: [Bug]: Backend entry points can be erroneously duplicated +* :ghissue:`28358`: [Bug]: Labels don't get wrapped when set_yticks() is used in subplots +* :ghissue:`28374`: [Bug]: rcParam ``tk.window_focus: True`` is causes crash on Linux in version 3.9.0. +* :ghissue:`28324`: [Bug]: show(block=False) freezes +* :ghissue:`28239`: [Doc]: Gallery example showing 3D slice planes +* :ghissue:`27603`: [Bug]: _raw_ticker() istep +* :ghissue:`24328`: [Bug]: class Axes3D.set_box_aspect() sets wrong aspect ratios when Axes3D.view_init( vertical_axis='y') is enabled. +* :ghissue:`28221`: [Doc]: drawedges attribute described twice in matplotlib.colorbar documentation +* :ghissue:`28296`: [Doc]: Missing comma +* :ghissue:`28256`: [Bug]: axes3d.py's _on_move() converts the roll angle to radians, but then passes it to view_init() as if it were still in degrees +* :ghissue:`28267`: [Bug]: for Python 3.11.9 gor ValueError: PyCapsule_New called with null pointer +* :ghissue:`28022`: [Bug]: Type of Axes is unknown pyright +* :ghissue:`28002`: Segfault from path editor example with QtAgg diff --git a/doc/users/release_notes.rst b/doc/users/release_notes.rst index 1204450f6c05..74bc0f13bf1f 100644 --- a/doc/users/release_notes.rst +++ b/doc/users/release_notes.rst @@ -19,9 +19,11 @@ Version 3.9 :maxdepth: 1 prev_whats_new/whats_new_3.9.0.rst + ../api/prev_api_changes/api_changes_3.9.2.rst ../api/prev_api_changes/api_changes_3.9.1.rst ../api/prev_api_changes/api_changes_3.9.0.rst github_stats.rst + prev_whats_new/github_stats_3.9.1.rst prev_whats_new/github_stats_3.9.0.rst Version 3.8 From a254b687df97cda8c6affa37a1dfcf213f8e6c3a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 12 Aug 2024 19:35:24 -0400 Subject: [PATCH 0136/1230] REL: 3.9.2 This is the second bugfix release of the 3.9.x series. This release contains several bug-fixes and adjustments: - Be more resilient to I/O failures when writing font cache - Fix nondeterministic behavior with subplot spacing and constrained layout - Fix sticky edge tolerance relative to data range - Improve formatting of image values in cases of singular norms Windows wheels now bundle the MSVC runtime DLL statically to avoid inconsistencies with other wheels and random crashes depending on import order. From 4b30b1d938b1ccad1e96b35ec11292e9fb8f05fd Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 12 Aug 2024 19:52:20 -0400 Subject: [PATCH 0137/1230] BLD: bump branch away from tag So the tarballs from GitHub are stable. From 3aea791026cabe9b9bdaba6d9a23c122bbf04115 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 12 Aug 2024 20:13:22 -0400 Subject: [PATCH 0138/1230] DOC: Add Zenodo DOI for 3.9.2 --- doc/_static/zenodo_cache/13308876.svg | 35 +++++++++++++++++++++++++++ doc/project/citing.rst | 3 +++ tools/cache_zenodo_svg.py | 1 + 3 files changed, 39 insertions(+) create mode 100644 doc/_static/zenodo_cache/13308876.svg diff --git a/doc/_static/zenodo_cache/13308876.svg b/doc/_static/zenodo_cache/13308876.svg new file mode 100644 index 000000000000..749bc3c19026 --- /dev/null +++ b/doc/_static/zenodo_cache/13308876.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.13308876 + + + 10.5281/zenodo.13308876 + + + \ No newline at end of file diff --git a/doc/project/citing.rst b/doc/project/citing.rst index e0b99995ad11..38c989fca195 100644 --- a/doc/project/citing.rst +++ b/doc/project/citing.rst @@ -32,6 +32,9 @@ By version .. START OF AUTOGENERATED +v3.9.2 + .. image:: ../_static/zenodo_cache/13308876.svg + :target: https://doi.org/10.5281/zenodo.13308876 v3.9.1 .. image:: ../_static/zenodo_cache/12652732.svg :target: https://doi.org/10.5281/zenodo.12652732 diff --git a/tools/cache_zenodo_svg.py b/tools/cache_zenodo_svg.py index 1dc2fbba020b..40814d21573c 100644 --- a/tools/cache_zenodo_svg.py +++ b/tools/cache_zenodo_svg.py @@ -63,6 +63,7 @@ def _get_xdg_cache_dir(): if __name__ == "__main__": data = { + "v3.9.2": "13308876", "v3.9.1": "12652732", "v3.9.0": "11201097", "v3.8.4": "10916799", From d04b2f64fe2fdc3f83707229d64d1516bb56405d Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 12 Aug 2024 21:55:17 -0400 Subject: [PATCH 0139/1230] DOC: Fix a typo in GitHub stats --- doc/users/github_stats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/users/github_stats.rst b/doc/users/github_stats.rst index 00c3e5d656a1..d357a6759d30 100644 --- a/doc/users/github_stats.rst +++ b/doc/users/github_stats.rst @@ -89,7 +89,7 @@ Issues (9): * :ghissue:`28250`: [Doc]: Sphinx gallery links mispointed for Axes3D methods * :ghissue:`28574`: [Bug]: Nondeterministic behavior with subplot spacing and constrained layout * :ghissue:`28626`: [Doc]: Remove old TODO's from animation.py -* :ghissue:`28648`: [Bug]: format_image_data on an image of only zeros produses a large number of zeros +* :ghissue:`28648`: [Bug]: format_image_data on an image of only zeros produces a large number of zeros * :ghissue:`28624`: [Bug]: Bad type hint in ``_AxesBase.twinx()`` * :ghissue:`28567`: [Bug]: sticky edge related changes for datetime plots * :ghissue:`28533`: [Doc]: Stackplot hatch functionality has version dependencies From 0e57cc43350d8f44252535af306a5c010769a558 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 13 Aug 2024 02:54:56 -0400 Subject: [PATCH 0140/1230] DOC: Add a few more notes to release guide I make releases in a new worktree, so it doesn't always have pre-commit setup, so I always forget to check `codespell`, and so making a release can introduce issues from the Issue/PR titles in the GitHub stats. Also, note another file to update in the documentation repo. --- doc/devel/release_guide.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/devel/release_guide.rst b/doc/devel/release_guide.rst index 4ec8319db20e..0e0ebb98fd1d 100644 --- a/doc/devel/release_guide.rst +++ b/doc/devel/release_guide.rst @@ -143,7 +143,8 @@ prepare this list: --project 'matplotlib/matplotlib' --links > doc/users/github_stats.rst 3. Review and commit changes. Some issue/PR titles may not be valid reST (the most - common issue is ``*`` which is interpreted as unclosed markup). + common issue is ``*`` which is interpreted as unclosed markup). Also confirm that + ``codespell`` does not find any issues. .. note:: @@ -450,7 +451,7 @@ which will copy the built docs over. If this is a final release, link the rm stable ln -s 3.7.0 stable -You will also need to edit :file:`sitemap.xml` to include +You will also need to edit :file:`sitemap.xml` and :file:`versions.html` to include the newly released version. Now commit and push everything to GitHub :: git add * From b7fc61e3edf6a4a4ce7ea2ea5b26f3df56c36182 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 13 Aug 2024 04:18:45 -0400 Subject: [PATCH 0141/1230] DOC: Mark 3.9.2 as the stable version --- doc/_static/switcher.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/_static/switcher.json b/doc/_static/switcher.json index 1ceeb2c259cc..5a48ec138f4d 100644 --- a/doc/_static/switcher.json +++ b/doc/_static/switcher.json @@ -1,7 +1,7 @@ [ { "name": "3.9 (stable)", - "version": "3.9.1", + "version": "3.9.2", "url": "https://matplotlib.org/stable/", "preferred": true }, From 50c671779c4efd5d63d1dd3f947a37d47c481d59 Mon Sep 17 00:00:00 2001 From: David Bakaj <106930686+dbakaj@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:15:28 +0100 Subject: [PATCH 0142/1230] Fixed arrowstyle doc interpolation in FancyPatch.set_arrow() #28698. (#28704) * Fixed arrowstyle doc interpolation in FancyPatch.set_arrow() #28698. * Fixed arrowstyle doc interpolation in FancyPatch.set_arrow() #28698. --- lib/matplotlib/patches.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index bc75e6923879..ed676543fc5b 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -4326,6 +4326,7 @@ def get_connectionstyle(self): """Return the `ConnectionStyle` used.""" return self._connector + @_docstring.dedent_interpd def set_arrowstyle(self, arrowstyle=None, **kwargs): """ Set the arrow style, possibly with further attributes. From 6d4e601cf9e8a8d5f963df91f8aed50fa5c38a81 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Mon, 12 Aug 2024 20:59:35 +0100 Subject: [PATCH 0143/1230] DOC: clarify alpha handling for indicate_inset[_zoom] --- lib/matplotlib/axes/_axes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 2c9cc8cc1e9a..598a954f8819 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -452,8 +452,10 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, edgecolor : :mpltype:`color`, default: '0.5' Color of the rectangle and color of the connecting lines. - alpha : float, default: 0.5 - Transparency of the rectangle and connector lines. + alpha : float or None, default: 0.5 + Transparency of the rectangle and connector lines. If not + ``None``, this overrides any alpha value included in the + *facecolor* and *edgecolor* parameters. zorder : float, default: 4.99 Drawing order of the rectangle and connector lines. The default, From 8ae9f536f9574a00bdf5aa795dcb323eb9d9ba38 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 13 Aug 2024 16:23:17 -0400 Subject: [PATCH 0144/1230] DOC: Update missing references for numpydoc 1.8.0 The numpydoc 1.8.0 release re-ordered some sections [1] in generated docs, which caused our missing reference extension to no longer ignore some entries since they are at different lines. Fixes #28715 [1] https://github.com/numpy/numpydoc/pull/571 --- doc/missing-references.json | 118 ++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/doc/missing-references.json b/doc/missing-references.json index 7eb45863589d..a93a03b6ef73 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -81,28 +81,28 @@ "lib/matplotlib/scale.py:docstring of matplotlib.scale.ScaleBase:8" ], "output_dims": [ - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.AitoffAxes.AitoffTransform.transform_non_affine:20", - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.AitoffAxes.InvertedAitoffTransform.transform_non_affine:20", - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.HammerAxes.HammerTransform.transform_non_affine:20", - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.HammerAxes.InvertedHammerTransform.transform_non_affine:20", - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.LambertAxes.InvertedLambertTransform.transform_non_affine:20", - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.LambertAxes.LambertTransform.transform_non_affine:20", - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.MollweideAxes.InvertedMollweideTransform.transform_non_affine:20", - "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.MollweideAxes.MollweideTransform.transform_non_affine:20", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform:14", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform_affine:21", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform_non_affine:20", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.CompositeGenericTransform.transform_affine:21", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.CompositeGenericTransform.transform_non_affine:20", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform:14", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform_affine:21", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform_non_affine:20" + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.AitoffAxes.AitoffTransform.transform_non_affine:22", + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.AitoffAxes.InvertedAitoffTransform.transform_non_affine:22", + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.HammerAxes.HammerTransform.transform_non_affine:22", + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.HammerAxes.InvertedHammerTransform.transform_non_affine:22", + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.LambertAxes.InvertedLambertTransform.transform_non_affine:22", + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.LambertAxes.LambertTransform.transform_non_affine:22", + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.MollweideAxes.InvertedMollweideTransform.transform_non_affine:22", + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.MollweideAxes.MollweideTransform.transform_non_affine:22", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform:16", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform_affine:23", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform_non_affine:22", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.CompositeGenericTransform.transform_affine:23", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.CompositeGenericTransform.transform_non_affine:22", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform:16", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform_affine:23", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform_non_affine:22" ], "triangulation": [ "lib/matplotlib/tri/_trirefine.py:docstring of matplotlib.tri._trirefine.UniformTriRefiner.refine_triangulation:2" ], "use_sticky_edges": [ - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.margins:57" + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.margins:59" ], "width": [ "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Bbox.bounds:2" @@ -274,11 +274,11 @@ }, "py:data": { "matplotlib.axes.Axes.transAxes": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:248", - "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:249", - "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:201", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:249", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:248" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:250", + "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:251", + "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:209", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:251", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:250" ] }, "py:meth": { @@ -299,13 +299,13 @@ "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:9" ], "matplotlib.collections._CollectionWithSizes.set_sizes": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.barbs:177", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.broken_barh:82", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_between:118", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_betweenx:118", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.hexbin:211", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolor:180", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.quiver:213", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.barbs:179", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.broken_barh:84", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_between:120", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_betweenx:120", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.hexbin:213", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolor:182", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.quiver:215", "lib/matplotlib/collections.py:docstring of matplotlib.artist.AsteriskPolygonCollection.set:44", "lib/matplotlib/collections.py:docstring of matplotlib.artist.CircleCollection.set:44", "lib/matplotlib/collections.py:docstring of matplotlib.artist.PathCollection.set:44", @@ -313,25 +313,25 @@ "lib/matplotlib/collections.py:docstring of matplotlib.artist.PolyQuadMesh.set:44", "lib/matplotlib/collections.py:docstring of matplotlib.artist.RegularPolyCollection.set:44", "lib/matplotlib/collections.py:docstring of matplotlib.artist.StarPolygonCollection.set:44", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.barbs:177", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.broken_barh:82", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_between:118", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_betweenx:118", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.hexbin:211", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolor:180", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.quiver:213", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.barbs:179", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.broken_barh:84", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_between:120", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_betweenx:120", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.hexbin:213", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolor:182", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.quiver:215", "lib/matplotlib/quiver.py:docstring of matplotlib.artist.Barbs.set:45", "lib/matplotlib/quiver.py:docstring of matplotlib.artist.Quiver.set:45", - "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:210", - "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Quiver:249", + "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:212", + "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Quiver:251", "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Path3DCollection.set:46", "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Poly3DCollection.set:44" ], "matplotlib.collections._MeshData.set_array": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolormesh:162", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolormesh:164", "lib/matplotlib/collections.py:docstring of matplotlib.artist.PolyQuadMesh.set:17", "lib/matplotlib/collections.py:docstring of matplotlib.artist.QuadMesh.set:17", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolormesh:162" + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolormesh:164" ] }, "py:obj": { @@ -359,10 +359,10 @@ "doc/users/explain/figure/event_handling.rst:568" ], "QuadContourSet.changed()": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contour:154", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contourf:154", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contour:154", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contourf:154" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contour:156", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contourf:156", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contour:156", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contourf:156" ], "Rectangle.contains": [ "doc/users/explain/figure/event_handling.rst:280" @@ -377,7 +377,7 @@ ], "ToolContainer": [ "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase.remove_toolitem:8", - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase:20" + "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase:9" ], "_iter_collection": [ "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.RendererBase.draw_path_collection:15", @@ -394,21 +394,21 @@ "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.PathEffectRenderer.draw_path_collection:15" ], "_read": [ - "lib/matplotlib/dviread.py:docstring of matplotlib.dviread.Vf:20" + "lib/matplotlib/dviread.py:docstring of matplotlib.dviread.Vf:22" ], "active": [ - "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.AxesWidget:32" + "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.AxesWidget:21" ], "ax.transAxes": [ "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.indicate_inset:19", "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.inset_axes:11" ], "axes.bbox": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:144", - "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:145", - "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:97", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:145", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:144" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:146", + "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:147", + "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:105", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:147", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:146" ], "can_composite": [ "lib/matplotlib/image.py:docstring of matplotlib.image.composite_images:9" @@ -420,11 +420,11 @@ "lib/matplotlib/backends/backend_agg.py:docstring of matplotlib.backends.backend_agg.RendererAgg.option_scale_image:2" ], "figure.bbox": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:144", - "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:145", - "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:97", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:145", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:144" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:146", + "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:147", + "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:105", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:147", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:146" ], "fmt_xdata": [ "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.format_xdata:4" @@ -439,12 +439,12 @@ "doc/users/explain/figure/interactive.rst:361" ], "kde.covariance_factor": [ - "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:40" + "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:29" ], "kde.factor": [ "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.violinplot:58", "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:12", - "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:44", + "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:33", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.violinplot:58" ], "make_image": [ From 61d019b30ea44863451685f05938cd549fd174d4 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:38:26 +0200 Subject: [PATCH 0145/1230] DOC: Clarify axhline() uses axes coordinates Closes #28612. --- .../artists/transforms_tutorial.py | 1 + lib/matplotlib/axes/_axes.py | 26 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/galleries/users_explain/artists/transforms_tutorial.py b/galleries/users_explain/artists/transforms_tutorial.py index 8eed53c812b8..0be5fa3c2e21 100644 --- a/galleries/users_explain/artists/transforms_tutorial.py +++ b/galleries/users_explain/artists/transforms_tutorial.py @@ -22,6 +22,7 @@ :class:`~matplotlib.figure.Figure` instance, and ``subfigure`` is a :class:`~matplotlib.figure.SubFigure` instance. +.. _coordinate-systems: +----------------+-----------------------------------+-----------------------------+ |Coordinate |Description |Transformation object | diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 598a954f8819..243c175a1e5f 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -767,20 +767,25 @@ def annotate(self, text, xy, xytext=None, xycoords='data', textcoords=None, @_docstring.dedent_interpd def axhline(self, y=0, xmin=0, xmax=1, **kwargs): """ - Add a horizontal line across the Axes. + Add a horizontal line spanning the whole or fraction of the Axes. + + Note: If you want to set x-limits in data coordinates, use + `~.Axes.hlines` instead. Parameters ---------- y : float, default: 0 - y position in data coordinates of the horizontal line. + y position in :ref:`data coordinates `. xmin : float, default: 0 - Should be between 0 and 1, 0 being the far left of the plot, 1 the - far right of the plot. + The start x-position in :ref:`axes coordinates `. + Should be between 0 and 1, 0 being the far left of the plot, + 1 the far right of the plot. xmax : float, default: 1 - Should be between 0 and 1, 0 being the far left of the plot, 1 the - far right of the plot. + The end x-position in :ref:`axes coordinates `. + Should be between 0 and 1, 0 being the far left of the plot, + 1 the far right of the plot. Returns ------- @@ -836,18 +841,23 @@ def axhline(self, y=0, xmin=0, xmax=1, **kwargs): @_docstring.dedent_interpd def axvline(self, x=0, ymin=0, ymax=1, **kwargs): """ - Add a vertical line across the Axes. + Add a vertical line spanning the whole or fraction of the Axes. + + Note: If you want to set y-limits in data coordinates, use + `~.Axes.vlines` instead. Parameters ---------- x : float, default: 0 - x position in data coordinates of the vertical line. + y position in :ref:`data coordinates `. ymin : float, default: 0 + The start y-position in :ref:`axes coordinates `. Should be between 0 and 1, 0 being the bottom of the plot, 1 the top of the plot. ymax : float, default: 1 + The end y-position in :ref:`axes coordinates `. Should be between 0 and 1, 0 being the bottom of the plot, 1 the top of the plot. From 7ac8eee0c17855e1b4a7b4cd72019c71a76542a5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 14 Aug 2024 16:06:02 -0400 Subject: [PATCH 0146/1230] ci: Skip GTK4 on macOS 12 temporarily This causes homebrew to update Python, but because the image is outdated, this causes conflicts. --- .github/workflows/tests.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8c27b09f1ad5..1f15607df709 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -158,7 +158,12 @@ jobs: ;; macOS) brew update - brew install ccache ghostscript gobject-introspection gtk4 ninja + brew install ccache ghostscript ninja + # The macOS 12 images have an older Python, and this causes homebrew to generate conflicts. + # We'll just skip GTK for now, to not pull in Python updates. + if [[ "${{ matrix.os }}" = macos-14 ]]; then + brew install gobject-introspection gtk4 + fi brew install --cask font-noto-sans-cjk inkscape ;; esac From 9c88d13af664bcdc28bab3234172435629689f20 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 14 Aug 2024 16:44:38 -0400 Subject: [PATCH 0147/1230] TST: Guard against PyGObject existing, but not gobject-introspection --- lib/matplotlib/tests/test_backends_interactive.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index d624b5db0ac2..2c6b61a48438 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -81,10 +81,18 @@ def _get_available_interactive_backends(): elif env["MPLBACKEND"] == 'macosx' and os.environ.get('TF_BUILD'): reason = "macosx backend fails on Azure" elif env["MPLBACKEND"].startswith('gtk'): - import gi # type: ignore + try: + import gi # type: ignore + except ImportError: + # Though we check that `gi` exists above, it is possible that its + # C-level dependencies are not available, and then it still raises an + # `ImportError`, so guard against that. + available_gtk_versions = [] + else: + gi_repo = gi.Repository.get_default() + available_gtk_versions = gi_repo.enumerate_versions('Gtk') version = env["MPLBACKEND"][3] - repo = gi.Repository.get_default() - if f'{version}.0' not in repo.enumerate_versions('Gtk'): + if f'{version}.0' not in available_gtk_versions: reason = "no usable GTK bindings" marks = [] if reason: From 4047b5ea85e3bff6b6f88e224066bfe991f2b5a5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 14 Aug 2024 16:58:08 -0400 Subject: [PATCH 0148/1230] ci: Disable eager upgrades from Homebrew GitHub and Azure's images install a pre-existing Python, which Homebrew attempts to upgrade if any of the packages we want to install depend on it. Unfortunately, this may cause conflicts on Python's symlinks, so stop Homebrew from trying to do an automatic upgrade. --- .github/workflows/tests.yml | 1 + azure-pipelines.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1f15607df709..0c71577c0875 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -158,6 +158,7 @@ jobs: ;; macOS) brew update + export HOMEBREW_NO_INSTALL_UPGRADE=1 HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install ccache ghostscript ninja # The macOS 12 images have an older Python, and this causes homebrew to generate conflicts. # We'll just skip GTK for now, to not pull in Python updates. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 35c95c3b1f94..7919e9512f48 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -111,6 +111,7 @@ stages: ;; Darwin) brew update + export HOMEBREW_NO_INSTALL_UPGRADE=1 HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install --cask xquartz brew install ccache ffmpeg imagemagick mplayer ninja pkg-config brew install --cask font-noto-sans-cjk-sc From 36c04a41a29e1470f7004fa0f8c8e741e7d5d8be Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 14 Aug 2024 17:02:23 -0400 Subject: [PATCH 0149/1230] BLD: Avoid pybind11 2.13.3 due to Windows quoting bug See https://github.com/pybind/pybind11/issues/5300#issuecomment-2287698500 --- pyproject.toml | 4 ++-- requirements/dev/build-requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0f181ccb629e..b706d86cb7b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ requires-python = ">=3.10" dev = [ "meson-python>=0.13.1", "numpy>=1.25", - "pybind11>=2.6", + "pybind11>=2.6,!=2.13.3", "setuptools_scm>=7", # Not required by us but setuptools_scm without a version, cso _if_ # installed, then setuptools_scm 8 requires at least this version. @@ -71,7 +71,7 @@ build-backend = "mesonpy" # Also keep in sync with optional dependencies above. requires = [ "meson-python>=0.13.1", - "pybind11>=2.6", + "pybind11>=2.6,!=2.13.3", "setuptools_scm>=7", # Comments on numpy build requirement range: diff --git a/requirements/dev/build-requirements.txt b/requirements/dev/build-requirements.txt index 1b22d228e217..6f0c6029f4a2 100644 --- a/requirements/dev/build-requirements.txt +++ b/requirements/dev/build-requirements.txt @@ -1,4 +1,4 @@ -pybind11 +pybind11!=2.13.3 meson-python numpy setuptools-scm From 23aa06020c84bc512ad7a49a8044b9369ab4d10f Mon Sep 17 00:00:00 2001 From: hannah Date: Tue, 4 Jun 2024 21:52:13 -0400 Subject: [PATCH 0150/1230] added triage section to new contributor docs Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- doc/devel/contribute.rst | 18 ++++++++++++++++++ doc/devel/index.rst | 40 ++++++++++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/doc/devel/contribute.rst b/doc/devel/contribute.rst index 4eb900bce7ed..ad61c43fd348 100644 --- a/doc/devel/contribute.rst +++ b/doc/devel/contribute.rst @@ -158,6 +158,24 @@ please reach out on the :ref:`contributor_incubator` .. _`open an issue`: https://github.com/matplotlib/matplotlib/issues/new?assignees=&labels=Documentation&projects=&template=documentation.yml&title=%5BDoc%5D%3A+ +.. _contribute_triage: + +Triage +------ +We appreciate your help keeping the `issue tracker `_ +organized because it is our centralized location for feature requests, +bug reports, tracking major projects, and discussing priorities. Some examples of what +we mean by triage are: + +* labeling issues and pull requests +* verifying bug reports +* debugging and resolving issues +* linking to related issues, discussion, and external work + +Our triage process is discussed in detail in :ref:`bug_triaging`. + +If you have any questions about the process, please reach out on the +:ref:`contributor_incubator` .. _other_ways_to_contribute: diff --git a/doc/devel/index.rst b/doc/devel/index.rst index 672f2ce9f9d9..fedf9b76e875 100644 --- a/doc/devel/index.rst +++ b/doc/devel/index.rst @@ -84,43 +84,63 @@ to contributing, we recommend that you first read our contribute .. grid:: 1 1 2 2 - :class-row: sd-align-minor-center + :class-row: sd-fs-5 sd-align-minor-center .. grid-item:: - :class: sd-fs-5 - :octicon:`info;1em;sd-text-info` :ref:`Where should I start? ` + .. grid:: 1 + :gutter: 1 + + .. grid-item:: + + :octicon:`info;1em;sd-text-info` :ref:`Where should I start? ` + + .. grid-item:: + + :octicon:`question;1em;sd-text-info` :ref:`Where should I ask questions? ` - :octicon:`question;1em;sd-text-info` :ref:`Where should I ask questions? ` + .. grid-item:: - :octicon:`git-pull-request;1em;sd-text-info` :ref:`How do I work on an issue? ` + :octicon:`git-pull-request;1em;sd-text-info` :ref:`How do I work on an issue? ` - :octicon:`codespaces;1em;sd-text-info` :ref:`How do I start a pull request? ` + .. grid-item:: + + :octicon:`codespaces;1em;sd-text-info` :ref:`How do I start a pull request? ` .. grid-item:: .. grid:: 1 :gutter: 1 - :class-row: sd-fs-5 .. grid-item-card:: :link: contribute_code :link-type: ref - :shadow: none + :class-card: sd-shadow-none + :class-body: sd-text-{primary} :octicon:`code;1em;sd-text-info` Contribute code .. grid-item-card:: :link: contribute_documentation :link-type: ref - :shadow: none + :class-card: sd-shadow-none + :class-body: sd-text-{primary} :octicon:`note;1em;sd-text-info` Write documentation + .. grid-item-card:: + :link: contribute_triage + :link-type: ref + :class-card: sd-shadow-none + :class-body: sd-text-{primary} + + :octicon:`issue-opened;1em;sd-text-info` Triage issues + .. grid-item-card:: :link: other_ways_to_contribute :link-type: ref - :shadow: none + :class-card: sd-shadow-none + :class-body: sd-text-{primary} :octicon:`globe;1em;sd-text-info` Build community From 9283ce5527501ae53a19d87905e2a454ba101b31 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 13 Aug 2024 03:13:13 -0400 Subject: [PATCH 0151/1230] Simplify _api.warn_external on Python 3.12+ Python 3.12 added the `skip_file_prefixes` argument, which essentially does what this helper function does for us. Technically, I think the old implementation would set the stack level to the tests, if called in one, but this one doesn't. However, that shouldn't be a problem, as either 1) warnings are errors, or 2), we catch the warning with `pytest.warns` and don't see the stack level. --- lib/matplotlib/_api/__init__.py | 36 +++++++++++++++++++----------- lib/matplotlib/tests/test_cbook.py | 25 +++++++++++++++------ 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/lib/matplotlib/_api/__init__.py b/lib/matplotlib/_api/__init__.py index 27d68529b7d4..22b58b62ff8e 100644 --- a/lib/matplotlib/_api/__init__.py +++ b/lib/matplotlib/_api/__init__.py @@ -12,6 +12,7 @@ import functools import itertools +import pathlib import re import sys import warnings @@ -366,16 +367,25 @@ def warn_external(message, category=None): warnings.warn`` (or ``functools.partial(warnings.warn, stacklevel=2)``, etc.). """ - frame = sys._getframe() - for stacklevel in itertools.count(1): - if frame is None: - # when called in embedded context may hit frame is None - break - if not re.match(r"\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))", - # Work around sphinx-gallery not setting __name__. - frame.f_globals.get("__name__", "")): - break - frame = frame.f_back - # preemptively break reference cycle between locals and the frame - del frame - warnings.warn(message, category, stacklevel) + kwargs = {} + if sys.version_info[:2] >= (3, 12): + # Go to Python's `site-packages` or `lib` from an editable install. + basedir = pathlib.Path(__file__).parents[2] + kwargs['skip_file_prefixes'] = (str(basedir / 'matplotlib'), + str(basedir / 'mpl_toolkits')) + else: + frame = sys._getframe() + for stacklevel in itertools.count(1): + if frame is None: + # when called in embedded context may hit frame is None + kwargs['stacklevel'] = stacklevel + break + if not re.match(r"\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))", + # Work around sphinx-gallery not setting __name__. + frame.f_globals.get("__name__", "")): + kwargs['stacklevel'] = stacklevel + break + frame = frame.f_back + # preemptively break reference cycle between locals and the frame + del frame + warnings.warn(message, category, **kwargs) diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 6e0ad71f68be..222cc23b7e4d 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -1,8 +1,9 @@ from __future__ import annotations -import sys import itertools +import pathlib import pickle +import sys from typing import Any from unittest.mock import patch, Mock @@ -478,6 +479,22 @@ def test_normalize_kwargs_pass(inp, expected, kwargs_to_norm): assert expected == cbook.normalize_kwargs(inp, **kwargs_to_norm) +def test_warn_external(recwarn): + _api.warn_external("oops") + assert len(recwarn) == 1 + if sys.version_info[:2] >= (3, 12): + # With Python 3.12, we let Python figure out the stacklevel using the + # `skip_file_prefixes` argument, which cannot exempt tests, so just confirm + # the filename is not in the package. + basedir = pathlib.Path(__file__).parents[2] + assert not recwarn[0].filename.startswith((str(basedir / 'matplotlib'), + str(basedir / 'mpl_toolkits'))) + else: + # On older Python versions, we manually calculated the stacklevel, and had an + # exception for our own tests. + assert recwarn[0].filename == __file__ + + def test_warn_external_frame_embedded_python(): with patch.object(cbook, "sys") as mock_sys: mock_sys._getframe = Mock(return_value=None) @@ -784,12 +801,6 @@ def test_safe_first_element_pandas_series(pd): assert actual == 0 -def test_warn_external(recwarn): - _api.warn_external("oops") - assert len(recwarn) == 1 - assert recwarn[0].filename == __file__ - - def test_array_patch_perimeters(): # This compares the old implementation as a reference for the # vectorized one. From 2084d86d8756b33559e11b99df94f8c1ebb20de5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 8 Aug 2024 21:05:00 -0400 Subject: [PATCH 0152/1230] ci: Enable testing on Python 3.13 --- .github/workflows/tests.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c71577c0875..c293e731b6e7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -85,6 +85,12 @@ jobs: pyqt6-ver: '!=6.6.0' # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 pyside6-ver: '!=6.5.1' + - os: ubuntu-22.04 + python-version: '3.13' + # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html + pyqt6-ver: '!=6.6.0' + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 + pyside6-ver: '!=6.5.1' - os: macos-12 # This runner is on Intel chips. python-version: '3.10' # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 @@ -93,6 +99,10 @@ jobs: python-version: '3.12' # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 pyside6-ver: '!=6.5.1' + - os: macos-14 # This runner is on M1 (arm64) chips. + python-version: '3.13' + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 + pyside6-ver: '!=6.5.1' steps: - uses: actions/checkout@v4 @@ -103,6 +113,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install OS dependencies run: | @@ -249,11 +260,11 @@ jobs: python -c 'import PyQt5.QtCore' && echo 'PyQt5 is available' || echo 'PyQt5 is not available' - # Even though PySide2 wheels can be installed on Python 3.12, they are broken and since PySide2 is + # Even though PySide2 wheels can be installed on Python 3.12+, they are broken and since PySide2 is # deprecated, they are unlikely to be fixed. For the same deprecation reason, there are no wheels # on M1 macOS, so don't bother there either. if [[ "${{ matrix.os }}" != 'macos-14' - && "${{ matrix.python-version }}" != '3.12' ]]; then + && "${{ matrix.python-version }}" != '3.12' && "${{ matrix.python-version }}" != '3.13' ]]; then python -mpip install --upgrade pyside2${{ matrix.pyside2-ver }} && python -c 'import PySide2.QtCore' && echo 'PySide2 is available' || From 5c8012d2f8fc9f8f582806753178f60811bfb3ed Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 9 Aug 2024 00:11:56 -0400 Subject: [PATCH 0153/1230] TST: Expand some ARM tolerances to Apple Silicon as well --- lib/matplotlib/tests/test_axes.py | 4 ++-- lib/matplotlib/tests/test_contour.py | 2 +- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 2c10a93796fa..859ae564afb2 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1216,7 +1216,7 @@ def test_imshow(): @image_comparison( ['imshow_clip'], style='mpl20', - tol=1.24 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) + tol=1.24 if platform.machine() in ('aarch64', 'arm64', 'ppc64le', 's390x') else 0) def test_imshow_clip(): # As originally reported by Gellule Xg # use former defaults to match existing baseline image @@ -2613,7 +2613,7 @@ def test_contour_hatching(): @image_comparison( ['contour_colorbar'], style='mpl20', - tol=0.54 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) + tol=0.54 if platform.machine() in ('aarch64', 'arm64', 'ppc64le', 's390x') else 0) def test_contour_colorbar(): x, y, z = contour_dat() diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index d4600a14fe1c..0622c099a20c 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -442,7 +442,7 @@ def test_contourf_log_extension(split_collections): @pytest.mark.parametrize("split_collections", [False, True]) @image_comparison( ['contour_addlines.png'], remove_text=True, style='mpl20', - tol=0.15 if platform.machine() in ('aarch64', 'ppc64le', 's390x') + tol=0.15 if platform.machine() in ('aarch64', 'arm64', 'ppc64le', 's390x') else 0.03) # tolerance is because image changed minutely when tick finding on # colorbars was cleaned up... diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index c31398fb8260..c64e888fdc2e 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -222,7 +222,7 @@ def test_bar3d_lightsource(): @mpl3d_image_comparison( ['contour3d.png'], style='mpl20', - tol=0.002 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) + tol=0.002 if platform.machine() in ('aarch64', 'arm64', 'ppc64le', 's390x') else 0) def test_contour3d(): plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated fig = plt.figure() From 85b4e026ae56b87005c5cd2778e0059125952e1f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 9 Aug 2024 00:24:57 -0400 Subject: [PATCH 0154/1230] CI: Add CI to test matplotlib against free-threaded Python MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Edgar Andrés Margffoy Tuay --- .github/workflows/tests.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c293e731b6e7..4de46a1ed80f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -91,6 +91,13 @@ jobs: pyqt6-ver: '!=6.6.0' # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 pyside6-ver: '!=6.5.1' + - name-suffix: "Free-threaded" + os: ubuntu-22.04 + python-version: '3.13t' + # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html + pyqt6-ver: '!=6.6.0' + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 + pyside6-ver: '!=6.5.1' - os: macos-12 # This runner is on Intel chips. python-version: '3.10' # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 @@ -111,10 +118,18 @@ jobs: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 + if: matrix.python-version != '3.13t' with: python-version: ${{ matrix.python-version }} allow-prereleases: true + - name: Set up Python ${{ matrix.python-version }} + uses: deadsnakes/action@6c8b9b82fe0b4344f4b98f2775fcc395df45e494 # v3.1.0 + if: matrix.python-version == '3.13t' + with: + python-version: '3.13' + nogil: true + - name: Install OS dependencies run: | case "${{ runner.os }}" in @@ -160,6 +175,11 @@ jobs: texlive-luatex \ texlive-pictures \ texlive-xetex + if [[ "${{ matrix.python-version }}" = '3.13t' ]]; then + # TODO: Remove this once setup-python supports nogil distributions. + sudo apt-get install -yy --no-install-recommends \ + python3.13-tk-nogil + fi if [[ "${{ matrix.os }}" = ubuntu-20.04 ]]; then sudo apt-get install -yy --no-install-recommends libopengl0 else # ubuntu-22.04 @@ -216,6 +236,15 @@ jobs: 4-${{ runner.os }}-py${{ matrix.python-version }}-mpl-${{ github.ref }}- 4-${{ runner.os }}-py${{ matrix.python-version }}-mpl- + - name: Install the nightly dependencies + if: matrix.python-version == '3.13t' + run: | + python -m pip install pytz tzdata python-dateutil # Must be installed for Pandas. + python -m pip install \ + --pre \ + --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \ + --upgrade --only-binary=:all: numpy pandas pillow contourpy + - name: Install Python dependencies run: | # Upgrade pip and setuptools and wheel to get as clean an install as @@ -241,6 +270,7 @@ jobs: # Sphinx is needed to run sphinxext tests python -m pip install --upgrade sphinx!=6.1.2 + if [[ "${{ matrix.python-version }}" != '3.13t' ]]; then # GUI toolkits are pip-installable only for some versions of Python # so don't fail if we can't install them. Make it easier to check # whether the install was successful by trying to import the toolkit @@ -286,6 +316,8 @@ jobs: echo 'wxPython is available' || echo 'wxPython is not available' + fi # Skip backends on Python 3.13t. + - name: Install the nightly dependencies # Only install the nightly dependencies during the scheduled event if: github.event_name == 'schedule' && matrix.name-suffix != '(Minimum Versions)' @@ -324,6 +356,9 @@ jobs: - name: Run pytest run: | + if [[ "${{ matrix.python-version }}" == '3.13t' ]]; then + export PYTHON_GIL=0 + fi pytest -rfEsXR -n auto \ --maxfail=50 --timeout=300 --durations=25 \ --cov-report=xml --cov=lib --log-level=DEBUG --color=yes From 8e2d7914a2c8943187405661be3e876537c32e54 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 14 Aug 2024 16:44:38 -0400 Subject: [PATCH 0155/1230] TST: Guard against PyGObject existing, but not gobject-introspection --- lib/matplotlib/tests/test_backends_interactive.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index d624b5db0ac2..2c6b61a48438 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -81,10 +81,18 @@ def _get_available_interactive_backends(): elif env["MPLBACKEND"] == 'macosx' and os.environ.get('TF_BUILD'): reason = "macosx backend fails on Azure" elif env["MPLBACKEND"].startswith('gtk'): - import gi # type: ignore + try: + import gi # type: ignore + except ImportError: + # Though we check that `gi` exists above, it is possible that its + # C-level dependencies are not available, and then it still raises an + # `ImportError`, so guard against that. + available_gtk_versions = [] + else: + gi_repo = gi.Repository.get_default() + available_gtk_versions = gi_repo.enumerate_versions('Gtk') version = env["MPLBACKEND"][3] - repo = gi.Repository.get_default() - if f'{version}.0' not in repo.enumerate_versions('Gtk'): + if f'{version}.0' not in available_gtk_versions: reason = "no usable GTK bindings" marks = [] if reason: From f6b32660a348aa027b25709cc3d8218b017e8db5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 14 Aug 2024 17:02:23 -0400 Subject: [PATCH 0156/1230] BLD: Avoid pybind11 2.13.3 due to Windows quoting bug See https://github.com/pybind/pybind11/issues/5300#issuecomment-2287698500 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 52bbe308c0f9..891ef87e4342 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ requires-python = ">=3.9" dev = [ "meson-python>=0.13.1", "numpy>=1.25", - "pybind11>=2.6", + "pybind11>=2.6,!=2.13.3", "setuptools_scm>=7", # Not required by us but setuptools_scm without a version, cso _if_ # installed, then setuptools_scm 8 requires at least this version. @@ -73,7 +73,7 @@ build-backend = "mesonpy" # Also keep in sync with optional dependencies above. requires = [ "meson-python>=0.13.1", - "pybind11>=2.6", + "pybind11>=2.6,!=2.13.3", "setuptools_scm>=7", # Comments on numpy build requirement range: From 57d41fe06d82edd35126f8acf7ba6a97561dd319 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 9 Aug 2024 21:14:05 -0400 Subject: [PATCH 0157/1230] Stop disabling FH4 Exception Handling on MSVC As of #28687, our extensions depend on `VCRUNTIME140_1.dll`, and this was allowed because Python 3.8+ started shipping that file. The original report in #18292 was for Python 3.7, which didn't ship the DLL, but we require Python 3.10 now, so it's safe again. Since we can use that dependency, there's no need to disable the option that started requiring it in the first place. As noted in the original blog post [1], this will make our extensions smaller, and slightly faster. [1] https://devblogs.microsoft.com/cppblog/making-cpp-exception-handling-smaller-x64/ --- src/meson.build | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/meson.build b/src/meson.build index bbef93c13d92..a046b3306ab8 100644 --- a/src/meson.build +++ b/src/meson.build @@ -160,21 +160,12 @@ extension_data = { }, } -cpp_special_arguments = [] -if cpp.get_id() == 'msvc' and get_option('buildtype') != 'plain' - # Disable FH4 Exception Handling implementation so that we don't require - # VCRUNTIME140_1.dll. For more details, see: - # https://devblogs.microsoft.com/cppblog/making-cpp-exception-handling-smaller-x64/ - # https://github.com/joerick/cibuildwheel/issues/423#issuecomment-677763904 - cpp_special_arguments += ['/d2FH4-'] -endif - foreach ext, kwargs : extension_data # Ensure that PY_ARRAY_UNIQUE_SYMBOL is uniquely defined for each extension. unique_array_api = '-DPY_ARRAY_UNIQUE_SYMBOL=MPL_@0@_ARRAY_API'.format(ext.replace('.', '_')) additions = { 'c_args': [unique_array_api] + kwargs.get('c_args', []), - 'cpp_args': cpp_special_arguments + [unique_array_api] + kwargs.get('cpp_args', []), + 'cpp_args': [unique_array_api] + kwargs.get('cpp_args', []), } py3.extension_module( ext, From d4f0ebbcf5b5d1a13a756e8796d8b42b85e415c9 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:34:27 +0200 Subject: [PATCH 0158/1230] MNT: Better workaround for format_cursor_data on ScalarMappables By introducing an explicit override function, we can move the formatting code to ScalarMappable, where it logically belongs. Also, artist.py does no longer depend on colors.py and cm.py, which simplifies module dependencies. --- lib/matplotlib/artist.py | 38 +++++--------------------------------- lib/matplotlib/cm.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 24aaa349ee05..e51661d1f0be 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -13,8 +13,6 @@ import matplotlib as mpl from . import _api, cbook -from .colors import BoundaryNorm -from .cm import ScalarMappable from .path import Path from .transforms import (BboxBase, Bbox, IdentityTransform, Transform, TransformedBbox, TransformedPatchPath, TransformedPath) @@ -1346,37 +1344,11 @@ def format_cursor_data(self, data): -------- get_cursor_data """ - if np.ndim(data) == 0 and isinstance(self, ScalarMappable): - # This block logically belongs to ScalarMappable, but can't be - # implemented in it because most ScalarMappable subclasses inherit - # from Artist first and from ScalarMappable second, so - # Artist.format_cursor_data would always have precedence over - # ScalarMappable.format_cursor_data. - n = self.cmap.N - if np.ma.getmask(data): - return "[]" - normed = self.norm(data) - if np.isfinite(normed): - if isinstance(self.norm, BoundaryNorm): - # not an invertible normalization mapping - cur_idx = np.argmin(np.abs(self.norm.boundaries - data)) - neigh_idx = max(0, cur_idx - 1) - # use max diff to prevent delta == 0 - delta = np.diff( - self.norm.boundaries[neigh_idx:cur_idx + 2] - ).max() - elif self.norm.vmin == self.norm.vmax: - # singular norms, use delta of 10% of only value - delta = np.abs(self.norm.vmin * .1) - else: - # Midpoints of neighboring color intervals. - neighbors = self.norm.inverse( - (int(normed * n) + np.array([0, 1])) / n) - delta = abs(neighbors - data).max() - g_sig_digits = cbook._g_sig_digits(data, delta) - else: - g_sig_digits = 3 # Consistent with default below. - return f"[{data:-#.{g_sig_digits}g}]" + if np.ndim(data) == 0 and hasattr(self, "_format_cursor_data_override"): + # workaround for ScalarMappable to be able to define its own + # format_cursor_data(). See ScalarMappable._format_cursor_data_override + # for details. + return self._format_cursor_data_override(data) else: try: data[0] diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index 071c93f9f0b3..f5bc455df1f7 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -611,6 +611,38 @@ def changed(self): self.callbacks.process('changed', self) self.stale = True + def _format_cursor_data_override(self, data): + # This function overwrites Artist.format_cursor_data(). We cannot + # implement ScalarMappable.format_cursor_data() directly, because + # most ScalarMappable subclasses inherit from Artist first and from + # ScalarMappable second, so Artist.format_cursor_data would always + # have precedence over ScalarMappable.format_cursor_data. + n = self.cmap.N + if np.ma.getmask(data): + return "[]" + normed = self.norm(data) + if np.isfinite(normed): + if isinstance(self.norm, colors.BoundaryNorm): + # not an invertible normalization mapping + cur_idx = np.argmin(np.abs(self.norm.boundaries - data)) + neigh_idx = max(0, cur_idx - 1) + # use max diff to prevent delta == 0 + delta = np.diff( + self.norm.boundaries[neigh_idx:cur_idx + 2] + ).max() + elif self.norm.vmin == self.norm.vmax: + # singular norms, use delta of 10% of only value + delta = np.abs(self.norm.vmin * .1) + else: + # Midpoints of neighboring color intervals. + neighbors = self.norm.inverse( + (int(normed * n) + np.array([0, 1])) / n) + delta = abs(neighbors - data).max() + g_sig_digits = cbook._g_sig_digits(data, delta) + else: + g_sig_digits = 3 # Consistent with default below. + return f"[{data:-#.{g_sig_digits}g}]" + # The docstrings here must be generic enough to apply to all relevant methods. mpl._docstring.interpd.update( From 04fea8c33fad75b204030de1ce6f783f7458233c Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:04:26 +0200 Subject: [PATCH 0159/1230] MNT: Deprecate reimported functions in top-level namespace --- .../deprecations/28728-TH.rst | 10 ++++++++++ lib/matplotlib/__init__.py | 20 +++++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/28728-TH.rst diff --git a/doc/api/next_api_changes/deprecations/28728-TH.rst b/doc/api/next_api_changes/deprecations/28728-TH.rst new file mode 100644 index 000000000000..56d5a80b439c --- /dev/null +++ b/doc/api/next_api_changes/deprecations/28728-TH.rst @@ -0,0 +1,10 @@ +matplotlib.validate_backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +...is deprecated. Please use `matplotlib.rcsetup.validate_backend` instead. + + +matplotlib.sanitize_sequence +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +...is deprecated. Please use `matplotlib.cbook.sanitize_sequence` instead. diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 8a77e5601d8c..6b5746ea0f16 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -157,10 +157,8 @@ # cbook must import matplotlib only within function # definitions, so it is safe to import from it here. from . import _api, _version, cbook, _docstring, rcsetup -from matplotlib.cbook import sanitize_sequence from matplotlib._api import MatplotlibDeprecationWarning from matplotlib.rcsetup import cycler # noqa: F401 -from matplotlib.rcsetup import validate_backend _log = logging.getLogger(__name__) @@ -1236,7 +1234,7 @@ def use(backend, *, force=True): matplotlib.pyplot.switch_backend """ - name = validate_backend(backend) + name = rcsetup.validate_backend(backend) # don't (prematurely) resolve the "auto" backend setting if rcParams._get_backend_or_none() == name: # Nothing to do if the requested backend is already set @@ -1340,7 +1338,7 @@ def _replacer(data, value): except Exception: # key does not exist, silently fall back to key pass - return sanitize_sequence(value) + return cbook.sanitize_sequence(value) def _label_from_arg(y, default_name): @@ -1472,8 +1470,8 @@ def inner(ax, *args, data=None, **kwargs): if data is None: return func( ax, - *map(sanitize_sequence, args), - **{k: sanitize_sequence(v) for k, v in kwargs.items()}) + *map(cbook.sanitize_sequence, args), + **{k: cbook.sanitize_sequence(v) for k, v in kwargs.items()}) bound = new_sig.bind(ax, *args, **kwargs) auto_label = (bound.arguments.get(label_namer) @@ -1510,6 +1508,16 @@ def inner(ax, *args, data=None, **kwargs): _log.debug('platform is %s', sys.platform) +@_api.deprecated("3.10", alternative="matplotlib.cbook.sanitize_sequence") +def sanitize_sequence(data): + return cbook.sanitize_sequence(data) + + +@_api.deprecated("3.10", alternative="matplotlib.rcsetup.validate_backend") +def validate_backend(s): + return rcsetup.validate_backend(s) + + # workaround: we must defer colormaps import to after loading rcParams, because # colormap creation depends on rcParams from matplotlib.cm import _colormaps as colormaps # noqa: E402 From 01ceeef0b28ea7bdd4e7e32bef1d3eabb8aa23b2 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:45:02 +0200 Subject: [PATCH 0160/1230] MNT: Don't rely on RcParams being a dict subclass in internal code Eventually, we want to be able to remove the dict subclassing from RcParams, which will allow better initialization and handling. We've publically announced that people should not rely on dict in https://matplotlib.org/stable/api/prev_api_changes/api_changes_3.7.0.html#rcparams-type This is an internal cleanup step to remove the dict-assumption and a preparation for further refactoring. --- lib/matplotlib/__init__.py | 48 +++++++++++++++++++++++++++---------- lib/matplotlib/__init__.pyi | 4 ++++ lib/matplotlib/pyplot.py | 5 ++-- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 8a77e5601d8c..9f9e7cbceddc 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -712,6 +712,35 @@ def _get(self, key): """ return dict.__getitem__(self, key) + def _update_raw(self, other_params): + """ + Directly update the data from *other_params*, bypassing deprecation, + backend and validation logic on both sides. + + This ``rcParams._update_raw(params)`` replaces the previous pattern + ``dict.update(rcParams, params)``. + + Parameters + ---------- + other_params : dict or `.RcParams` + The input mapping from which to update. + """ + if isinstance(other_params, RcParams): + other_params = dict.items(other_params) + dict.update(self, other_params) + + def _ensure_has_backend(self): + """ + Ensure that a "backend" entry exists. + + Normally, the default matplotlibrc file contains *no* entry for "backend" (the + corresponding line starts with ##, not #; we fill in _auto_backend_sentinel + in that case. However, packagers can set a different default backend + (resulting in a normal `#backend: foo` line) in which case we should *not* + fill in _auto_backend_sentinel. + """ + dict.setdefault(self, "backend", rcsetup._auto_backend_sentinel) + def __setitem__(self, key, val): try: if key in _deprecated_map: @@ -961,24 +990,17 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): return config -# When constructing the global instances, we need to perform certain updates -# by explicitly calling the superclass (dict.update, dict.items) to avoid -# triggering resolution of _auto_backend_sentinel. rcParamsDefault = _rc_params_in_file( cbook._get_data_path("matplotlibrc"), # Strip leading comment. transform=lambda line: line[1:] if line.startswith("#") else line, fail_on_error=True) -dict.update(rcParamsDefault, rcsetup._hardcoded_defaults) -# Normally, the default matplotlibrc file contains *no* entry for backend (the -# corresponding line starts with ##, not #; we fill on _auto_backend_sentinel -# in that case. However, packagers can set a different default backend -# (resulting in a normal `#backend: foo` line) in which case we should *not* -# fill in _auto_backend_sentinel. -dict.setdefault(rcParamsDefault, "backend", rcsetup._auto_backend_sentinel) +rcParamsDefault._update_raw(rcsetup._hardcoded_defaults) +rcParamsDefault._ensure_has_backend() + rcParams = RcParams() # The global instance. -dict.update(rcParams, dict.items(rcParamsDefault)) -dict.update(rcParams, _rc_params_in_file(matplotlib_fname())) +rcParams._update_raw(rcParamsDefault) +rcParams._update_raw(_rc_params_in_file(matplotlib_fname())) rcParamsOrig = rcParams.copy() with _api.suppress_matplotlib_deprecation_warning(): # This also checks that all rcParams are indeed listed in the template. @@ -1190,7 +1212,7 @@ def rc_context(rc=None, fname=None): rcParams.update(rc) yield finally: - dict.update(rcParams, orig) # Revert to the original rcs. + rcParams._update_raw(orig) # Revert to the original rcs. def use(backend, *, force=True): diff --git a/lib/matplotlib/__init__.pyi b/lib/matplotlib/__init__.pyi index e7208a17c99f..05dc927dc6c9 100644 --- a/lib/matplotlib/__init__.pyi +++ b/lib/matplotlib/__init__.pyi @@ -70,6 +70,10 @@ class RcParams(dict[str, Any]): def __init__(self, *args, **kwargs) -> None: ... def _set(self, key: str, val: Any) -> None: ... def _get(self, key: str) -> Any: ... + + def _update_raw(self, other_params: dict | RcParams) -> None: ... + + def _ensure_has_backend(self) -> None: ... def __setitem__(self, key: str, val: Any) -> None: ... def __getitem__(self, key: str) -> Any: ... def __iter__(self) -> Generator[str, None, None]: ... diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index d54a25056175..6e917ac9b53b 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -410,8 +410,7 @@ def switch_backend(newbackend: str) -> None: switch_backend("agg") rcParamsOrig["backend"] = "agg" return - # have to escape the switch on access logic - old_backend = dict.__getitem__(rcParams, 'backend') + old_backend = rcParams._get('backend') # get without triggering backend resolution module = backend_registry.load_backend_module(newbackend) canvas_class = module.FigureCanvas @@ -841,7 +840,7 @@ def xkcd( "xkcd mode is not compatible with text.usetex = True") stack = ExitStack() - stack.callback(dict.update, rcParams, rcParams.copy()) # type: ignore[arg-type] + stack.callback(rcParams._update_raw, rcParams.copy()) # type: ignore[arg-type] from matplotlib import patheffects rcParams.update({ From ba11a8ce10d8442d4e313857750b8e361fd12432 Mon Sep 17 00:00:00 2001 From: Charlie LeWarne Date: Thu, 15 Aug 2024 13:23:31 -0700 Subject: [PATCH 0161/1230] Renamed the minumumSizeHint method to minimumSizeHint according to #28716 --- lib/matplotlib/backends/backend_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index c592858cef0b..e693811df4f0 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -393,7 +393,7 @@ def sizeHint(self): w, h = self.get_width_height() return QtCore.QSize(w, h) - def minumumSizeHint(self): + def minimumSizeHint(self): return QtCore.QSize(10, 10) @staticmethod From cb124887a10c2e69e70974847a47f320353e791c Mon Sep 17 00:00:00 2001 From: hannah Date: Thu, 15 Aug 2024 18:21:14 -0400 Subject: [PATCH 0162/1230] Backport PR #28732: Renames the minumumSizeHint method to minimumSizeHint --- lib/matplotlib/backends/backend_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 6603883075d4..242c6fdbf9f9 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -393,7 +393,7 @@ def sizeHint(self): w, h = self.get_width_height() return QtCore.QSize(w, h) - def minumumSizeHint(self): + def minimumSizeHint(self): return QtCore.QSize(10, 10) @staticmethod From c4ec3f6ee946e768c4c1bb934b4e69ed8af259fb Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Wed, 8 May 2024 09:23:53 +0200 Subject: [PATCH 0163/1230] Expire deprecations in Axis and update docs --- doc/api/axis_api.rst | 4 ++-- lib/matplotlib/axis.py | 37 +++++++------------------------------ lib/matplotlib/axis.pyi | 3 --- 3 files changed, 9 insertions(+), 35 deletions(-) diff --git a/doc/api/axis_api.rst b/doc/api/axis_api.rst index 17e892b99df8..424213445169 100644 --- a/doc/api/axis_api.rst +++ b/doc/api/axis_api.rst @@ -217,6 +217,7 @@ Other Axis.axes Axis.limit_range_for_scale Axis.reset_ticks + Axis.set_clip_path Axis.set_default_intervals Discouraged @@ -263,8 +264,7 @@ specify a matching series of labels. Calling ``set_ticks`` makes a Tick.get_tick_padding Tick.get_tickdir Tick.get_view_interval - Tick.set_label1 - Tick.set_label2 + Tick.set_clip_path Tick.set_pad Tick.set_url Tick.update_position diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 123eef01bf00..40efbaa09fd6 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -231,7 +231,6 @@ def get_children(self): self.gridline, self.label1, self.label2] return children - @_api.rename_parameter("3.8", "clippath", "path") def set_clip_path(self, path, transform=None): # docstring inherited super().set_clip_path(path, transform) @@ -278,32 +277,6 @@ def draw(self, renderer): renderer.close_group(self.__name__) self.stale = False - @_api.deprecated("3.8") - def set_label1(self, s): - """ - Set the label1 text. - - Parameters - ---------- - s : str - """ - self.label1.set_text(s) - self.stale = True - - set_label = set_label1 - - @_api.deprecated("3.8") - def set_label2(self, s): - """ - Set the label2 text. - - Parameters - ---------- - s : str - """ - self.label2.set_text(s) - self.stale = True - def set_url(self, url): """ Set the url of label1 and label2. @@ -833,6 +806,10 @@ def _set_axes_scale(self, value, **kwargs): **{f"scale{k}": k == name for k in self.axes._axis_names}) def limit_range_for_scale(self, vmin, vmax): + """ + Return the range *vmin*, *vmax*, restricted to the domain supported by the + current scale. + """ return self._scale.limit_range_for_scale(vmin, vmax, self.get_minpos()) def _get_autoscale_on(self): @@ -841,8 +818,9 @@ def _get_autoscale_on(self): def _set_autoscale_on(self, b): """ - Set whether this Axis is autoscaled when drawing or by - `.Axes.autoscale_view`. If b is None, then the value is not changed. + Set whether this Axis is autoscaled when drawing or by `.Axes.autoscale_view`. + + If b is None, then the value is not changed. Parameters ---------- @@ -1131,7 +1109,6 @@ def _translate_tick_params(kw, reverse=False): kwtrans.update(kw_) return kwtrans - @_api.rename_parameter("3.8", "clippath", "path") def set_clip_path(self, path, transform=None): super().set_clip_path(path, transform) for child in self.majorTicks + self.minorTicks: diff --git a/lib/matplotlib/axis.pyi b/lib/matplotlib/axis.pyi index 8f69fe4039a8..8f7b213c51e3 100644 --- a/lib/matplotlib/axis.pyi +++ b/lib/matplotlib/axis.pyi @@ -60,9 +60,6 @@ class Tick(martist.Artist): def set_pad(self, val: float) -> None: ... def get_pad(self) -> None: ... def get_loc(self) -> float: ... - def set_label1(self, s: object) -> None: ... - def set_label(self, s: object) -> None: ... - def set_label2(self, s: object) -> None: ... def set_url(self, url: str | None) -> None: ... def get_view_interval(self) -> ArrayLike: ... def update_position(self, loc: float) -> None: ... From f621b0173f9fe7cd4088e9384ac1e46832064e43 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Wed, 8 May 2024 09:24:21 +0200 Subject: [PATCH 0164/1230] Expire parameter renaming deprecations --- lib/matplotlib/image.py | 2 -- lib/matplotlib/legend.py | 1 - lib/matplotlib/projections/geo.py | 8 -------- lib/matplotlib/projections/polar.py | 2 -- lib/matplotlib/scale.py | 8 -------- lib/matplotlib/table.py | 1 - lib/matplotlib/text.py | 1 - lib/matplotlib/transforms.py | 10 ---------- 8 files changed, 33 deletions(-) diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 95994201b94e..2801e9410219 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -1129,11 +1129,9 @@ def get_extent(self): raise RuntimeError('Must set data first') return self._Ax[0], self._Ax[-1], self._Ay[0], self._Ay[-1] - @_api.rename_parameter("3.8", "s", "filternorm") def set_filternorm(self, filternorm): pass - @_api.rename_parameter("3.8", "s", "filterrad") def set_filterrad(self, filterrad): pass diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 9033fc23c1a1..a353451adaa2 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -1196,7 +1196,6 @@ def _find_best_position(self, width, height, renderer): return l, b - @_api.rename_parameter("3.8", "event", "mouseevent") def contains(self, mouseevent): return self.legendPatch.contains(mouseevent) diff --git a/lib/matplotlib/projections/geo.py b/lib/matplotlib/projections/geo.py index 89a9de7618be..d5ab3c746dea 100644 --- a/lib/matplotlib/projections/geo.py +++ b/lib/matplotlib/projections/geo.py @@ -258,7 +258,6 @@ class AitoffAxes(GeoAxes): class AitoffTransform(_GeoTransform): """The base Aitoff transform.""" - @_api.rename_parameter("3.8", "ll", "values") def transform_non_affine(self, values): # docstring inherited longitude, latitude = values.T @@ -280,7 +279,6 @@ def inverted(self): class InvertedAitoffTransform(_GeoTransform): - @_api.rename_parameter("3.8", "xy", "values") def transform_non_affine(self, values): # docstring inherited # MGDTODO: Math is hard ;( @@ -306,7 +304,6 @@ class HammerAxes(GeoAxes): class HammerTransform(_GeoTransform): """The base Hammer transform.""" - @_api.rename_parameter("3.8", "ll", "values") def transform_non_affine(self, values): # docstring inherited longitude, latitude = values.T @@ -324,7 +321,6 @@ def inverted(self): class InvertedHammerTransform(_GeoTransform): - @_api.rename_parameter("3.8", "xy", "values") def transform_non_affine(self, values): # docstring inherited x, y = values.T @@ -353,7 +349,6 @@ class MollweideAxes(GeoAxes): class MollweideTransform(_GeoTransform): """The base Mollweide transform.""" - @_api.rename_parameter("3.8", "ll", "values") def transform_non_affine(self, values): # docstring inherited def d(theta): @@ -394,7 +389,6 @@ def inverted(self): class InvertedMollweideTransform(_GeoTransform): - @_api.rename_parameter("3.8", "xy", "values") def transform_non_affine(self, values): # docstring inherited x, y = values.T @@ -435,7 +429,6 @@ def __init__(self, center_longitude, center_latitude, resolution): self._center_longitude = center_longitude self._center_latitude = center_latitude - @_api.rename_parameter("3.8", "ll", "values") def transform_non_affine(self, values): # docstring inherited longitude, latitude = values.T @@ -469,7 +462,6 @@ def __init__(self, center_longitude, center_latitude, resolution): self._center_longitude = center_longitude self._center_latitude = center_latitude - @_api.rename_parameter("3.8", "xy", "values") def transform_non_affine(self, values): # docstring inherited x, y = values.T diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 025155351f88..07a05e8d0045 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -79,7 +79,6 @@ def _get_rorigin(self): return self._scale_transform.transform( (0, self._axis.get_rorigin()))[1] - @_api.rename_parameter("3.8", "tr", "values") def transform_non_affine(self, values): # docstring inherited theta, r = np.transpose(values) @@ -235,7 +234,6 @@ def __init__(self, axis=None, use_rmin=True, use_rmin="_use_rmin", apply_theta_transforms="_apply_theta_transforms") - @_api.rename_parameter("3.8", "xy", "values") def transform_non_affine(self, values): # docstring inherited x, y = values.T diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 7f90362b574b..f81137c75082 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -213,7 +213,6 @@ def __str__(self): return "{}(base={}, nonpositive={!r})".format( type(self).__name__, self.base, "clip" if self._clip else "mask") - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): # Ignore invalid values due to nans being passed to the transform. with np.errstate(divide="ignore", invalid="ignore"): @@ -250,7 +249,6 @@ def __init__(self, base): def __str__(self): return f"{type(self).__name__}(base={self.base})" - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): return np.power(self.base, values) @@ -362,7 +360,6 @@ def __init__(self, base, linthresh, linscale): self._linscale_adj = (linscale / (1.0 - self.base ** -1)) self._log_base = np.log(base) - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): abs_a = np.abs(values) with np.errstate(divide="ignore", invalid="ignore"): @@ -390,7 +387,6 @@ def __init__(self, base, linthresh, linscale): self.linscale = linscale self._linscale_adj = (linscale / (1.0 - self.base ** -1)) - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): abs_a = np.abs(values) with np.errstate(divide="ignore", invalid="ignore"): @@ -472,7 +468,6 @@ def __init__(self, linear_width): "must be strictly positive") self.linear_width = linear_width - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): return self.linear_width * np.arcsinh(values / self.linear_width) @@ -488,7 +483,6 @@ def __init__(self, linear_width): super().__init__() self.linear_width = linear_width - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): return self.linear_width * np.sinh(values / self.linear_width) @@ -589,7 +583,6 @@ def __init__(self, nonpositive='mask'): self._nonpositive = nonpositive self._clip = {"clip": True, "mask": False}[nonpositive] - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): """logit transform (base 10), masked or clipped""" with np.errstate(divide="ignore", invalid="ignore"): @@ -613,7 +606,6 @@ def __init__(self, nonpositive='mask'): super().__init__() self._nonpositive = nonpositive - @_api.rename_parameter("3.8", "a", "values") def transform_non_affine(self, values): """logistic transform (base 10)""" return 1.0 / (1 + 10**(-values)) diff --git a/lib/matplotlib/table.py b/lib/matplotlib/table.py index 7d8c8ec4c3f4..52f29efe5433 100644 --- a/lib/matplotlib/table.py +++ b/lib/matplotlib/table.py @@ -103,7 +103,6 @@ def __init__(self, xy, width, height, *, text=text, fontproperties=fontproperties, horizontalalignment=loc, verticalalignment='center') - @_api.rename_parameter("3.8", "trans", "t") def set_transform(self, t): super().set_transform(t) # the text does not get the transform! diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index af990ec1bf9f..d2f6270bcb74 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -1848,7 +1848,6 @@ def transform(renderer) -> Transform # Must come last, as some kwargs may be propagated to arrow_patch. Text.__init__(self, x, y, text, **kwargs) - @_api.rename_parameter("3.8", "event", "mouseevent") def contains(self, mouseevent): if self._different_canvas(mouseevent): return False, {} diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 21b51b61f363..324caa8362cd 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -606,7 +606,6 @@ def expanded(self, sw, sh): a = np.array([[-deltaw, -deltah], [deltaw, deltah]]) return Bbox(self._points + a) - @_api.rename_parameter("3.8", "p", "w_pad") def padded(self, w_pad, h_pad=None): """ Construct a `Bbox` by padding this one on all four sides. @@ -1799,7 +1798,6 @@ def transform_affine(self, values): raise NotImplementedError('Affine subclasses should override this ' 'method.') - @_api.rename_parameter("3.8", "points", "values") def transform_non_affine(self, values): # docstring inherited return values @@ -1857,7 +1855,6 @@ def to_values(self): mtx = self.get_matrix() return tuple(mtx[:2].swapaxes(0, 1).flat) - @_api.rename_parameter("3.8", "points", "values") def transform_affine(self, values): mtx = self.get_matrix() if isinstance(values, np.ma.MaskedArray): @@ -1868,7 +1865,6 @@ def transform_affine(self, values): if DEBUG: _transform_affine = transform_affine - @_api.rename_parameter("3.8", "points", "values") def transform_affine(self, values): # docstring inherited # The major speed trap here is just converting to the @@ -2131,17 +2127,14 @@ def get_matrix(self): # docstring inherited return self._mtx - @_api.rename_parameter("3.8", "points", "values") def transform(self, values): # docstring inherited return np.asanyarray(values) - @_api.rename_parameter("3.8", "points", "values") def transform_affine(self, values): # docstring inherited return np.asanyarray(values) - @_api.rename_parameter("3.8", "points", "values") def transform_non_affine(self, values): # docstring inherited return np.asanyarray(values) @@ -2230,7 +2223,6 @@ def frozen(self): # docstring inherited return blended_transform_factory(self._x.frozen(), self._y.frozen()) - @_api.rename_parameter("3.8", "points", "values") def transform_non_affine(self, values): # docstring inherited if self._x.is_affine and self._y.is_affine: @@ -2423,12 +2415,10 @@ def contains_branch_seperately(self, other_transform): __str__ = _make_str_method("_a", "_b") - @_api.rename_parameter("3.8", "points", "values") def transform_affine(self, values): # docstring inherited return self.get_affine().transform(values) - @_api.rename_parameter("3.8", "points", "values") def transform_non_affine(self, values): # docstring inherited if self._a.is_affine and self._b.is_affine: From 68ed8e62e0e0ef7a168784e8847cf83268c12fd7 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Wed, 8 May 2024 09:31:05 +0200 Subject: [PATCH 0165/1230] Expire numdecs deprecation --- lib/matplotlib/tests/test_ticker.py | 7 ++----- lib/matplotlib/ticker.py | 12 ++---------- lib/matplotlib/ticker.pyi | 3 --- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 5f3619cb8cf0..222a0d7e11b0 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -362,15 +362,12 @@ def test_switch_to_autolocator(self): def test_set_params(self): """ Create log locator with default value, base=10.0, subs=[1.0], - numdecs=4, numticks=15 and change it to something else. + numticks=15 and change it to something else. See if change was successful. Should not raise exception. """ loc = mticker.LogLocator() - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="numdecs"): - loc.set_params(numticks=7, numdecs=8, subs=[2.0], base=4) + loc.set_params(numticks=7, subs=[2.0], base=4) assert loc.numticks == 7 - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="numdecs"): - assert loc.numdecs == 8 assert loc._base == 4 assert list(loc._subs) == [2.0] diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 2b00937f9e29..940cacc63fb9 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2275,8 +2275,7 @@ class LogLocator(Locator): Places ticks at the values ``subs[j] * base**i``. """ - @_api.delete_parameter("3.8", "numdecs") - def __init__(self, base=10.0, subs=(1.0,), numdecs=4, numticks=None): + def __init__(self, base=10.0, subs=(1.0,), numticks=None): """ Parameters ---------- @@ -2305,24 +2304,17 @@ def __init__(self, base=10.0, subs=(1.0,), numdecs=4, numticks=None): numticks = 'auto' self._base = float(base) self._set_subs(subs) - self._numdecs = numdecs self.numticks = numticks - @_api.delete_parameter("3.8", "numdecs") - def set_params(self, base=None, subs=None, numdecs=None, numticks=None): + def set_params(self, base=None, subs=None, numticks=None): """Set parameters within this locator.""" if base is not None: self._base = float(base) if subs is not None: self._set_subs(subs) - if numdecs is not None: - self._numdecs = numdecs if numticks is not None: self.numticks = numticks - numdecs = _api.deprecate_privatize_attribute( - "3.8", addendum="This attribute has no effect.") - def _set_subs(self, subs): """ Set the minor ticks for the log scaling every ``base**i*subs[j]``. diff --git a/lib/matplotlib/ticker.pyi b/lib/matplotlib/ticker.pyi index f026b4943c94..4ecc6054feb9 100644 --- a/lib/matplotlib/ticker.pyi +++ b/lib/matplotlib/ticker.pyi @@ -231,20 +231,17 @@ class MaxNLocator(Locator): def view_limits(self, dmin: float, dmax: float) -> tuple[float, float]: ... class LogLocator(Locator): - numdecs: float numticks: int | None def __init__( self, base: float = ..., subs: None | Literal["auto", "all"] | Sequence[float] = ..., - numdecs: float = ..., numticks: int | None = ..., ) -> None: ... def set_params( self, base: float | None = ..., subs: Literal["auto", "all"] | Sequence[float] | None = ..., - numdecs: float | None = ..., numticks: int | None = ..., ) -> None: ... From 05a1c3151f90fc7c5e7641ea7b1053e30b1f77fd Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Wed, 8 May 2024 09:45:13 +0200 Subject: [PATCH 0166/1230] Expire deprecations in proj3d --- doc/api/toolkits/mplot3d.rst | 6 --- lib/mpl_toolkits/mplot3d/proj3d.py | 64 ------------------------------ 2 files changed, 70 deletions(-) diff --git a/doc/api/toolkits/mplot3d.rst b/doc/api/toolkits/mplot3d.rst index f14918314b97..0d860bd2cfea 100644 --- a/doc/api/toolkits/mplot3d.rst +++ b/doc/api/toolkits/mplot3d.rst @@ -118,12 +118,6 @@ the toolbar pan and zoom buttons are not used. :template: autosummary.rst proj3d.inv_transform - proj3d.persp_transformation - proj3d.proj_points - proj3d.proj_trans_points proj3d.proj_transform proj3d.proj_transform_clip - proj3d.rot_x - proj3d.transform - proj3d.view_transformation proj3d.world_transformation diff --git a/lib/mpl_toolkits/mplot3d/proj3d.py b/lib/mpl_toolkits/mplot3d/proj3d.py index 1fcbafbbcdbc..efee2699767e 100644 --- a/lib/mpl_toolkits/mplot3d/proj3d.py +++ b/lib/mpl_toolkits/mplot3d/proj3d.py @@ -29,14 +29,6 @@ def world_transformation(xmin, xmax, [ 0, 0, 0, 1]]) -@_api.deprecated("3.8") -def rotation_about_vector(v, angle): - """ - Produce a rotation matrix for an angle in radians about a vector. - """ - return _rotation_about_vector(v, angle) - - def _rotation_about_vector(v, angle): """ Produce a rotation matrix for an angle in radians about a vector. @@ -116,32 +108,6 @@ def _view_transformation_uvw(u, v, w, E): return M -@_api.deprecated("3.8") -def view_transformation(E, R, V, roll): - """ - Return the view transformation matrix. - - Parameters - ---------- - E : 3-element numpy array - The coordinates of the eye/camera. - R : 3-element numpy array - The coordinates of the center of the view box. - V : 3-element numpy array - Unit vector in the direction of the vertical axis. - roll : float - The roll angle in radians. - """ - u, v, w = _view_axes(E, R, V, roll) - M = _view_transformation_uvw(u, v, w, E) - return M - - -@_api.deprecated("3.8") -def persp_transformation(zfront, zback, focal_length): - return _persp_transformation(zfront, zback, focal_length) - - def _persp_transformation(zfront, zback, focal_length): e = focal_length a = 1 # aspect ratio @@ -154,11 +120,6 @@ def _persp_transformation(zfront, zback, focal_length): return proj_matrix -@_api.deprecated("3.8") -def ortho_transformation(zfront, zback): - return _ortho_transformation(zfront, zback) - - def _ortho_transformation(zfront, zback): # note: w component in the resulting vector will be (zback-zfront), not 1 a = -(zfront + zback) @@ -217,11 +178,6 @@ def proj_transform(xs, ys, zs, M): return _proj_transform_vec(vec, M) -transform = _api.deprecated( - "3.8", obj_type="function", name="transform", - alternative="proj_transform")(proj_transform) - - @_api.deprecated("3.10") def proj_transform_clip(xs, ys, zs, M): return _proj_transform_clip(xs, ys, zs, M, focal_length=np.inf) @@ -237,30 +193,10 @@ def _proj_transform_clip(xs, ys, zs, M, focal_length): return _proj_transform_vec_clip(vec, M, focal_length) -@_api.deprecated("3.8") -def proj_points(points, M): - return _proj_points(points, M) - - def _proj_points(points, M): return np.column_stack(_proj_trans_points(points, M)) -@_api.deprecated("3.8") -def proj_trans_points(points, M): - return _proj_trans_points(points, M) - - def _proj_trans_points(points, M): xs, ys, zs = zip(*points) return proj_transform(xs, ys, zs, M) - - -@_api.deprecated("3.8") -def rot_x(V, alpha): - cosa, sina = np.cos(alpha), np.sin(alpha) - M1 = np.array([[1, 0, 0, 0], - [0, cosa, -sina, 0], - [0, sina, cosa, 0], - [0, 0, 0, 1]]) - return np.dot(M1, V) From e36397c508544c5097d8b79a011801ee5b0717fb Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Wed, 8 May 2024 09:48:45 +0200 Subject: [PATCH 0167/1230] Expire keyword only deprecations --- lib/matplotlib/axes/_base.py | 5 ++--- lib/matplotlib/figure.py | 3 +-- lib/mpl_toolkits/axes_grid1/parasite_axes.py | 3 +-- lib/mpl_toolkits/mplot3d/axes3d.py | 5 ++--- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index a0e588569465..d0e049284068 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -4420,9 +4420,8 @@ def get_default_bbox_extra_artists(self): return [a for a in artists if a.get_visible() and a.get_in_layout() and (isinstance(a, noclip) or not a._fully_clipped_to_axes())] - @_api.make_keyword_only("3.8", "call_axes_locator") - def get_tightbbox(self, renderer=None, call_axes_locator=True, - bbox_extra_artists=None, *, for_layout_only=False): + def get_tightbbox(self, renderer=None, *, call_axes_locator=True, + bbox_extra_artists=None, for_layout_only=False): """ Return the tight bounding box of the Axes, including axis and their decorators (xlabel, title, etc). diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 41d4b6078223..796df51af997 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1800,8 +1800,7 @@ def get_default_bbox_extra_artists(self): bbox_artists.extend(ax.get_default_bbox_extra_artists()) return bbox_artists - @_api.make_keyword_only("3.8", "bbox_extra_artists") - def get_tightbbox(self, renderer=None, bbox_extra_artists=None): + def get_tightbbox(self, renderer=None, *, bbox_extra_artists=None): """ Return a (tight) bounding box of the figure *in inches*. diff --git a/lib/mpl_toolkits/axes_grid1/parasite_axes.py b/lib/mpl_toolkits/axes_grid1/parasite_axes.py index 2a2b5957e844..c4f86b313bfd 100644 --- a/lib/mpl_toolkits/axes_grid1/parasite_axes.py +++ b/lib/mpl_toolkits/axes_grid1/parasite_axes.py @@ -215,8 +215,7 @@ def _remove_any_twin(self, ax): self.axis[tuple(restore)].set_visible(True) self.axis[tuple(restore)].toggle(ticklabels=False, label=False) - @_api.make_keyword_only("3.8", "call_axes_locator") - def get_tightbbox(self, renderer=None, call_axes_locator=True, + def get_tightbbox(self, renderer=None, *, call_axes_locator=True, bbox_extra_artists=None): bbs = [ *[ax.get_tightbbox(renderer, call_axes_locator=call_axes_locator) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index ea93d3eadf82..c89fc2d0e40a 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -3757,9 +3757,8 @@ def _digout_minmax(err_arr, coord_label): return errlines, caplines, limmarks - @_api.make_keyword_only("3.8", "call_axes_locator") - def get_tightbbox(self, renderer=None, call_axes_locator=True, - bbox_extra_artists=None, *, for_layout_only=False): + def get_tightbbox(self, renderer=None, *, call_axes_locator=True, + bbox_extra_artists=None, for_layout_only=False): ret = super().get_tightbbox(renderer, call_axes_locator=call_axes_locator, bbox_extra_artists=bbox_extra_artists, From 8aafa3aef83146cf51d61dfefea33548f5cce0c8 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Fri, 16 Aug 2024 12:02:59 +0200 Subject: [PATCH 0168/1230] Add API removal note --- .../next_api_changes/removals/28183-OG.rst | 58 +++++++++++++++++++ .../api_changes_3.8.0/deprecations.rst | 4 +- 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 doc/api/next_api_changes/removals/28183-OG.rst diff --git a/doc/api/next_api_changes/removals/28183-OG.rst b/doc/api/next_api_changes/removals/28183-OG.rst new file mode 100644 index 000000000000..9511a33b5519 --- /dev/null +++ b/doc/api/next_api_changes/removals/28183-OG.rst @@ -0,0 +1,58 @@ +``Tick.set_label1`` and ``Tick.set_label2`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... are removed. Calling these methods from third-party code usually had no +effect, as the labels are overwritten at draw time by the tick formatter. + + +Functions in ``mpl_toolkits.mplot3d.proj3d`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The function ``transform`` is just an alias for ``proj_transform``, +use the latter instead. + +The following functions were either unused (so no longer required in Matplotlib) +or considered private. + +* ``ortho_transformation`` +* ``persp_transformation`` +* ``proj_points`` +* ``proj_trans_points`` +* ``rot_x`` +* ``rotation_about_vector`` +* ``view_transformation`` + + +Arguments other than ``renderer`` to ``get_tightbbox`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... are keyword-only arguments. This is for consistency and that +different classes have different additional arguments. + + +Method parameters renamed to match base classes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The only parameter of ``transform_affine`` and ``transform_non_affine`` in ``Transform`` subclasses is renamed +to *values*. + +The *points* parameter of ``transforms.IdentityTransform.transform`` is renamed to *values*. + +The *trans* parameter of ``table.Cell.set_transform`` is renamed to *t* consistently with +`.Artist.set_transform`. + +The *clippath* parameters of ``axis.Axis.set_clip_path`` and ``axis.Tick.set_clip_path`` are +renamed to *path* consistently with `.Artist.set_clip_path`. + +The *s* parameter of ``images.NonUniformImage.set_filternorm`` is renamed to *filternorm* +consistently with ``_ImageBase.set_filternorm``. + +The *s* parameter of ``images.NonUniformImage.set_filterrad`` is renamed to *filterrad* +consistently with ``_ImageBase.set_filterrad``. + +The only parameter of ``Annotation.contains`` and ``Legend.contains`` is renamed to *mouseevent* +consistently with `.Artist.contains`. + + +*numdecs* parameter and attribute of ``LogLocator`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... are removed without replacement, because they had no effect. diff --git a/doc/api/prev_api_changes/api_changes_3.8.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.8.0/deprecations.rst index b442a4af51dc..5398cec623b9 100644 --- a/doc/api/prev_api_changes/api_changes_3.8.0/deprecations.rst +++ b/doc/api/prev_api_changes/api_changes_3.8.0/deprecations.rst @@ -153,10 +153,10 @@ The *clippath* parameters of ``axis.Axis.set_clip_path`` and ``axis.Tick.set_cl renamed to *path* consistently with `.Artist.set_clip_path`. The *s* parameter of ``images.NonUniformImage.set_filternorm`` is renamed to *filternorm* -consistently with ```_ImageBase.set_filternorm``. +consistently with ``_ImageBase.set_filternorm``. The *s* parameter of ``images.NonUniformImage.set_filterrad`` is renamed to *filterrad* -consistently with ```_ImageBase.set_filterrad``. +consistently with ``_ImageBase.set_filterrad``. *numdecs* parameter and attribute of ``LogLocator`` From a031573878e709e9cfc6324c8f8f85766b599102 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 16 Aug 2024 21:39:14 -0400 Subject: [PATCH 0169/1230] ci: Avoid setuptools 72.2.0 when installing kiwi on PyPy Due to https://github.com/pypa/setuptools/issues/4571, kiwisolver fails to build on PyPy. Until kiwisolver has PyPy 3.10 wheels (https://github.com/nucleic/kiwi/pull/182), we should avoid the buggy setuptools. --- .github/workflows/cibuildwheel.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index e258ea38c482..0db8c53b3a79 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -193,6 +193,14 @@ jobs: env: CIBW_BUILD: "pp310-*" CIBW_ARCHS: ${{ matrix.cibw_archs }} + # Work around for https://github.com/pypa/setuptools/issues/4571 + # This can be removed once kiwisolver has wheels for PyPy 3.10 + # https://github.com/nucleic/kiwi/pull/182 + CIBW_BEFORE_TEST: >- + export PIP_CONSTRAINT=pypy-constraint.txt && + echo "setuptools!=72.2.0" > $PIP_CONSTRAINT && + pip install kiwisolver && + unset PIP_CONSTRAINT if: matrix.cibw_archs != 'aarch64' && matrix.os != 'windows-latest' - uses: actions/upload-artifact@v4 From e06ebbdad4ef180e211b5da1ebe132450acd9ea1 Mon Sep 17 00:00:00 2001 From: Matthew Petroff Date: Sun, 3 Mar 2024 15:37:57 -0500 Subject: [PATCH 0170/1230] Add ten-color accessible color cycle as style sheet. Color cycle survey palette from Petroff (2021): https://arxiv.org/abs/2107.02270 https://github.com/mpetroff/accessible-color-cycles --- doc/users/next_whats_new/ccs_color_cycle.rst | 19 +++++++++++++++++++ .../mpl-data/stylelib/ccs10.mplstyle | 5 +++++ 2 files changed, 24 insertions(+) create mode 100644 doc/users/next_whats_new/ccs_color_cycle.rst create mode 100644 lib/matplotlib/mpl-data/stylelib/ccs10.mplstyle diff --git a/doc/users/next_whats_new/ccs_color_cycle.rst b/doc/users/next_whats_new/ccs_color_cycle.rst new file mode 100644 index 000000000000..d10562919278 --- /dev/null +++ b/doc/users/next_whats_new/ccs_color_cycle.rst @@ -0,0 +1,19 @@ +New more-accessible color cycle +------------------------------- + +A new color cycle named 'ccs10' was added. This cycle was constructed using a +combination of algorithmically-enforced accessibility constraints, including +color-vision-deficiency modeling, and a machine-learning-based aesthetics model +developed from a crowdsourced color-preference survey. It aims to be both +generally pleasing aesthetically and colorblind accessible such that it could +serve as a default in the aim of universal design. For more details +see `Petroff, M. A.: "Accessible Color Sequences for Data Visualization" +`_ and related `SciPy talk`_. A demonstration +is included in the style sheets reference_. To load this color cycle in place +of the default:: + + import matplotlib.pyplot as plt + plt.style.use('ccs10') + +.. _reference: https://matplotlib.org/gallery/style_sheets/style_sheets_reference.html +.. _SciPy talk: https://www.youtube.com/watch?v=Gapv8wR5DYU diff --git a/lib/matplotlib/mpl-data/stylelib/ccs10.mplstyle b/lib/matplotlib/mpl-data/stylelib/ccs10.mplstyle new file mode 100644 index 000000000000..62d1262a09cd --- /dev/null +++ b/lib/matplotlib/mpl-data/stylelib/ccs10.mplstyle @@ -0,0 +1,5 @@ +# Color cycle survey palette from Petroff (2021): +# https://arxiv.org/abs/2107.02270 +# https://github.com/mpetroff/accessible-color-cycles +axes.prop_cycle: cycler('color', ['3f90da', 'ffa90e', 'bd1f01', '94a4a2', '832db6', 'a96b59', 'e76300', 'b9ac70', '717581', '92dadd']) +patch.facecolor: 3f90da From 44a4abaa414b4d0867f8aa0defd6a388b77c6f5f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 16 Aug 2024 16:21:58 -0400 Subject: [PATCH 0171/1230] MNT: rename style See discussion in https://github.com/mpetroff/accessible-color-cycles/issues/1 Co-authored-by: Matthew Feickert --- doc/users/next_whats_new/ccs_color_cycle.rst | 4 ++-- .../mpl-data/stylelib/{ccs10.mplstyle => petroff10.mplstyle} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename lib/matplotlib/mpl-data/stylelib/{ccs10.mplstyle => petroff10.mplstyle} (100%) diff --git a/doc/users/next_whats_new/ccs_color_cycle.rst b/doc/users/next_whats_new/ccs_color_cycle.rst index d10562919278..2c9c0c85145c 100644 --- a/doc/users/next_whats_new/ccs_color_cycle.rst +++ b/doc/users/next_whats_new/ccs_color_cycle.rst @@ -1,7 +1,7 @@ New more-accessible color cycle ------------------------------- -A new color cycle named 'ccs10' was added. This cycle was constructed using a +A new color cycle named 'petroff10' was added. This cycle was constructed using a combination of algorithmically-enforced accessibility constraints, including color-vision-deficiency modeling, and a machine-learning-based aesthetics model developed from a crowdsourced color-preference survey. It aims to be both @@ -13,7 +13,7 @@ is included in the style sheets reference_. To load this color cycle in place of the default:: import matplotlib.pyplot as plt - plt.style.use('ccs10') + plt.style.use('petroff10') .. _reference: https://matplotlib.org/gallery/style_sheets/style_sheets_reference.html .. _SciPy talk: https://www.youtube.com/watch?v=Gapv8wR5DYU diff --git a/lib/matplotlib/mpl-data/stylelib/ccs10.mplstyle b/lib/matplotlib/mpl-data/stylelib/petroff10.mplstyle similarity index 100% rename from lib/matplotlib/mpl-data/stylelib/ccs10.mplstyle rename to lib/matplotlib/mpl-data/stylelib/petroff10.mplstyle From ff584d790e2fcb217cad5e363e0291d036ea1713 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 16 Aug 2024 16:38:12 -0400 Subject: [PATCH 0172/1230] ENH: add petroff10 to the sequence registry --- lib/matplotlib/_cm.py | 14 ++++++++++++++ lib/matplotlib/colors.py | 1 + 2 files changed, 15 insertions(+) diff --git a/lib/matplotlib/_cm.py b/lib/matplotlib/_cm.py index 59d260107f3b..b942d1697934 100644 --- a/lib/matplotlib/_cm.py +++ b/lib/matplotlib/_cm.py @@ -1366,6 +1366,20 @@ def _gist_yarg(x): return 1 - x ) +_petroff10_data = ( + (0.24705882352941178, 0.5647058823529412, 0.8549019607843137), # 3f90da + (1.0, 0.6627450980392157, 0.054901960784313725), # ffa90e + (0.7411764705882353, 0.12156862745098039, 0.00392156862745098), # bd1f01 + (0.5803921568627451, 0.6431372549019608, 0.6352941176470588), # 94a4a2 + (0.5137254901960784, 0.17647058823529413, 0.7137254901960784), # 832db6 + (0.6627450980392157, 0.4196078431372549, 0.34901960784313724), # a96b59 + (0.9058823529411765, 0.38823529411764707, 0.0), # e76300 + (0.7254901960784313, 0.6745098039215687, 0.4392156862745098), # b9ac70 + (0.44313725490196076, 0.4588235294117647, 0.5058823529411764), # 717581 + (0.5725490196078431, 0.8549019607843137, 0.8666666666666667), # 92dadd +) + + datad = { 'Blues': _Blues_data, 'BrBG': _BrBG_data, diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 5f40e7b0fb9a..7c127fce7819 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -129,6 +129,7 @@ class ColorSequenceRegistry(Mapping): 'Set1': _cm._Set1_data, 'Set2': _cm._Set2_data, 'Set3': _cm._Set3_data, + 'petroff10': _cm._petroff10_data, } def __init__(self): From 055ad6e22ce5c75b5da7e05edd81911cd4948912 Mon Sep 17 00:00:00 2001 From: Matthew Petroff Date: Sun, 18 Aug 2024 16:38:26 -0400 Subject: [PATCH 0173/1230] MNT: Update test to match sequence registry. --- lib/matplotlib/tests/test_colors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index d99dd91e9cf5..3fd7cbdc9d46 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -1634,7 +1634,7 @@ def test_color_sequences(): assert plt.color_sequences is matplotlib.color_sequences # same registry assert list(plt.color_sequences) == [ 'tab10', 'tab20', 'tab20b', 'tab20c', 'Pastel1', 'Pastel2', 'Paired', - 'Accent', 'Dark2', 'Set1', 'Set2', 'Set3'] + 'Accent', 'Dark2', 'Set1', 'Set2', 'Set3', 'petroff10'] assert len(plt.color_sequences['tab10']) == 10 assert len(plt.color_sequences['tab20']) == 20 From aac678396e7501b97faa511bf3f677994a860770 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 20:14:23 -0400 Subject: [PATCH 0174/1230] Simplify compound statements in dviread I find it a bit confusing to use tuple packing and unpacking when the RHS is more complex than a simple constant. Also, if we're assigning an immutable constant to multiple things, it's simpler to use compound assignment intead of the multiple copies in a tuple. --- lib/matplotlib/dviread.py | 41 ++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 7d61367fd661..3d21d06f0764 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -271,7 +271,8 @@ def _output(self): Output the text and boxes belonging to the most recent page. page = dvi._output() """ - minx, miny, maxx, maxy = np.inf, np.inf, -np.inf, -np.inf + minx = miny = np.inf + maxx = maxy = -np.inf maxy_pure = -np.inf for elt in self.text + self.boxes: if isinstance(elt, Box): @@ -422,7 +423,7 @@ def _nop(self, _): @_dispatch(139, state=_dvistate.outer, args=('s4',)*11) def _bop(self, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, p): self.state = _dvistate.inpage - self.h, self.v, self.w, self.x, self.y, self.z = 0, 0, 0, 0, 0, 0 + self.h = self.v = self.w = self.x = self.y = self.z = 0 self.stack = [] self.text = [] # list of Text objects self.boxes = [] # list of Box objects @@ -678,8 +679,8 @@ def _read(self): Read one page from the file. Return True if successful, False if there were no more pages. """ - packet_char, packet_ends = None, None - packet_len, packet_width = None, None + packet_char = packet_ends = None + packet_len = packet_width = None while True: byte = self.file.read(1)[0] # If we are in a packet, execute the dvi instructions @@ -687,7 +688,7 @@ def _read(self): byte_at = self.file.tell()-1 if byte_at == packet_ends: self._finalize_packet(packet_char, packet_width) - packet_len, packet_char, packet_width = None, None, None + packet_len = packet_char = packet_width = None # fall through to out-of-packet code elif byte_at > packet_ends: raise ValueError("Packet length mismatch in vf file") @@ -701,23 +702,31 @@ def _read(self): # We are outside a packet if byte < 242: # a short packet (length given by byte) packet_len = byte - packet_char, packet_width = self._arg(1), self._arg(3) + packet_char = self._arg(1) + packet_width = self._arg(3) packet_ends = self._init_packet(byte) self.state = _dvistate.inpage elif byte == 242: # a long packet - packet_len, packet_char, packet_width = \ - [self._arg(x) for x in (4, 4, 4)] + packet_len = self._arg(4) + packet_char = self._arg(4) + packet_width = self._arg(4) self._init_packet(packet_len) elif 243 <= byte <= 246: k = self._arg(byte - 242, byte == 246) - c, s, d, a, l = [self._arg(x) for x in (4, 4, 4, 1, 1)] + c = self._arg(4) + s = self._arg(4) + d = self._arg(4) + a = self._arg(1) + l = self._arg(1) self._fnt_def_real(k, c, s, d, a, l) if self._first_font is None: self._first_font = k elif byte == 247: # preamble - i, k = self._arg(1), self._arg(1) + i = self._arg(1) + k = self._arg(1) x = self.file.read(k) - cs, ds = self._arg(4), self._arg(4) + cs = self._arg(4) + ds = self._arg(4) self._pre(i, x, cs, ds) elif byte == 248: # postamble (just some number of 248s) break @@ -727,8 +736,10 @@ def _read(self): def _init_packet(self, pl): if self.state != _dvistate.outer: raise ValueError("Misplaced packet in vf file") - self.h, self.v, self.w, self.x, self.y, self.z = 0, 0, 0, 0, 0, 0 - self.stack, self.text, self.boxes = [], [], [] + self.h = self.v = self.w = self.x = self.y = self.z = 0 + self.stack = [] + self.text = [] + self.boxes = [] self.f = self._first_font self._missing_font = None return self.file.tell() + pl @@ -794,7 +805,9 @@ def __init__(self, filename): widths = struct.unpack(f'!{nw}i', file.read(4*nw)) heights = struct.unpack(f'!{nh}i', file.read(4*nh)) depths = struct.unpack(f'!{nd}i', file.read(4*nd)) - self.width, self.height, self.depth = {}, {}, {} + self.width = {} + self.height = {} + self.depth = {} for idx, char in enumerate(range(bc, ec+1)): byte0 = char_info[4*idx] byte1 = char_info[4*idx+1] From 0049fee4757abfa59316e1ce000ac6bb5c5cdc86 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Jul 2024 20:24:38 -0400 Subject: [PATCH 0175/1230] Use more f-strings in dviread --- lib/matplotlib/dviread.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 3d21d06f0764..5561be7b22ed 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -507,7 +507,7 @@ def _fnt_def_real(self, k, c, s, d, a, l): self.fonts[k] = exc return if c != 0 and tfm.checksum != 0 and c != tfm.checksum: - raise ValueError('tfm checksum mismatch: %s' % n) + raise ValueError(f'tfm checksum mismatch: {n}') try: vf = _vffile(fontname) except FileNotFoundError: @@ -518,7 +518,7 @@ def _fnt_def_real(self, k, c, s, d, a, l): def _pre(self, i, num, den, mag, k): self.file.read(k) # comment in the dvi file if i != 2: - raise ValueError("Unknown dvi format %d" % i) + raise ValueError(f"Unknown dvi format {i}") if num != 25400000 or den != 7227 * 2**16: raise ValueError("Nonstandard units in dvi file") # meaning: TeX always uses those exact values, so it @@ -694,8 +694,7 @@ def _read(self): raise ValueError("Packet length mismatch in vf file") else: if byte in (139, 140) or byte >= 243: - raise ValueError( - "Inappropriate opcode %d in vf file" % byte) + raise ValueError(f"Inappropriate opcode {byte} in vf file") Dvi._dtable[byte](self, byte) continue @@ -731,7 +730,7 @@ def _read(self): elif byte == 248: # postamble (just some number of 248s) break else: - raise ValueError("Unknown vf opcode %d" % byte) + raise ValueError(f"Unknown vf opcode {byte}") def _init_packet(self, pl): if self.state != _dvistate.outer: @@ -755,7 +754,7 @@ def _pre(self, i, x, cs, ds): if self.state is not _dvistate.pre: raise ValueError("pre command in middle of vf file") if i != 202: - raise ValueError("Unknown vf format %d" % i) + raise ValueError(f"Unknown vf format {i}") if len(x): _log.debug('vf file comment: %s', x) self.state = _dvistate.outer From d734a450b24cc68346b3432f444623f14cd2f7f3 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 20 Aug 2024 04:05:19 -0400 Subject: [PATCH 0176/1230] Rename Dvi._arg to Dvi._read_arg for clarity Hopefully it's clearer that this method is doing something stateful now. --- lib/matplotlib/dviread.py | 46 +++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 5561be7b22ed..040ca5ef4365 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -132,20 +132,20 @@ def glyph_name_or_index(self): # raw: Return delta as is. raw=lambda dvi, delta: delta, # u1: Read 1 byte as an unsigned number. - u1=lambda dvi, delta: dvi._arg(1, signed=False), + u1=lambda dvi, delta: dvi._read_arg(1, signed=False), # u4: Read 4 bytes as an unsigned number. - u4=lambda dvi, delta: dvi._arg(4, signed=False), + u4=lambda dvi, delta: dvi._read_arg(4, signed=False), # s4: Read 4 bytes as a signed number. - s4=lambda dvi, delta: dvi._arg(4, signed=True), + s4=lambda dvi, delta: dvi._read_arg(4, signed=True), # slen: Read delta bytes as a signed number, or None if delta is None. - slen=lambda dvi, delta: dvi._arg(delta, signed=True) if delta else None, + slen=lambda dvi, delta: dvi._read_arg(delta, signed=True) if delta else None, # slen1: Read (delta + 1) bytes as a signed number. - slen1=lambda dvi, delta: dvi._arg(delta + 1, signed=True), + slen1=lambda dvi, delta: dvi._read_arg(delta + 1, signed=True), # ulen1: Read (delta + 1) bytes as an unsigned number. - ulen1=lambda dvi, delta: dvi._arg(delta + 1, signed=False), + ulen1=lambda dvi, delta: dvi._read_arg(delta + 1, signed=False), # olen1: Read (delta + 1) bytes as an unsigned number if less than 4 bytes, # as a signed number if 4 bytes. - olen1=lambda dvi, delta: dvi._arg(delta + 1, signed=(delta == 3)), + olen1=lambda dvi, delta: dvi._read_arg(delta + 1, signed=(delta == 3)), ) @@ -358,7 +358,7 @@ def _read(self): self.close() return False - def _arg(self, nbytes, signed=False): + def _read_arg(self, nbytes, signed=False): """ Read and return a big-endian integer *nbytes* long. Signedness is determined by the *signed* keyword. @@ -701,31 +701,31 @@ def _read(self): # We are outside a packet if byte < 242: # a short packet (length given by byte) packet_len = byte - packet_char = self._arg(1) - packet_width = self._arg(3) + packet_char = self._read_arg(1) + packet_width = self._read_arg(3) packet_ends = self._init_packet(byte) self.state = _dvistate.inpage elif byte == 242: # a long packet - packet_len = self._arg(4) - packet_char = self._arg(4) - packet_width = self._arg(4) + packet_len = self._read_arg(4) + packet_char = self._read_arg(4) + packet_width = self._read_arg(4) self._init_packet(packet_len) elif 243 <= byte <= 246: - k = self._arg(byte - 242, byte == 246) - c = self._arg(4) - s = self._arg(4) - d = self._arg(4) - a = self._arg(1) - l = self._arg(1) + k = self._read_arg(byte - 242, byte == 246) + c = self._read_arg(4) + s = self._read_arg(4) + d = self._read_arg(4) + a = self._read_arg(1) + l = self._read_arg(1) self._fnt_def_real(k, c, s, d, a, l) if self._first_font is None: self._first_font = k elif byte == 247: # preamble - i = self._arg(1) - k = self._arg(1) + i = self._read_arg(1) + k = self._read_arg(1) x = self.file.read(k) - cs = self._arg(4) - ds = self._arg(4) + cs = self._read_arg(4) + ds = self._read_arg(4) self._pre(i, x, cs, ds) elif byte == 248: # postamble (just some number of 248s) break From 146ba539f9e67c00442ea5d1a2462cf98fec8591 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:09:25 +0000 Subject: [PATCH 0177/1230] compressed layout moves suptitle --- .../next_api_changes/behavior/28734-REC.rst | 7 ++++++ lib/matplotlib/_constrained_layout.py | 7 ++++++ .../tests/test_constrainedlayout.py | 24 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 doc/api/next_api_changes/behavior/28734-REC.rst diff --git a/doc/api/next_api_changes/behavior/28734-REC.rst b/doc/api/next_api_changes/behavior/28734-REC.rst new file mode 100644 index 000000000000..825922f4fafb --- /dev/null +++ b/doc/api/next_api_changes/behavior/28734-REC.rst @@ -0,0 +1,7 @@ +``suptitle`` in compressed layout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Compressed layout now automatically positions the `~.Figure.suptitle` just +above the top row of axes. To keep this title in its previous position, +either pass ``in_layout=False`` or explicitly set ``y=0.98`` in the +`~.Figure.suptitle` call. diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index b960f363e9d4..28dbe1104004 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -140,6 +140,13 @@ def do_constrained_layout(fig, h_pad, w_pad, w_pad=w_pad, hspace=hspace, wspace=wspace) else: _api.warn_external(warn_collapsed) + + if ((suptitle := fig._suptitle) is not None and + suptitle.get_in_layout() and suptitle._autopos): + x, _ = suptitle.get_position() + suptitle.set_position( + (x, layoutgrids[fig].get_inner_bbox().y1 + h_pad)) + suptitle.set_verticalalignment('bottom') else: _api.warn_external(warn_collapsed) reset_margins(layoutgrids, fig) diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index 4dc4d9501ec1..e42e2ee9bfd8 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -662,6 +662,30 @@ def test_compressed1(): np.testing.assert_allclose(pos.y0, 0.1934, atol=1e-3) +def test_compressed_suptitle(): + fig, (ax0, ax1) = plt.subplots( + nrows=2, figsize=(4, 10), layout="compressed", + gridspec_kw={"height_ratios": (1 / 4, 3 / 4), "hspace": 0}) + + ax0.axis("equal") + ax0.set_box_aspect(1/3) + + ax1.axis("equal") + ax1.set_box_aspect(1) + + title = fig.suptitle("Title") + fig.draw_without_rendering() + assert title.get_position()[1] == pytest.approx(0.7457, abs=1e-3) + + title = fig.suptitle("Title", y=0.98) + fig.draw_without_rendering() + assert title.get_position()[1] == 0.98 + + title = fig.suptitle("Title", in_layout=False) + fig.draw_without_rendering() + assert title.get_position()[1] == 0.98 + + @pytest.mark.parametrize('arg, state', [ (True, True), (False, False), From 3443604e8c2d6431224f41d5ea3691f42e8c5dfa Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:19:47 +0100 Subject: [PATCH 0178/1230] Add sphinxcontrib-video to environment.yml --- environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/environment.yml b/environment.yml index 264f02800690..5cc009303672 100644 --- a/environment.yml +++ b/environment.yml @@ -46,6 +46,7 @@ dependencies: - pip: - mpl-sphinx-theme~=3.8.0 - sphinxcontrib-svg2pdfconverter>=1.1.0 + - sphinxcontrib-video>=0.2.1 - pikepdf # testing - black<24 From 541b924fa9c2d769b43ca327974d06ea231f5157 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 20 Aug 2024 06:16:59 -0400 Subject: [PATCH 0179/1230] TST: Fix image comparison directory for test_striped_lines The image comparison directory is determined by `inspect.getfile(func)`, but when a test is wrapped in `rc_context`, the file returned is `contextlib` since that decorator is `contextlib.contextmanager`. Since this test uses `check_figures_equal`, that doesn't break it, but it does break the `triage_tests.py` tool as it cannot find a corresponding baseline image directory. In this case, the context doesn't set anything that would affect figures, so inline the effect of the context as keyword arguments. --- lib/matplotlib/tests/test_collections.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 5e7937053496..2cb8f182f0b5 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -1311,7 +1311,6 @@ def test_check_offsets_dtype(): @pytest.mark.parametrize('gapcolor', ['orange', ['r', 'k']]) @check_figures_equal(extensions=['png']) -@mpl.rc_context({'lines.linewidth': 20}) def test_striped_lines(fig_test, fig_ref, gapcolor): ax_test = fig_test.add_subplot(111) ax_ref = fig_ref.add_subplot(111) @@ -1323,11 +1322,12 @@ def test_striped_lines(fig_test, fig_ref, gapcolor): x = range(1, 6) linestyles = [':', '-', '--'] - ax_test.vlines(x, 0, 1, linestyle=linestyles, gapcolor=gapcolor, alpha=0.5) + ax_test.vlines(x, 0, 1, linewidth=20, linestyle=linestyles, gapcolor=gapcolor, + alpha=0.5) if isinstance(gapcolor, str): gapcolor = [gapcolor] for x, gcol, ls in zip(x, itertools.cycle(gapcolor), itertools.cycle(linestyles)): - ax_ref.axvline(x, 0, 1, linestyle=ls, gapcolor=gcol, alpha=0.5) + ax_ref.axvline(x, 0, 1, linewidth=20, linestyle=ls, gapcolor=gcol, alpha=0.5) From 3900fa24aa082c1727ca274b98747933f1578671 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Wed, 21 Aug 2024 08:22:23 +0100 Subject: [PATCH 0180/1230] Backport PR #28737: TST: Fix image comparison directory for test_striped_lines --- lib/matplotlib/tests/test_collections.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 23e951b17a2f..c4f98d4eeb45 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -1316,7 +1316,6 @@ def test_check_offsets_dtype(): @pytest.mark.parametrize('gapcolor', ['orange', ['r', 'k']]) @check_figures_equal(extensions=['png']) -@mpl.rc_context({'lines.linewidth': 20}) def test_striped_lines(fig_test, fig_ref, gapcolor): ax_test = fig_test.add_subplot(111) ax_ref = fig_ref.add_subplot(111) @@ -1328,11 +1327,12 @@ def test_striped_lines(fig_test, fig_ref, gapcolor): x = range(1, 6) linestyles = [':', '-', '--'] - ax_test.vlines(x, 0, 1, linestyle=linestyles, gapcolor=gapcolor, alpha=0.5) + ax_test.vlines(x, 0, 1, linewidth=20, linestyle=linestyles, gapcolor=gapcolor, + alpha=0.5) if isinstance(gapcolor, str): gapcolor = [gapcolor] for x, gcol, ls in zip(x, itertools.cycle(gapcolor), itertools.cycle(linestyles)): - ax_ref.axvline(x, 0, 1, linestyle=ls, gapcolor=gcol, alpha=0.5) + ax_ref.axvline(x, 0, 1, linewidth=20, linestyle=ls, gapcolor=gcol, alpha=0.5) From ce1af79f25377d135c0f3b4e9cb6e5e84d0239d8 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 21 Aug 2024 09:59:53 +0200 Subject: [PATCH 0181/1230] Tweak interactivity docs wording (and fix capitalization). Interactivity is useful even when not working with "data" (e.g. when plotting mathematical equations). --- galleries/users_explain/figure/event_handling.rst | 15 +++++++++------ galleries/users_explain/figure/interactive.rst | 2 +- .../users_explain/figure/interactive_guide.rst | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/galleries/users_explain/figure/event_handling.rst b/galleries/users_explain/figure/event_handling.rst index 49d73afeb366..32da038634ae 100644 --- a/galleries/users_explain/figure/event_handling.rst +++ b/galleries/users_explain/figure/event_handling.rst @@ -251,7 +251,8 @@ is created every time a mouse is pressed:: def __call__(self, event): print('click', event) - if event.inaxes!=self.line.axes: return + if event.inaxes != self.line.axes: + return self.xs.append(event.xdata) self.ys.append(event.ydata) self.line.set_data(self.xs, self.ys) @@ -277,17 +278,19 @@ event.ydata)``. In addition to the ``LocationEvent`` attributes, it also has: Draggable rectangle exercise ---------------------------- -Write draggable rectangle class that is initialized with a +Write a draggable rectangle class that is initialized with a `.Rectangle` instance but will move its ``xy`` -location when dragged. Hint: you will need to store the original -``xy`` location of the rectangle which is stored as rect.xy and +location when dragged. + +Hint: You will need to store the original +``xy`` location of the rectangle which is stored as ``rect.xy`` and connect to the press, motion and release mouse events. When the mouse is pressed, check to see if the click occurs over your rectangle (see `.Rectangle.contains`) and if it does, store -the rectangle xy and the location of the mouse click in data coords. +the rectangle xy and the location of the mouse click in data coordinates. In the motion event callback, compute the deltax and deltay of the mouse movement, and add those deltas to the origin of the rectangle -you stored. The redraw the figure. On the button release event, just +you stored, then redraw the figure. On the button release event, just reset all the button press data you stored as None. Here is the solution:: diff --git a/galleries/users_explain/figure/interactive.rst b/galleries/users_explain/figure/interactive.rst index 6fd908fcac7a..b06283152e50 100644 --- a/galleries/users_explain/figure/interactive.rst +++ b/galleries/users_explain/figure/interactive.rst @@ -10,7 +10,7 @@ Interactive figures =================== -When working with data, interactivity can be invaluable. The pan/zoom and +Interactivity can be invaluable when exploring plots. The pan/zoom and mouse-location tools built into the Matplotlib GUI windows are often sufficient, but you can also use the event system to build customized data exploration tools. diff --git a/galleries/users_explain/figure/interactive_guide.rst b/galleries/users_explain/figure/interactive_guide.rst index 3b6f527f6d42..b08231e84f7e 100644 --- a/galleries/users_explain/figure/interactive_guide.rst +++ b/galleries/users_explain/figure/interactive_guide.rst @@ -236,7 +236,7 @@ which would poll for new data and update the figure at 1Hz. .. _spin_event_loop: -Explicitly spinning the event Loop +Explicitly spinning the event loop ---------------------------------- .. autosummary:: From fc5bc4a50b6c16cde5e21e122fb370862b56148c Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Wed, 21 Aug 2024 10:17:02 +0100 Subject: [PATCH 0182/1230] Backport PR #28739: Tweak interactivity docs wording (and fix capitalization). --- galleries/users_explain/figure/event_handling.rst | 15 +++++++++------ galleries/users_explain/figure/interactive.rst | 2 +- .../users_explain/figure/interactive_guide.rst | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/galleries/users_explain/figure/event_handling.rst b/galleries/users_explain/figure/event_handling.rst index 49d73afeb366..32da038634ae 100644 --- a/galleries/users_explain/figure/event_handling.rst +++ b/galleries/users_explain/figure/event_handling.rst @@ -251,7 +251,8 @@ is created every time a mouse is pressed:: def __call__(self, event): print('click', event) - if event.inaxes!=self.line.axes: return + if event.inaxes != self.line.axes: + return self.xs.append(event.xdata) self.ys.append(event.ydata) self.line.set_data(self.xs, self.ys) @@ -277,17 +278,19 @@ event.ydata)``. In addition to the ``LocationEvent`` attributes, it also has: Draggable rectangle exercise ---------------------------- -Write draggable rectangle class that is initialized with a +Write a draggable rectangle class that is initialized with a `.Rectangle` instance but will move its ``xy`` -location when dragged. Hint: you will need to store the original -``xy`` location of the rectangle which is stored as rect.xy and +location when dragged. + +Hint: You will need to store the original +``xy`` location of the rectangle which is stored as ``rect.xy`` and connect to the press, motion and release mouse events. When the mouse is pressed, check to see if the click occurs over your rectangle (see `.Rectangle.contains`) and if it does, store -the rectangle xy and the location of the mouse click in data coords. +the rectangle xy and the location of the mouse click in data coordinates. In the motion event callback, compute the deltax and deltay of the mouse movement, and add those deltas to the origin of the rectangle -you stored. The redraw the figure. On the button release event, just +you stored, then redraw the figure. On the button release event, just reset all the button press data you stored as None. Here is the solution:: diff --git a/galleries/users_explain/figure/interactive.rst b/galleries/users_explain/figure/interactive.rst index 6fd908fcac7a..b06283152e50 100644 --- a/galleries/users_explain/figure/interactive.rst +++ b/galleries/users_explain/figure/interactive.rst @@ -10,7 +10,7 @@ Interactive figures =================== -When working with data, interactivity can be invaluable. The pan/zoom and +Interactivity can be invaluable when exploring plots. The pan/zoom and mouse-location tools built into the Matplotlib GUI windows are often sufficient, but you can also use the event system to build customized data exploration tools. diff --git a/galleries/users_explain/figure/interactive_guide.rst b/galleries/users_explain/figure/interactive_guide.rst index 3b6f527f6d42..b08231e84f7e 100644 --- a/galleries/users_explain/figure/interactive_guide.rst +++ b/galleries/users_explain/figure/interactive_guide.rst @@ -236,7 +236,7 @@ which would poll for new data and update the figure at 1Hz. .. _spin_event_loop: -Explicitly spinning the event Loop +Explicitly spinning the event loop ---------------------------------- .. autosummary:: From cbc42296dce9d199ef6f33ee6a893ebb11fbfac5 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Wed, 21 Aug 2024 10:17:02 +0100 Subject: [PATCH 0183/1230] Backport PR #28739: Tweak interactivity docs wording (and fix capitalization). --- galleries/users_explain/figure/event_handling.rst | 15 +++++++++------ galleries/users_explain/figure/interactive.rst | 2 +- .../users_explain/figure/interactive_guide.rst | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/galleries/users_explain/figure/event_handling.rst b/galleries/users_explain/figure/event_handling.rst index 49d73afeb366..32da038634ae 100644 --- a/galleries/users_explain/figure/event_handling.rst +++ b/galleries/users_explain/figure/event_handling.rst @@ -251,7 +251,8 @@ is created every time a mouse is pressed:: def __call__(self, event): print('click', event) - if event.inaxes!=self.line.axes: return + if event.inaxes != self.line.axes: + return self.xs.append(event.xdata) self.ys.append(event.ydata) self.line.set_data(self.xs, self.ys) @@ -277,17 +278,19 @@ event.ydata)``. In addition to the ``LocationEvent`` attributes, it also has: Draggable rectangle exercise ---------------------------- -Write draggable rectangle class that is initialized with a +Write a draggable rectangle class that is initialized with a `.Rectangle` instance but will move its ``xy`` -location when dragged. Hint: you will need to store the original -``xy`` location of the rectangle which is stored as rect.xy and +location when dragged. + +Hint: You will need to store the original +``xy`` location of the rectangle which is stored as ``rect.xy`` and connect to the press, motion and release mouse events. When the mouse is pressed, check to see if the click occurs over your rectangle (see `.Rectangle.contains`) and if it does, store -the rectangle xy and the location of the mouse click in data coords. +the rectangle xy and the location of the mouse click in data coordinates. In the motion event callback, compute the deltax and deltay of the mouse movement, and add those deltas to the origin of the rectangle -you stored. The redraw the figure. On the button release event, just +you stored, then redraw the figure. On the button release event, just reset all the button press data you stored as None. Here is the solution:: diff --git a/galleries/users_explain/figure/interactive.rst b/galleries/users_explain/figure/interactive.rst index 6fd908fcac7a..b06283152e50 100644 --- a/galleries/users_explain/figure/interactive.rst +++ b/galleries/users_explain/figure/interactive.rst @@ -10,7 +10,7 @@ Interactive figures =================== -When working with data, interactivity can be invaluable. The pan/zoom and +Interactivity can be invaluable when exploring plots. The pan/zoom and mouse-location tools built into the Matplotlib GUI windows are often sufficient, but you can also use the event system to build customized data exploration tools. diff --git a/galleries/users_explain/figure/interactive_guide.rst b/galleries/users_explain/figure/interactive_guide.rst index 3b6f527f6d42..b08231e84f7e 100644 --- a/galleries/users_explain/figure/interactive_guide.rst +++ b/galleries/users_explain/figure/interactive_guide.rst @@ -236,7 +236,7 @@ which would poll for new data and update the figure at 1Hz. .. _spin_event_loop: -Explicitly spinning the event Loop +Explicitly spinning the event loop ---------------------------------- .. autosummary:: From 1e35e7379ff35234bcd53941659d3f434ab0cd07 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Wed, 21 Aug 2024 13:43:09 +0200 Subject: [PATCH 0184/1230] Minor fixes in ticker docs --- lib/matplotlib/ticker.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 2b00937f9e29..d824bbe3b6e2 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -435,10 +435,10 @@ class ScalarFormatter(Formatter): lim = (1_000_000, 1_000_010) fig, (ax1, ax2, ax3) = plt.subplots(3, 1, gridspec_kw={'hspace': 2}) - ax1.set(title='offset_notation', xlim=lim) + ax1.set(title='offset notation', xlim=lim) ax2.set(title='scientific notation', xlim=lim) ax2.xaxis.get_major_formatter().set_useOffset(False) - ax3.set(title='floating point notation', xlim=lim) + ax3.set(title='floating-point notation', xlim=lim) ax3.xaxis.get_major_formatter().set_useOffset(False) ax3.xaxis.get_major_formatter().set_scientific(False) @@ -1146,10 +1146,10 @@ def __init__( Parameters ---------- use_overline : bool, default: False - If x > 1/2, with x = 1-v, indicate if x should be displayed as - $\overline{v}$. The default is to display $1-v$. + If x > 1/2, with x = 1 - v, indicate if x should be displayed as + $\overline{v}$. The default is to display $1 - v$. - one_half : str, default: r"\frac{1}{2}" + one_half : str, default: r"\\frac{1}{2}" The string used to represent 1/2. minor : bool, default: False @@ -1179,9 +1179,9 @@ def use_overline(self, use_overline): Parameters ---------- - use_overline : bool, default: False - If x > 1/2, with x = 1-v, indicate if x should be displayed as - $\overline{v}$. The default is to display $1-v$. + use_overline : bool + If x > 1/2, with x = 1 - v, indicate if x should be displayed as + $\overline{v}$. The default is to display $1 - v$. """ self._use_overline = use_overline @@ -1189,7 +1189,7 @@ def set_one_half(self, one_half): r""" Set the way one half is displayed. - one_half : str, default: r"\frac{1}{2}" + one_half : str The string used to represent 1/2. """ self._one_half = one_half @@ -1707,14 +1707,14 @@ def tick_values(self, vmin, vmax): class FixedLocator(Locator): - """ + r""" Place ticks at a set of fixed values. If *nbins* is None ticks are placed at all values. Otherwise, the *locs* array of - possible positions will be subsampled to keep the number of ticks <= - :math:`nbins* +1`. The subsampling will be done to include the smallest absolute - value; for example, if zero is included in the array of possibilities, then it of - the chosen ticks. + possible positions will be subsampled to keep the number of ticks + :math:`\leq nbins + 1`. The subsampling will be done to include the smallest + absolute value; for example, if zero is included in the array of possibilities, then + it will be included in the chosen ticks. """ def __init__(self, locs, nbins=None): @@ -1861,9 +1861,9 @@ def __init__(self, base=1.0, offset=0.0): """ Parameters ---------- - base : float > 0 + base : float > 0, default: 1.0 Interval between ticks. - offset : float + offset : float, default: 0.0 Value added to each multiple of *base*. .. versionadded:: 3.8 @@ -1877,9 +1877,9 @@ def set_params(self, base=None, offset=None): Parameters ---------- - base : float > 0 + base : float > 0, optional Interval between ticks. - offset : float + offset : float, optional Value added to each multiple of *base*. .. versionadded:: 3.8 From 436a12abf2641483a933aabaded8a5df03e6081d Mon Sep 17 00:00:00 2001 From: OdileVidrine <65249488+OdileVidrine@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:38:28 -0400 Subject: [PATCH 0185/1230] ConciseDateFormatter's offset string is correct on an inverted axis (#28501) --- doc/api/next_api_changes/behavior/28501-OV.rst | 5 +++++ lib/matplotlib/dates.py | 7 ++++++- lib/matplotlib/tests/test_dates.py | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 doc/api/next_api_changes/behavior/28501-OV.rst diff --git a/doc/api/next_api_changes/behavior/28501-OV.rst b/doc/api/next_api_changes/behavior/28501-OV.rst new file mode 100644 index 000000000000..cc816e55f696 --- /dev/null +++ b/doc/api/next_api_changes/behavior/28501-OV.rst @@ -0,0 +1,5 @@ +The offset string associated with ConciseDateFormatter will now invert when the axis is inverted +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Previously, when the axis was inverted, the offset string associated with ConciseDateFormatter would not change, +so the offset string indicated the axis was oriented in the wrong direction. Now, when the axis is inverted, the offset +string is oriented correctly. diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index c12d9f31ba4b..15de61f69df7 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -796,7 +796,12 @@ def format_ticks(self, values): if show_offset: # set the offset string: - self.offset_string = tickdatetime[-1].strftime(offsetfmts[level]) + if (self._locator.axis and + self._locator.axis.__name__ in ('xaxis', 'yaxis') + and self._locator.axis.get_inverted()): + self.offset_string = tickdatetime[0].strftime(offsetfmts[level]) + else: + self.offset_string = tickdatetime[-1].strftime(offsetfmts[level]) if self._usetex: self.offset_string = _wrap_in_tex(self.offset_string) else: diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index 4133524e0e1a..2d60e3525b2a 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -636,6 +636,23 @@ def test_concise_formatter_show_offset(t_delta, expected): assert formatter.get_offset() == expected +def test_concise_formatter_show_offset_inverted(): + # Test for github issue #28481 + d1 = datetime.datetime(1997, 1, 1) + d2 = d1 + datetime.timedelta(days=60) + + fig, ax = plt.subplots() + locator = mdates.AutoDateLocator() + formatter = mdates.ConciseDateFormatter(locator) + ax.xaxis.set_major_locator(locator) + ax.xaxis.set_major_formatter(formatter) + ax.invert_xaxis() + + ax.plot([d1, d2], [0, 0]) + fig.canvas.draw() + assert formatter.get_offset() == '1997-Jan' + + def test_concise_converter_stays(): # This test demonstrates problems introduced by gh-23417 (reverted in gh-25278) # In particular, downstream libraries like Pandas had their designated converters From d9d1a4d3f6612e9349f1607f46f359bdce8934f6 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:20:39 +0200 Subject: [PATCH 0186/1230] Backport PR #28743: Minor fixes in ticker docs --- lib/matplotlib/ticker.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 2b00937f9e29..d824bbe3b6e2 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -435,10 +435,10 @@ class ScalarFormatter(Formatter): lim = (1_000_000, 1_000_010) fig, (ax1, ax2, ax3) = plt.subplots(3, 1, gridspec_kw={'hspace': 2}) - ax1.set(title='offset_notation', xlim=lim) + ax1.set(title='offset notation', xlim=lim) ax2.set(title='scientific notation', xlim=lim) ax2.xaxis.get_major_formatter().set_useOffset(False) - ax3.set(title='floating point notation', xlim=lim) + ax3.set(title='floating-point notation', xlim=lim) ax3.xaxis.get_major_formatter().set_useOffset(False) ax3.xaxis.get_major_formatter().set_scientific(False) @@ -1146,10 +1146,10 @@ def __init__( Parameters ---------- use_overline : bool, default: False - If x > 1/2, with x = 1-v, indicate if x should be displayed as - $\overline{v}$. The default is to display $1-v$. + If x > 1/2, with x = 1 - v, indicate if x should be displayed as + $\overline{v}$. The default is to display $1 - v$. - one_half : str, default: r"\frac{1}{2}" + one_half : str, default: r"\\frac{1}{2}" The string used to represent 1/2. minor : bool, default: False @@ -1179,9 +1179,9 @@ def use_overline(self, use_overline): Parameters ---------- - use_overline : bool, default: False - If x > 1/2, with x = 1-v, indicate if x should be displayed as - $\overline{v}$. The default is to display $1-v$. + use_overline : bool + If x > 1/2, with x = 1 - v, indicate if x should be displayed as + $\overline{v}$. The default is to display $1 - v$. """ self._use_overline = use_overline @@ -1189,7 +1189,7 @@ def set_one_half(self, one_half): r""" Set the way one half is displayed. - one_half : str, default: r"\frac{1}{2}" + one_half : str The string used to represent 1/2. """ self._one_half = one_half @@ -1707,14 +1707,14 @@ def tick_values(self, vmin, vmax): class FixedLocator(Locator): - """ + r""" Place ticks at a set of fixed values. If *nbins* is None ticks are placed at all values. Otherwise, the *locs* array of - possible positions will be subsampled to keep the number of ticks <= - :math:`nbins* +1`. The subsampling will be done to include the smallest absolute - value; for example, if zero is included in the array of possibilities, then it of - the chosen ticks. + possible positions will be subsampled to keep the number of ticks + :math:`\leq nbins + 1`. The subsampling will be done to include the smallest + absolute value; for example, if zero is included in the array of possibilities, then + it will be included in the chosen ticks. """ def __init__(self, locs, nbins=None): @@ -1861,9 +1861,9 @@ def __init__(self, base=1.0, offset=0.0): """ Parameters ---------- - base : float > 0 + base : float > 0, default: 1.0 Interval between ticks. - offset : float + offset : float, default: 0.0 Value added to each multiple of *base*. .. versionadded:: 3.8 @@ -1877,9 +1877,9 @@ def set_params(self, base=None, offset=None): Parameters ---------- - base : float > 0 + base : float > 0, optional Interval between ticks. - offset : float + offset : float, optional Value added to each multiple of *base*. .. versionadded:: 3.8 From f1865148351d1db206c8367cdc7bed91a873f851 Mon Sep 17 00:00:00 2001 From: vittoboa <38300176+vittoboa@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:24:04 +0200 Subject: [PATCH 0187/1230] Format drawing of draggable legend in on_release into a single line Co-authored-by: Elliott Sales de Andrade --- lib/matplotlib/offsetbox.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 417806e4599d..194b950a8a30 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -1507,8 +1507,7 @@ def on_release(self, event): self.got_artist = False if self._use_blit: self.canvas.restore_region(self.background) - self.ref_artist.draw( - self.ref_artist.figure._get_renderer()) + self.ref_artist.draw(self.ref_artist.figure._get_renderer()) self.canvas.blit() self.ref_artist.set_animated(False) From 83251ac2a1d780dc4af2bacab07b43552c5b7a70 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 22 Aug 2024 14:50:03 -0400 Subject: [PATCH 0188/1230] Backport PR #28271: Fix draggable legend disappearing when picking while use_blit=True --- lib/matplotlib/offsetbox.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 32c5bafcde1d..194b950a8a30 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -1486,11 +1486,13 @@ def on_motion(self, evt): self.canvas.draw() def on_pick(self, evt): - if self._check_still_parented() and evt.artist == self.ref_artist: - self.mouse_x = evt.mouseevent.x - self.mouse_y = evt.mouseevent.y - self.got_artist = True - if self._use_blit: + if self._check_still_parented(): + if evt.artist == self.ref_artist: + self.mouse_x = evt.mouseevent.x + self.mouse_y = evt.mouseevent.y + self.save_offset() + self.got_artist = True + if self.got_artist and self._use_blit: self.ref_artist.set_animated(True) self.canvas.draw() self.background = \ @@ -1498,13 +1500,15 @@ def on_pick(self, evt): self.ref_artist.draw( self.ref_artist.figure._get_renderer()) self.canvas.blit() - self.save_offset() def on_release(self, event): if self._check_still_parented() and self.got_artist: self.finalize_offset() self.got_artist = False if self._use_blit: + self.canvas.restore_region(self.background) + self.ref_artist.draw(self.ref_artist.figure._get_renderer()) + self.canvas.blit() self.ref_artist.set_animated(False) def _check_still_parented(self): From 725cb38ff1f9005a99a87b898deb1bfeea2dcd35 Mon Sep 17 00:00:00 2001 From: hannah Date: Thu, 22 Aug 2024 15:27:20 -0400 Subject: [PATCH 0189/1230] quick fix dev build by locking out numpy version that's breaking things --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 40ba933cf0d9..2fa296746efb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -98,7 +98,7 @@ commands: parameters: numpy_version: type: string - default: "" + default: "!=2.1.0" steps: - run: name: Install Python dependencies From 2947cc4f49bd2aebde3ac8e5cc87620cdfbea85e Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Wed, 21 Aug 2024 16:04:02 -0500 Subject: [PATCH 0190/1230] Update missing references for moved lines --- doc/missing-references.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/missing-references.json b/doc/missing-references.json index a93a03b6ef73..87c9ce9b716f 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -356,7 +356,7 @@ "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.gci:4" ], "Line2D.pick": [ - "doc/users/explain/figure/event_handling.rst:568" + "doc/users/explain/figure/event_handling.rst:571" ], "QuadContourSet.changed()": [ "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contour:156", @@ -365,7 +365,7 @@ "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contourf:156" ], "Rectangle.contains": [ - "doc/users/explain/figure/event_handling.rst:280" + "doc/users/explain/figure/event_handling.rst:285" ], "Size.from_any": [ "lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid:87", From 25be2f1a1330eaebe727d1d3c93b11f71d569f73 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 22 Feb 2024 22:03:42 -0500 Subject: [PATCH 0191/1230] BLD: Make ft2font classes final There appears to be no reason for them to be subtyped, as they are semi-private, and we don't do that. --- doc/api/next_api_changes/behavior/27891-ES.rst | 5 +++++ lib/matplotlib/ft2font.pyi | 5 ++++- src/ft2font_wrapper.cpp | 6 +++--- 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/27891-ES.rst diff --git a/doc/api/next_api_changes/behavior/27891-ES.rst b/doc/api/next_api_changes/behavior/27891-ES.rst new file mode 100644 index 000000000000..f60b4b320a44 --- /dev/null +++ b/doc/api/next_api_changes/behavior/27891-ES.rst @@ -0,0 +1,5 @@ +ft2font classes are now final +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ft2font classes `.ft2font.FT2Font`, and `.ft2font.FT2Image` are now final +and can no longer be subclassed. diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 6a0716e993a5..d47614cc6f48 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -1,4 +1,4 @@ -from typing import BinaryIO, Literal, TypedDict, overload +from typing import BinaryIO, Literal, TypedDict, final, overload import numpy as np from numpy.typing import NDArray @@ -158,6 +158,7 @@ class _SfntPcltDict(TypedDict): widthType: int serifStyle: int +@final class FT2Font: ascender: int bbox: tuple[int, int, int, int] @@ -233,11 +234,13 @@ class FT2Font: self, string: str, angle: float = ..., flags: int = ... ) -> NDArray[np.float64]: ... +@final class FT2Image: # TODO: When updating mypy>=1.4, subclass from Buffer. def __init__(self, width: float, height: float) -> None: ... def draw_rect(self, x0: float, y0: float, x1: float, y1: float) -> None: ... def draw_rect_filled(self, x0: float, y0: float, x1: float, y1: float) -> None: ... +@final class Glyph: width: int height: int diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 0fdb0165b462..9e0226455972 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -146,7 +146,7 @@ static PyTypeObject* PyFT2Image_init_type() PyFT2ImageType.tp_name = "matplotlib.ft2font.FT2Image"; PyFT2ImageType.tp_basicsize = sizeof(PyFT2Image); PyFT2ImageType.tp_dealloc = (destructor)PyFT2Image_dealloc; - PyFT2ImageType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; + PyFT2ImageType.tp_flags = Py_TPFLAGS_DEFAULT; PyFT2ImageType.tp_methods = methods; PyFT2ImageType.tp_new = PyFT2Image_new; PyFT2ImageType.tp_init = (initproc)PyFT2Image_init; @@ -236,7 +236,7 @@ static PyTypeObject *PyGlyph_init_type() PyGlyphType.tp_name = "matplotlib.ft2font.Glyph"; PyGlyphType.tp_basicsize = sizeof(PyGlyph); PyGlyphType.tp_dealloc = (destructor)PyGlyph_dealloc; - PyGlyphType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; + PyGlyphType.tp_flags = Py_TPFLAGS_DEFAULT; PyGlyphType.tp_members = members; PyGlyphType.tp_getset = getset; @@ -1495,7 +1495,7 @@ static PyTypeObject *PyFT2Font_init_type() PyFT2FontType.tp_doc = PyFT2Font_init__doc__; PyFT2FontType.tp_basicsize = sizeof(PyFT2Font); PyFT2FontType.tp_dealloc = (destructor)PyFT2Font_dealloc; - PyFT2FontType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; + PyFT2FontType.tp_flags = Py_TPFLAGS_DEFAULT; PyFT2FontType.tp_methods = methods; PyFT2FontType.tp_getset = getset; PyFT2FontType.tp_new = PyFT2Font_new; From 5f2a89ab3a85e90eab41492acd94a78432ad2bf5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 5 Mar 2024 23:51:22 -0500 Subject: [PATCH 0192/1230] Move Python code from ft2font to its wrapper This improves the encapsulation and separation of concerns between the files. --- src/ft2font.cpp | 86 +++++++++++++---------------------------- src/ft2font.h | 10 +++-- src/ft2font_wrapper.cpp | 71 +++++++++++++++++++++++++++------- 3 files changed, 90 insertions(+), 77 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index b20f224715bf..41203340dd47 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -1,18 +1,16 @@ /* -*- mode: c++; c-basic-offset: 4 -*- */ -#define NO_IMPORT_ARRAY - #include +#include #include #include #include #include #include +#include #include "ft2font.h" #include "mplutils.h" -#include "numpy_cpp.h" -#include "py_exceptions.h" #ifndef M_PI #define M_PI 3.14159265358979323846264338328 @@ -185,30 +183,6 @@ FT2Image::draw_rect_filled(unsigned long x0, unsigned long y0, unsigned long x1, m_dirty = true; } -static void ft_glyph_warn(FT_ULong charcode, std::set family_names) -{ - PyObject *text_helpers = NULL, *tmp = NULL; - std::set::iterator it = family_names.begin(); - std::stringstream ss; - ss<<*it; - while(++it != family_names.end()){ - ss<<", "<<*it; - } - - if (!(text_helpers = PyImport_ImportModule("matplotlib._text_helpers")) || - !(tmp = PyObject_CallMethod(text_helpers, - "warn_on_missing_glyph", "(k, s)", - charcode, ss.str().c_str()))) { - goto exit; - } -exit: - Py_XDECREF(text_helpers); - Py_XDECREF(tmp); - if (PyErr_Occurred()) { - throw mpl::exception(); - } -} - // ft_outline_decomposer should be passed to FT_Outline_Decompose. On the // first pass, vertices and codes are set to NULL, and index is simply // incremented for each vertex that should be inserted, so that it is set, at @@ -296,52 +270,41 @@ static FT_Outline_Funcs ft_outline_funcs = { ft_outline_conic_to, ft_outline_cubic_to}; -PyObject* -FT2Font::get_path() +void +FT2Font::get_path(std::vector &vertices, std::vector &codes) { if (!face->glyph) { - PyErr_SetString(PyExc_RuntimeError, "No glyph loaded"); - return NULL; + throw std::runtime_error("No glyph loaded"); } ft_outline_decomposer decomposer = {}; - if (FT_Error error = - FT_Outline_Decompose( - &face->glyph->outline, &ft_outline_funcs, &decomposer)) { - PyErr_Format(PyExc_RuntimeError, - "FT_Outline_Decompose failed with error 0x%x", error); - return NULL; + if (FT_Error error = FT_Outline_Decompose( + &face->glyph->outline, &ft_outline_funcs, &decomposer)) { + throw std::runtime_error("FT_Outline_Decompose failed with error " + + std::to_string(error)); } if (!decomposer.index) { // Don't append CLOSEPOLY to null glyphs. - npy_intp vertices_dims[2] = { 0, 2 }; - numpy::array_view vertices(vertices_dims); - npy_intp codes_dims[1] = { 0 }; - numpy::array_view codes(codes_dims); - return Py_BuildValue("NN", vertices.pyobj(), codes.pyobj()); - } - npy_intp vertices_dims[2] = { decomposer.index + 1, 2 }; - numpy::array_view vertices(vertices_dims); - npy_intp codes_dims[1] = { decomposer.index + 1 }; - numpy::array_view codes(codes_dims); + return; + } + vertices.resize((decomposer.index + 1) * 2); + codes.resize(decomposer.index + 1); decomposer.index = 0; decomposer.vertices = vertices.data(); decomposer.codes = codes.data(); - if (FT_Error error = - FT_Outline_Decompose( - &face->glyph->outline, &ft_outline_funcs, &decomposer)) { - PyErr_Format(PyExc_RuntimeError, - "FT_Outline_Decompose failed with error 0x%x", error); - return NULL; + if (FT_Error error = FT_Outline_Decompose( + &face->glyph->outline, &ft_outline_funcs, &decomposer)) { + throw std::runtime_error("FT_Outline_Decompose failed with error " + + std::to_string(error)); } *(decomposer.vertices++) = 0; *(decomposer.vertices++) = 0; *(decomposer.codes++) = CLOSEPOLY; - return Py_BuildValue("NN", vertices.pyobj(), codes.pyobj()); } FT2Font::FT2Font(FT_Open_Args &open_args, long hinting_factor_, - std::vector &fallback_list) - : image(), face(NULL) + std::vector &fallback_list, + FT2Font::WarnFunc warn) + : ft_glyph_warn(warn), image(), face(NULL) { clear(); @@ -819,7 +782,8 @@ void FT2Font::draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, im.draw_bitmap(&bitmap->bitmap, x + bitmap->left, y); } -void FT2Font::get_glyph_name(unsigned int glyph_number, char *buffer, bool fallback = false) +void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer, + bool fallback = false) { if (fallback && glyph_to_font.find(glyph_number) != glyph_to_font.end()) { // cache is only for parent FT2Font @@ -830,9 +794,11 @@ void FT2Font::get_glyph_name(unsigned int glyph_number, char *buffer, bool fallb if (!FT_HAS_GLYPH_NAMES(face)) { /* Note that this generated name must match the name that is generated by ttconv in ttfont_CharStrings_getname. */ - PyOS_snprintf(buffer, 128, "uni%08x", glyph_number); + buffer.replace(0, 3, "uni"); + std::to_chars(buffer.data() + 3, buffer.data() + buffer.size(), + glyph_number, 16); } else { - if (FT_Error error = FT_Get_Glyph_Name(face, glyph_number, buffer, 128)) { + if (FT_Error error = FT_Get_Glyph_Name(face, glyph_number, buffer.data(), buffer.size())) { throw_ft_error("Could not get glyph names", error); } } diff --git a/src/ft2font.h b/src/ft2font.h index 66b218316e90..0b2db6fe1510 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -71,9 +72,11 @@ extern FT_Library _ft2Library; class FT2Font { + typedef void (*WarnFunc)(FT_ULong charcode, std::set family_names); public: - FT2Font(FT_Open_Args &open_args, long hinting_factor, std::vector &fallback_list); + FT2Font(FT_Open_Args &open_args, long hinting_factor, + std::vector &fallback_list, WarnFunc warn); virtual ~FT2Font(); void clear(); void set_size(double ptsize, double dpi); @@ -106,10 +109,10 @@ class FT2Font void get_xys(bool antialiased, std::vector &xys); void draw_glyphs_to_bitmap(bool antialiased); void draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, bool antialiased); - void get_glyph_name(unsigned int glyph_number, char *buffer, bool fallback); + void get_glyph_name(unsigned int glyph_number, std::string &buffer, bool fallback); long get_name_index(char *name); FT_UInt get_char_index(FT_ULong charcode, bool fallback); - PyObject* get_path(); + void get_path(std::vector &vertices, std::vector &codes); bool get_char_fallback_index(FT_ULong charcode, int& index) const; FT_Face const &get_face() const @@ -143,6 +146,7 @@ class FT2Font } private: + WarnFunc ft_glyph_warn; FT2Image image; FT_Face face; FT_Vector pen; /* untransformed origin */ diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 9e0226455972..3551d82f48e9 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -1,11 +1,13 @@ #include "mplutils.h" #include "ft2font.h" +#include "numpy_cpp.h" #include "py_converters.h" #include "py_exceptions.h" // From Python #include +#include #include static PyObject *convert_xys_to_array(std::vector &xys) @@ -308,6 +310,31 @@ static void close_file_callback(FT_Stream stream) PyErr_Restore(type, value, traceback); } +static void +ft_glyph_warn(FT_ULong charcode, std::set family_names) +{ + PyObject *text_helpers = NULL, *tmp = NULL; + std::set::iterator it = family_names.begin(); + std::stringstream ss; + ss<<*it; + while(++it != family_names.end()){ + ss<<", "<<*it; + } + + if (!(text_helpers = PyImport_ImportModule("matplotlib._text_helpers")) || + !(tmp = PyObject_CallMethod(text_helpers, + "warn_on_missing_glyph", "(k, s)", + charcode, ss.str().c_str()))) { + goto exit; + } +exit: + Py_XDECREF(text_helpers); + Py_XDECREF(tmp); + if (PyErr_Occurred()) { + throw mpl::exception(); + } +} + static PyObject *PyFT2Font_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyFT2Font *self; @@ -455,7 +482,8 @@ static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds) Py_CLEAR(data); CALL_CPP_FULL( - "FT2Font", (self->x = new FT2Font(open_args, hinting_factor, fallback_fonts)), + "FT2Font", + (self->x = new FT2Font(open_args, hinting_factor, fallback_fonts, ft_glyph_warn)), Py_CLEAR(self->py_file), -1); CALL_CPP_INIT("FT2Font->set_kerning_factor", (self->x->set_kerning_factor(kerning_factor))); @@ -555,13 +583,13 @@ static PyObject *PyFT2Font_get_kerning(PyFT2Font *self, PyObject *args) { FT_UInt left, right, mode; int result; - int fallback = 1; + bool fallback = true; if (!PyArg_ParseTuple(args, "III:get_kerning", &left, &right, &mode)) { return NULL; } - CALL_CPP("get_kerning", (result = self->x->get_kerning(left, right, mode, (bool)fallback))); + CALL_CPP("get_kerning", (result = self->x->get_kerning(left, right, mode, fallback))); return PyLong_FromLong(result); } @@ -704,7 +732,7 @@ const char *PyFT2Font_load_char__doc__ = static PyObject *PyFT2Font_load_char(PyFT2Font *self, PyObject *args, PyObject *kwds) { long charcode; - int fallback = 1; + bool fallback = true; FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; const char *names[] = { "charcode", "flags", NULL }; @@ -717,7 +745,7 @@ static PyObject *PyFT2Font_load_char(PyFT2Font *self, PyObject *args, PyObject * } FT2Font *ft_object = NULL; - CALL_CPP("load_char", (self->x->load_char(charcode, flags, ft_object, (bool)fallback))); + CALL_CPP("load_char", (self->x->load_char(charcode, flags, ft_object, fallback))); return PyGlyph_from_FT2Font(ft_object); } @@ -743,7 +771,7 @@ static PyObject *PyFT2Font_load_glyph(PyFT2Font *self, PyObject *args, PyObject { FT_UInt glyph_index; FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; - int fallback = 1; + bool fallback = true; const char *names[] = { "glyph_index", "flags", NULL }; /* This makes a technically incorrect assumption that FT_Int32 is @@ -755,7 +783,7 @@ static PyObject *PyFT2Font_load_glyph(PyFT2Font *self, PyObject *args, PyObject } FT2Font *ft_object = NULL; - CALL_CPP("load_glyph", (self->x->load_glyph(glyph_index, flags, ft_object, (bool)fallback))); + CALL_CPP("load_glyph", (self->x->load_glyph(glyph_index, flags, ft_object, fallback))); return PyGlyph_from_FT2Font(ft_object); } @@ -912,14 +940,16 @@ const char *PyFT2Font_get_glyph_name__doc__ = static PyObject *PyFT2Font_get_glyph_name(PyFT2Font *self, PyObject *args) { unsigned int glyph_number; - char buffer[128]; - int fallback = 1; + std::string buffer; + bool fallback = true; if (!PyArg_ParseTuple(args, "I:get_glyph_name", &glyph_number)) { return NULL; } - CALL_CPP("get_glyph_name", (self->x->get_glyph_name(glyph_number, buffer, (bool)fallback))); - return PyUnicode_FromString(buffer); + buffer.resize(128); + CALL_CPP("get_glyph_name", + (self->x->get_glyph_name(glyph_number, buffer, fallback))); + return PyUnicode_FromString(buffer.c_str()); } const char *PyFT2Font_get_charmap__doc__ = @@ -962,13 +992,13 @@ static PyObject *PyFT2Font_get_char_index(PyFT2Font *self, PyObject *args) { FT_UInt index; FT_ULong ccode; - int fallback = 1; + bool fallback = true; if (!PyArg_ParseTuple(args, "k:get_char_index", &ccode)) { return NULL; } - CALL_CPP("get_char_index", index = self->x->get_char_index(ccode, (bool)fallback)); + CALL_CPP("get_char_index", index = self->x->get_char_index(ccode, fallback)); return PyLong_FromLong(index); } @@ -1270,7 +1300,20 @@ const char *PyFT2Font_get_path__doc__ = static PyObject *PyFT2Font_get_path(PyFT2Font *self, PyObject *args) { - CALL_CPP("get_path", return self->x->get_path()); + std::vector vertices; + std::vector codes; + + CALL_CPP("get_path", self->x->get_path(vertices, codes)); + + npy_intp length = codes.size(); + npy_intp vertices_dims[2] = { length, 2 }; + numpy::array_view vertices_arr(vertices_dims); + memcpy(vertices_arr.data(), vertices.data(), sizeof(double) * vertices.size()); + npy_intp codes_dims[1] = { length }; + numpy::array_view codes_arr(codes_dims); + memcpy(codes_arr.data(), codes.data(), codes.size()); + + return Py_BuildValue("NN", vertices_arr.pyobj(), codes_arr.pyobj()); } const char *PyFT2Font_get_image__doc__ = From 9765ea168c8efec9175a1cd9fa0226c5f990389b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 6 Mar 2024 04:12:59 -0500 Subject: [PATCH 0193/1230] Use std::vector directly with FT_Outline_Decompose This means we only need to do one pass through. --- src/ft2font.cpp | 108 ++++++++++++++++++++---------------------------- 1 file changed, 45 insertions(+), 63 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 41203340dd47..f2dc2adf91f5 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -183,35 +183,26 @@ FT2Image::draw_rect_filled(unsigned long x0, unsigned long y0, unsigned long x1, m_dirty = true; } -// ft_outline_decomposer should be passed to FT_Outline_Decompose. On the -// first pass, vertices and codes are set to NULL, and index is simply -// incremented for each vertex that should be inserted, so that it is set, at -// the end, to the total number of vertices. On a second pass, vertices and -// codes should point to correctly sized arrays, and index set again to zero, -// to get fill vertices and codes with the outline decomposition. +// ft_outline_decomposer should be passed to FT_Outline_Decompose. struct ft_outline_decomposer { - int index; - double* vertices; - unsigned char* codes; + std::vector &vertices; + std::vector &codes; }; static int ft_outline_move_to(FT_Vector const* to, void* user) { ft_outline_decomposer* d = reinterpret_cast(user); - if (d->codes) { - if (d->index) { - // Appending CLOSEPOLY is important to make patheffects work. - *(d->vertices++) = 0; - *(d->vertices++) = 0; - *(d->codes++) = CLOSEPOLY; - } - *(d->vertices++) = to->x * (1. / 64.); - *(d->vertices++) = to->y * (1. / 64.); - *(d->codes++) = MOVETO; - } - d->index += d->index ? 2 : 1; + if (!d->vertices.empty()) { + // Appending CLOSEPOLY is important to make patheffects work. + d->vertices.push_back(0); + d->vertices.push_back(0); + d->codes.push_back(CLOSEPOLY); + } + d->vertices.push_back(to->x * (1. / 64.)); + d->vertices.push_back(to->y * (1. / 64.)); + d->codes.push_back(MOVETO); return 0; } @@ -219,12 +210,9 @@ static int ft_outline_line_to(FT_Vector const* to, void* user) { ft_outline_decomposer* d = reinterpret_cast(user); - if (d->codes) { - *(d->vertices++) = to->x * (1. / 64.); - *(d->vertices++) = to->y * (1. / 64.); - *(d->codes++) = LINETO; - } - d->index++; + d->vertices.push_back(to->x * (1. / 64.)); + d->vertices.push_back(to->y * (1. / 64.)); + d->codes.push_back(LINETO); return 0; } @@ -232,15 +220,12 @@ static int ft_outline_conic_to(FT_Vector const* control, FT_Vector const* to, void* user) { ft_outline_decomposer* d = reinterpret_cast(user); - if (d->codes) { - *(d->vertices++) = control->x * (1. / 64.); - *(d->vertices++) = control->y * (1. / 64.); - *(d->vertices++) = to->x * (1. / 64.); - *(d->vertices++) = to->y * (1. / 64.); - *(d->codes++) = CURVE3; - *(d->codes++) = CURVE3; - } - d->index += 2; + d->vertices.push_back(control->x * (1. / 64.)); + d->vertices.push_back(control->y * (1. / 64.)); + d->vertices.push_back(to->x * (1. / 64.)); + d->vertices.push_back(to->y * (1. / 64.)); + d->codes.push_back(CURVE3); + d->codes.push_back(CURVE3); return 0; } @@ -249,18 +234,15 @@ ft_outline_cubic_to( FT_Vector const* c1, FT_Vector const* c2, FT_Vector const* to, void* user) { ft_outline_decomposer* d = reinterpret_cast(user); - if (d->codes) { - *(d->vertices++) = c1->x * (1. / 64.); - *(d->vertices++) = c1->y * (1. / 64.); - *(d->vertices++) = c2->x * (1. / 64.); - *(d->vertices++) = c2->y * (1. / 64.); - *(d->vertices++) = to->x * (1. / 64.); - *(d->vertices++) = to->y * (1. / 64.); - *(d->codes++) = CURVE4; - *(d->codes++) = CURVE4; - *(d->codes++) = CURVE4; - } - d->index += 3; + d->vertices.push_back(c1->x * (1. / 64.)); + d->vertices.push_back(c1->y * (1. / 64.)); + d->vertices.push_back(c2->x * (1. / 64.)); + d->vertices.push_back(c2->y * (1. / 64.)); + d->vertices.push_back(to->x * (1. / 64.)); + d->vertices.push_back(to->y * (1. / 64.)); + d->codes.push_back(CURVE4); + d->codes.push_back(CURVE4); + d->codes.push_back(CURVE4); return 0; } @@ -276,28 +258,28 @@ FT2Font::get_path(std::vector &vertices, std::vector &cod if (!face->glyph) { throw std::runtime_error("No glyph loaded"); } - ft_outline_decomposer decomposer = {}; + ft_outline_decomposer decomposer = { + vertices, + codes, + }; + // We can make a close-enough estimate based on number of points and number of + // contours (which produce a MOVETO each), though it's slightly underestimating due + // to higher-order curves. + size_t estimated_points = static_cast(face->glyph->outline.n_contours) + + static_cast(face->glyph->outline.n_points); + vertices.reserve(2 * estimated_points); + codes.reserve(estimated_points); if (FT_Error error = FT_Outline_Decompose( &face->glyph->outline, &ft_outline_funcs, &decomposer)) { throw std::runtime_error("FT_Outline_Decompose failed with error " + std::to_string(error)); } - if (!decomposer.index) { // Don't append CLOSEPOLY to null glyphs. + if (vertices.empty()) { // Don't append CLOSEPOLY to null glyphs. return; } - vertices.resize((decomposer.index + 1) * 2); - codes.resize(decomposer.index + 1); - decomposer.index = 0; - decomposer.vertices = vertices.data(); - decomposer.codes = codes.data(); - if (FT_Error error = FT_Outline_Decompose( - &face->glyph->outline, &ft_outline_funcs, &decomposer)) { - throw std::runtime_error("FT_Outline_Decompose failed with error " + - std::to_string(error)); - } - *(decomposer.vertices++) = 0; - *(decomposer.vertices++) = 0; - *(decomposer.codes++) = CLOSEPOLY; + vertices.push_back(0); + vertices.push_back(0); + codes.push_back(CLOSEPOLY); } FT2Font::FT2Font(FT_Open_Args &open_args, From d3da65f5d10e05c8cfd7a3cccfdfc8df115fc7b2 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 14 Aug 2024 19:35:23 -0400 Subject: [PATCH 0194/1230] Remove deprecated ft2font API --- .../next_api_changes/removals/27891-ES.rst | 4 ++ lib/matplotlib/ft2font.pyi | 2 - src/ft2font.cpp | 44 ------------- src/ft2font.h | 4 -- src/ft2font_wrapper.cpp | 62 ------------------- 5 files changed, 4 insertions(+), 112 deletions(-) create mode 100644 doc/api/next_api_changes/removals/27891-ES.rst diff --git a/doc/api/next_api_changes/removals/27891-ES.rst b/doc/api/next_api_changes/removals/27891-ES.rst new file mode 100644 index 000000000000..cb658e9bc671 --- /dev/null +++ b/doc/api/next_api_changes/removals/27891-ES.rst @@ -0,0 +1,4 @@ +``ft2font.FT2Image.draw_rect`` and ``ft2font.FT2Font.get_xys`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... have been removed as they are unused. diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index d47614cc6f48..0a27411ff39c 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -224,7 +224,6 @@ class FT2Font: @overload def get_sfnt_table(self, name: Literal["pclt"]) -> _SfntPcltDict | None: ... def get_width_height(self) -> tuple[int, int]: ... - def get_xys(self, antialiased: bool = ...) -> NDArray[np.float64]: ... def load_char(self, charcode: int, flags: int = ...) -> Glyph: ... def load_glyph(self, glyphindex: int, flags: int = ...) -> Glyph: ... def select_charmap(self, i: int) -> None: ... @@ -237,7 +236,6 @@ class FT2Font: @final class FT2Image: # TODO: When updating mypy>=1.4, subclass from Buffer. def __init__(self, width: float, height: float) -> None: ... - def draw_rect(self, x0: float, y0: float, x1: float, y1: float) -> None: ... def draw_rect_filled(self, x0: float, y0: float, x1: float, y1: float) -> None: ... @final diff --git a/src/ft2font.cpp b/src/ft2font.cpp index f2dc2adf91f5..d78d696e118f 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -145,27 +145,6 @@ void FT2Image::draw_bitmap(FT_Bitmap *bitmap, FT_Int x, FT_Int y) m_dirty = true; } -void FT2Image::draw_rect(unsigned long x0, unsigned long y0, unsigned long x1, unsigned long y1) -{ - if (x0 > m_width || x1 > m_width || y0 > m_height || y1 > m_height) { - throw std::runtime_error("Rect coords outside image bounds"); - } - - size_t top = y0 * m_width; - size_t bottom = y1 * m_width; - for (size_t i = x0; i < x1 + 1; ++i) { - m_buffer[i + top] = 255; - m_buffer[i + bottom] = 255; - } - - for (size_t j = y0 + 1; j < y1; ++j) { - m_buffer[x0 + j * m_width] = 255; - m_buffer[x1 + j * m_width] = 255; - } - - m_dirty = true; -} - void FT2Image::draw_rect_filled(unsigned long x0, unsigned long y0, unsigned long x1, unsigned long y1) { @@ -716,29 +695,6 @@ void FT2Font::draw_glyphs_to_bitmap(bool antialiased) } } -void FT2Font::get_xys(bool antialiased, std::vector &xys) -{ - for (size_t n = 0; n < glyphs.size(); n++) { - - FT_Error error = FT_Glyph_To_Bitmap( - &glyphs[n], antialiased ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO, 0, 1); - if (error) { - throw_ft_error("Could not convert glyph to bitmap", error); - } - - FT_BitmapGlyph bitmap = (FT_BitmapGlyph)glyphs[n]; - - // bitmap left and top in pixel, string bbox in subpixel - FT_Int x = (FT_Int)(bitmap->left - bbox.xMin * (1. / 64.)); - FT_Int y = (FT_Int)(bbox.yMax * (1. / 64.) - bitmap->top + 1); - // make sure the index is non-neg - x = x < 0 ? 0 : x; - y = y < 0 ? 0 : y; - xys.push_back(x); - xys.push_back(y); - } -} - void FT2Font::draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, bool antialiased) { FT_Vector sub_offset; diff --git a/src/ft2font.h b/src/ft2font.h index 0b2db6fe1510..4bd924497978 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -41,7 +41,6 @@ class FT2Image void resize(long width, long height); void draw_bitmap(FT_Bitmap *bitmap, FT_Int x, FT_Int y); - void draw_rect(unsigned long x0, unsigned long y0, unsigned long x1, unsigned long y1); void draw_rect_filled(unsigned long x0, unsigned long y0, unsigned long x1, unsigned long y1); unsigned char *get_buffer() @@ -104,9 +103,6 @@ class FT2Font void get_width_height(long *width, long *height); void get_bitmap_offset(long *x, long *y); long get_descent(); - // TODO: Since we know the size of the array upfront, we probably don't - // need to dynamically allocate like this - void get_xys(bool antialiased, std::vector &xys); void draw_glyphs_to_bitmap(bool antialiased); void draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, bool antialiased); void get_glyph_name(unsigned int glyph_number, std::string &buffer, bool fallback); diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 3551d82f48e9..6d6e8722b63b 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -63,35 +63,6 @@ static void PyFT2Image_dealloc(PyFT2Image *self) Py_TYPE(self)->tp_free((PyObject *)self); } -const char *PyFT2Image_draw_rect__doc__ = - "draw_rect(self, x0, y0, x1, y1)\n" - "--\n\n" - "Draw an empty rectangle to the image.\n" - "\n" - ".. deprecated:: 3.8\n"; -; - -static PyObject *PyFT2Image_draw_rect(PyFT2Image *self, PyObject *args) -{ - char const* msg = - "FT2Image.draw_rect is deprecated since Matplotlib 3.8 and will be removed " - "in Matplotlib 3.10 as it is not used in the library. If you rely on it, " - "please let us know."; - if (PyErr_WarnEx(PyExc_DeprecationWarning, msg, 1)) { - return NULL; - } - - double x0, y0, x1, y1; - - if (!PyArg_ParseTuple(args, "dddd:draw_rect", &x0, &y0, &x1, &y1)) { - return NULL; - } - - CALL_CPP("draw_rect", (self->x->draw_rect(x0, y0, x1, y1))); - - Py_RETURN_NONE; -} - const char *PyFT2Image_draw_rect_filled__doc__ = "draw_rect_filled(self, x0, y0, x1, y1)\n" "--\n\n" @@ -137,7 +108,6 @@ static int PyFT2Image_get_buffer(PyFT2Image *self, Py_buffer *buf, int flags) static PyTypeObject* PyFT2Image_init_type() { static PyMethodDef methods[] = { - {"draw_rect", (PyCFunction)PyFT2Image_draw_rect, METH_VARARGS, PyFT2Image_draw_rect__doc__}, {"draw_rect_filled", (PyCFunction)PyFT2Image_draw_rect_filled, METH_VARARGS, PyFT2Image_draw_rect_filled__doc__}, {NULL} }; @@ -856,37 +826,6 @@ static PyObject *PyFT2Font_draw_glyphs_to_bitmap(PyFT2Font *self, PyObject *args Py_RETURN_NONE; } -const char *PyFT2Font_get_xys__doc__ = - "get_xys(self, antialiased=True)\n" - "--\n\n" - "Get the xy locations of the current glyphs.\n" - "\n" - ".. deprecated:: 3.8\n"; - -static PyObject *PyFT2Font_get_xys(PyFT2Font *self, PyObject *args, PyObject *kwds) -{ - char const* msg = - "FT2Font.get_xys is deprecated since Matplotlib 3.8 and will be removed in " - "Matplotlib 3.10 as it is not used in the library. If you rely on it, " - "please let us know."; - if (PyErr_WarnEx(PyExc_DeprecationWarning, msg, 1)) { - return NULL; - } - - bool antialiased = true; - std::vector xys; - const char *names[] = { "antialiased", NULL }; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O&:get_xys", - (char **)names, &convert_bool, &antialiased)) { - return NULL; - } - - CALL_CPP("get_xys", (self->x->get_xys(antialiased, xys))); - - return convert_xys_to_array(xys); -} - const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = "draw_glyph_to_bitmap(self, image, x, y, glyph, antialiased=True)\n" "--\n\n" @@ -1517,7 +1456,6 @@ static PyTypeObject *PyFT2Font_init_type() {"get_bitmap_offset", (PyCFunction)PyFT2Font_get_bitmap_offset, METH_NOARGS, PyFT2Font_get_bitmap_offset__doc__}, {"get_descent", (PyCFunction)PyFT2Font_get_descent, METH_NOARGS, PyFT2Font_get_descent__doc__}, {"draw_glyphs_to_bitmap", (PyCFunction)PyFT2Font_draw_glyphs_to_bitmap, METH_VARARGS|METH_KEYWORDS, PyFT2Font_draw_glyphs_to_bitmap__doc__}, - {"get_xys", (PyCFunction)PyFT2Font_get_xys, METH_VARARGS|METH_KEYWORDS, PyFT2Font_get_xys__doc__}, {"draw_glyph_to_bitmap", (PyCFunction)PyFT2Font_draw_glyph_to_bitmap, METH_VARARGS|METH_KEYWORDS, PyFT2Font_draw_glyph_to_bitmap__doc__}, {"get_glyph_name", (PyCFunction)PyFT2Font_get_glyph_name, METH_VARARGS, PyFT2Font_get_glyph_name__doc__}, {"get_charmap", (PyCFunction)PyFT2Font_get_charmap, METH_NOARGS, PyFT2Font_get_charmap__doc__}, From 276fade9e375c3598a35f373e07f778c95baa066 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 22 Aug 2024 21:29:39 -0400 Subject: [PATCH 0195/1230] Remove unused FT2Image.m_dirty It is set in a few places, but never read. --- src/ft2font.cpp | 10 ++-------- src/ft2font.h | 1 - 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index d78d696e118f..cb9952f3b374 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -63,12 +63,12 @@ void throw_ft_error(std::string message, FT_Error error) { throw std::runtime_error(os.str()); } -FT2Image::FT2Image() : m_dirty(true), m_buffer(NULL), m_width(0), m_height(0) +FT2Image::FT2Image() : m_buffer(NULL), m_width(0), m_height(0) { } FT2Image::FT2Image(unsigned long width, unsigned long height) - : m_dirty(true), m_buffer(NULL), m_width(0), m_height(0) + : m_buffer(NULL), m_width(0), m_height(0) { resize(width, height); } @@ -102,8 +102,6 @@ void FT2Image::resize(long width, long height) if (numBytes && m_buffer) { memset(m_buffer, 0, numBytes); } - - m_dirty = true; } void FT2Image::draw_bitmap(FT_Bitmap *bitmap, FT_Int x, FT_Int y) @@ -141,8 +139,6 @@ void FT2Image::draw_bitmap(FT_Bitmap *bitmap, FT_Int x, FT_Int y) } else { throw std::runtime_error("Unknown pixel mode"); } - - m_dirty = true; } void @@ -158,8 +154,6 @@ FT2Image::draw_rect_filled(unsigned long x0, unsigned long y0, unsigned long x1, m_buffer[i + j * m_width] = 255; } } - - m_dirty = true; } // ft_outline_decomposer should be passed to FT_Outline_Decompose. diff --git a/src/ft2font.h b/src/ft2font.h index 4bd924497978..2f24bfb01f79 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -57,7 +57,6 @@ class FT2Image } private: - bool m_dirty; unsigned char *m_buffer; unsigned long m_width; unsigned long m_height; From b01462c9ac7b0d3875892d6f40c9213c56f0864f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Fri, 23 Aug 2024 20:31:22 +0200 Subject: [PATCH 0196/1230] MultivarColormap and BivarColormap (#28454) MultivarColormap and BivarColormap Creation and tests for the classes MultivarColormap and BivarColormap. Co-authored-by: Elliott Sales de Andrade --- lib/matplotlib/__init__.py | 4 + lib/matplotlib/__init__.pyi | 2 + lib/matplotlib/_cm_bivar.py | 1312 +++++++++++++++++ lib/matplotlib/_cm_multivar.py | 166 +++ lib/matplotlib/cm.py | 6 + lib/matplotlib/cm.pyi | 2 + lib/matplotlib/colors.py | 898 ++++++++++- lib/matplotlib/colors.pyi | 95 ++ lib/matplotlib/meson.build | 2 + .../bivariate_cmap_shapes.png | Bin 0 -> 5157 bytes .../multivar_alpha_mixing.png | Bin 0 -> 4917 bytes lib/matplotlib/tests/meson.build | 1 + .../tests/test_multivariate_colormaps.py | 564 +++++++ 13 files changed, 3047 insertions(+), 5 deletions(-) create mode 100644 lib/matplotlib/_cm_bivar.py create mode 100644 lib/matplotlib/_cm_multivar.py create mode 100644 lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/bivariate_cmap_shapes.png create mode 100644 lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/multivar_alpha_mixing.png create mode 100644 lib/matplotlib/tests/test_multivariate_colormaps.py diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index dc8c004a598b..13bfa81d9ffa 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -129,6 +129,8 @@ "interactive", "is_interactive", "colormaps", + "multivar_colormaps", + "bivar_colormaps", "color_sequences", ] @@ -1543,4 +1545,6 @@ def validate_backend(s): # workaround: we must defer colormaps import to after loading rcParams, because # colormap creation depends on rcParams from matplotlib.cm import _colormaps as colormaps # noqa: E402 +from matplotlib.cm import _multivar_colormaps as multivar_colormaps # noqa: E402 +from matplotlib.cm import _bivar_colormaps as bivar_colormaps # noqa: E402 from matplotlib.colors import _color_sequences as color_sequences # noqa: E402 diff --git a/lib/matplotlib/__init__.pyi b/lib/matplotlib/__init__.pyi index 05dc927dc6c9..5b6797d3a7da 100644 --- a/lib/matplotlib/__init__.pyi +++ b/lib/matplotlib/__init__.pyi @@ -116,4 +116,6 @@ def _preprocess_data( ) -> Callable: ... from matplotlib.cm import _colormaps as colormaps # noqa: E402 +from matplotlib.cm import _multivar_colormaps as multivar_colormaps # noqa: E402 +from matplotlib.cm import _bivar_colormaps as bivar_colormaps # noqa: E402 from matplotlib.colors import _color_sequences as color_sequences # noqa: E402 diff --git a/lib/matplotlib/_cm_bivar.py b/lib/matplotlib/_cm_bivar.py new file mode 100644 index 000000000000..53c0d48d7d6c --- /dev/null +++ b/lib/matplotlib/_cm_bivar.py @@ -0,0 +1,1312 @@ +# auto-generated by https://github.com/trygvrad/multivariate_colormaps +# date: 2024-05-24 + +import numpy as np +from matplotlib.colors import SegmentedBivarColormap + +BiPeak = np.array( + [0.000, 0.674, 0.931, 0.000, 0.680, 0.922, 0.000, 0.685, 0.914, 0.000, + 0.691, 0.906, 0.000, 0.696, 0.898, 0.000, 0.701, 0.890, 0.000, 0.706, + 0.882, 0.000, 0.711, 0.875, 0.000, 0.715, 0.867, 0.000, 0.720, 0.860, + 0.000, 0.725, 0.853, 0.000, 0.729, 0.845, 0.000, 0.733, 0.838, 0.000, + 0.737, 0.831, 0.000, 0.741, 0.824, 0.000, 0.745, 0.816, 0.000, 0.749, + 0.809, 0.000, 0.752, 0.802, 0.000, 0.756, 0.794, 0.000, 0.759, 0.787, + 0.000, 0.762, 0.779, 0.000, 0.765, 0.771, 0.000, 0.767, 0.764, 0.000, + 0.770, 0.755, 0.000, 0.772, 0.747, 0.000, 0.774, 0.739, 0.000, 0.776, + 0.730, 0.000, 0.777, 0.721, 0.000, 0.779, 0.712, 0.021, 0.780, 0.702, + 0.055, 0.781, 0.693, 0.079, 0.782, 0.682, 0.097, 0.782, 0.672, 0.111, + 0.782, 0.661, 0.122, 0.782, 0.650, 0.132, 0.782, 0.639, 0.140, 0.781, + 0.627, 0.147, 0.781, 0.615, 0.154, 0.780, 0.602, 0.159, 0.778, 0.589, + 0.164, 0.777, 0.576, 0.169, 0.775, 0.563, 0.173, 0.773, 0.549, 0.177, + 0.771, 0.535, 0.180, 0.768, 0.520, 0.184, 0.766, 0.505, 0.187, 0.763, + 0.490, 0.190, 0.760, 0.474, 0.193, 0.756, 0.458, 0.196, 0.753, 0.442, + 0.200, 0.749, 0.425, 0.203, 0.745, 0.408, 0.206, 0.741, 0.391, 0.210, + 0.736, 0.373, 0.213, 0.732, 0.355, 0.216, 0.727, 0.337, 0.220, 0.722, + 0.318, 0.224, 0.717, 0.298, 0.227, 0.712, 0.278, 0.231, 0.707, 0.258, + 0.235, 0.701, 0.236, 0.239, 0.696, 0.214, 0.242, 0.690, 0.190, 0.246, + 0.684, 0.165, 0.250, 0.678, 0.136, 0.000, 0.675, 0.934, 0.000, 0.681, + 0.925, 0.000, 0.687, 0.917, 0.000, 0.692, 0.909, 0.000, 0.697, 0.901, + 0.000, 0.703, 0.894, 0.000, 0.708, 0.886, 0.000, 0.713, 0.879, 0.000, + 0.718, 0.872, 0.000, 0.722, 0.864, 0.000, 0.727, 0.857, 0.000, 0.731, + 0.850, 0.000, 0.736, 0.843, 0.000, 0.740, 0.836, 0.000, 0.744, 0.829, + 0.000, 0.748, 0.822, 0.000, 0.752, 0.815, 0.000, 0.755, 0.808, 0.000, + 0.759, 0.800, 0.000, 0.762, 0.793, 0.000, 0.765, 0.786, 0.000, 0.768, + 0.778, 0.000, 0.771, 0.770, 0.000, 0.773, 0.762, 0.051, 0.776, 0.754, + 0.087, 0.778, 0.746, 0.111, 0.780, 0.737, 0.131, 0.782, 0.728, 0.146, + 0.783, 0.719, 0.159, 0.784, 0.710, 0.171, 0.785, 0.700, 0.180, 0.786, + 0.690, 0.189, 0.786, 0.680, 0.196, 0.787, 0.669, 0.202, 0.787, 0.658, + 0.208, 0.786, 0.647, 0.213, 0.786, 0.635, 0.217, 0.785, 0.623, 0.221, + 0.784, 0.610, 0.224, 0.782, 0.597, 0.227, 0.781, 0.584, 0.230, 0.779, + 0.570, 0.232, 0.777, 0.556, 0.234, 0.775, 0.542, 0.236, 0.772, 0.527, + 0.238, 0.769, 0.512, 0.240, 0.766, 0.497, 0.242, 0.763, 0.481, 0.244, + 0.760, 0.465, 0.246, 0.756, 0.448, 0.248, 0.752, 0.432, 0.250, 0.748, + 0.415, 0.252, 0.744, 0.397, 0.254, 0.739, 0.379, 0.256, 0.735, 0.361, + 0.259, 0.730, 0.343, 0.261, 0.725, 0.324, 0.264, 0.720, 0.304, 0.266, + 0.715, 0.284, 0.269, 0.709, 0.263, 0.271, 0.704, 0.242, 0.274, 0.698, + 0.220, 0.277, 0.692, 0.196, 0.280, 0.686, 0.170, 0.283, 0.680, 0.143, + 0.000, 0.676, 0.937, 0.000, 0.682, 0.928, 0.000, 0.688, 0.920, 0.000, + 0.694, 0.913, 0.000, 0.699, 0.905, 0.000, 0.704, 0.897, 0.000, 0.710, + 0.890, 0.000, 0.715, 0.883, 0.000, 0.720, 0.876, 0.000, 0.724, 0.869, + 0.000, 0.729, 0.862, 0.000, 0.734, 0.855, 0.000, 0.738, 0.848, 0.000, + 0.743, 0.841, 0.000, 0.747, 0.834, 0.000, 0.751, 0.827, 0.000, 0.755, + 0.820, 0.000, 0.759, 0.813, 0.000, 0.762, 0.806, 0.003, 0.766, 0.799, + 0.066, 0.769, 0.792, 0.104, 0.772, 0.784, 0.131, 0.775, 0.777, 0.152, + 0.777, 0.769, 0.170, 0.780, 0.761, 0.185, 0.782, 0.753, 0.198, 0.784, + 0.744, 0.209, 0.786, 0.736, 0.219, 0.787, 0.727, 0.228, 0.788, 0.717, + 0.236, 0.789, 0.708, 0.243, 0.790, 0.698, 0.249, 0.791, 0.688, 0.254, + 0.791, 0.677, 0.259, 0.791, 0.666, 0.263, 0.791, 0.654, 0.266, 0.790, + 0.643, 0.269, 0.789, 0.631, 0.272, 0.788, 0.618, 0.274, 0.787, 0.605, + 0.276, 0.785, 0.592, 0.278, 0.783, 0.578, 0.279, 0.781, 0.564, 0.280, + 0.779, 0.549, 0.282, 0.776, 0.535, 0.283, 0.773, 0.519, 0.284, 0.770, + 0.504, 0.285, 0.767, 0.488, 0.286, 0.763, 0.472, 0.287, 0.759, 0.455, + 0.288, 0.756, 0.438, 0.289, 0.751, 0.421, 0.291, 0.747, 0.403, 0.292, + 0.742, 0.385, 0.293, 0.738, 0.367, 0.295, 0.733, 0.348, 0.296, 0.728, + 0.329, 0.298, 0.723, 0.310, 0.300, 0.717, 0.290, 0.302, 0.712, 0.269, + 0.304, 0.706, 0.247, 0.306, 0.700, 0.225, 0.308, 0.694, 0.201, 0.310, + 0.688, 0.176, 0.312, 0.682, 0.149, 0.000, 0.678, 0.939, 0.000, 0.683, + 0.931, 0.000, 0.689, 0.923, 0.000, 0.695, 0.916, 0.000, 0.701, 0.908, + 0.000, 0.706, 0.901, 0.000, 0.711, 0.894, 0.000, 0.717, 0.887, 0.000, + 0.722, 0.880, 0.000, 0.727, 0.873, 0.000, 0.732, 0.866, 0.000, 0.736, + 0.859, 0.000, 0.741, 0.853, 0.000, 0.745, 0.846, 0.000, 0.750, 0.839, + 0.000, 0.754, 0.833, 0.035, 0.758, 0.826, 0.091, 0.762, 0.819, 0.126, + 0.765, 0.812, 0.153, 0.769, 0.805, 0.174, 0.772, 0.798, 0.193, 0.775, + 0.791, 0.209, 0.778, 0.783, 0.223, 0.781, 0.776, 0.236, 0.784, 0.768, + 0.247, 0.786, 0.760, 0.257, 0.788, 0.752, 0.266, 0.790, 0.743, 0.273, + 0.791, 0.734, 0.280, 0.793, 0.725, 0.287, 0.794, 0.715, 0.292, 0.794, + 0.706, 0.297, 0.795, 0.695, 0.301, 0.795, 0.685, 0.305, 0.795, 0.674, + 0.308, 0.795, 0.662, 0.310, 0.794, 0.651, 0.312, 0.794, 0.638, 0.314, + 0.792, 0.626, 0.316, 0.791, 0.613, 0.317, 0.789, 0.599, 0.318, 0.787, + 0.586, 0.319, 0.785, 0.571, 0.320, 0.783, 0.557, 0.320, 0.780, 0.542, + 0.321, 0.777, 0.527, 0.321, 0.774, 0.511, 0.322, 0.770, 0.495, 0.322, + 0.767, 0.478, 0.323, 0.763, 0.462, 0.323, 0.759, 0.445, 0.324, 0.755, + 0.427, 0.325, 0.750, 0.410, 0.325, 0.745, 0.391, 0.326, 0.741, 0.373, + 0.327, 0.736, 0.354, 0.328, 0.730, 0.335, 0.329, 0.725, 0.315, 0.330, + 0.720, 0.295, 0.331, 0.714, 0.274, 0.333, 0.708, 0.253, 0.334, 0.702, + 0.230, 0.336, 0.696, 0.207, 0.337, 0.690, 0.182, 0.339, 0.684, 0.154, + 0.000, 0.679, 0.942, 0.000, 0.685, 0.934, 0.000, 0.691, 0.927, 0.000, + 0.696, 0.919, 0.000, 0.702, 0.912, 0.000, 0.708, 0.905, 0.000, 0.713, + 0.898, 0.000, 0.718, 0.891, 0.000, 0.724, 0.884, 0.000, 0.729, 0.877, + 0.000, 0.734, 0.871, 0.000, 0.739, 0.864, 0.000, 0.743, 0.857, 0.035, + 0.748, 0.851, 0.096, 0.752, 0.844, 0.133, 0.757, 0.838, 0.161, 0.761, + 0.831, 0.185, 0.765, 0.825, 0.205, 0.769, 0.818, 0.223, 0.772, 0.811, + 0.238, 0.776, 0.804, 0.252, 0.779, 0.797, 0.265, 0.782, 0.790, 0.276, + 0.785, 0.783, 0.286, 0.788, 0.775, 0.296, 0.790, 0.767, 0.304, 0.792, + 0.759, 0.311, 0.794, 0.751, 0.318, 0.796, 0.742, 0.324, 0.797, 0.733, + 0.329, 0.798, 0.723, 0.334, 0.799, 0.714, 0.338, 0.799, 0.703, 0.341, + 0.800, 0.693, 0.344, 0.800, 0.682, 0.347, 0.799, 0.670, 0.349, 0.799, + 0.659, 0.351, 0.798, 0.646, 0.352, 0.797, 0.634, 0.353, 0.795, 0.621, + 0.354, 0.794, 0.607, 0.354, 0.792, 0.593, 0.355, 0.789, 0.579, 0.355, + 0.787, 0.564, 0.355, 0.784, 0.549, 0.355, 0.781, 0.534, 0.355, 0.778, + 0.518, 0.355, 0.774, 0.502, 0.355, 0.770, 0.485, 0.355, 0.766, 0.468, + 0.355, 0.762, 0.451, 0.355, 0.758, 0.434, 0.355, 0.753, 0.416, 0.356, + 0.748, 0.397, 0.356, 0.743, 0.379, 0.356, 0.738, 0.360, 0.357, 0.733, + 0.340, 0.357, 0.728, 0.321, 0.358, 0.722, 0.300, 0.359, 0.716, 0.279, + 0.360, 0.710, 0.258, 0.361, 0.704, 0.235, 0.361, 0.698, 0.212, 0.362, + 0.692, 0.187, 0.363, 0.686, 0.160, 0.000, 0.680, 0.945, 0.000, 0.686, + 0.937, 0.000, 0.692, 0.930, 0.000, 0.698, 0.922, 0.000, 0.703, 0.915, + 0.000, 0.709, 0.908, 0.000, 0.715, 0.901, 0.000, 0.720, 0.894, 0.000, + 0.726, 0.888, 0.000, 0.731, 0.881, 0.007, 0.736, 0.875, 0.084, 0.741, + 0.869, 0.127, 0.746, 0.862, 0.159, 0.751, 0.856, 0.185, 0.755, 0.850, + 0.208, 0.760, 0.843, 0.227, 0.764, 0.837, 0.245, 0.768, 0.830, 0.260, + 0.772, 0.824, 0.275, 0.776, 0.817, 0.288, 0.779, 0.811, 0.300, 0.783, + 0.804, 0.310, 0.786, 0.797, 0.320, 0.789, 0.789, 0.329, 0.792, 0.782, + 0.337, 0.794, 0.774, 0.345, 0.796, 0.766, 0.351, 0.798, 0.758, 0.357, + 0.800, 0.749, 0.363, 0.801, 0.740, 0.367, 0.803, 0.731, 0.371, 0.803, + 0.721, 0.375, 0.804, 0.711, 0.378, 0.804, 0.701, 0.380, 0.804, 0.690, + 0.382, 0.804, 0.679, 0.384, 0.803, 0.667, 0.385, 0.802, 0.654, 0.386, + 0.801, 0.642, 0.386, 0.800, 0.629, 0.387, 0.798, 0.615, 0.387, 0.796, + 0.601, 0.387, 0.793, 0.587, 0.387, 0.791, 0.572, 0.387, 0.788, 0.557, + 0.386, 0.785, 0.541, 0.386, 0.781, 0.525, 0.385, 0.778, 0.509, 0.385, + 0.774, 0.492, 0.385, 0.770, 0.475, 0.384, 0.765, 0.457, 0.384, 0.761, + 0.440, 0.384, 0.756, 0.422, 0.384, 0.751, 0.403, 0.384, 0.746, 0.384, + 0.384, 0.741, 0.365, 0.384, 0.735, 0.346, 0.384, 0.730, 0.326, 0.384, + 0.724, 0.305, 0.384, 0.718, 0.284, 0.385, 0.712, 0.263, 0.385, 0.706, + 0.240, 0.386, 0.700, 0.217, 0.386, 0.694, 0.192, 0.387, 0.687, 0.165, + 0.000, 0.680, 0.948, 0.000, 0.687, 0.940, 0.000, 0.693, 0.933, 0.000, + 0.699, 0.925, 0.000, 0.705, 0.918, 0.000, 0.711, 0.912, 0.000, 0.716, + 0.905, 0.000, 0.722, 0.898, 0.050, 0.728, 0.892, 0.109, 0.733, 0.886, + 0.147, 0.738, 0.879, 0.177, 0.743, 0.873, 0.202, 0.748, 0.867, 0.224, + 0.753, 0.861, 0.243, 0.758, 0.855, 0.261, 0.763, 0.849, 0.277, 0.767, + 0.842, 0.292, 0.771, 0.836, 0.305, 0.775, 0.830, 0.318, 0.779, 0.823, + 0.329, 0.783, 0.817, 0.340, 0.787, 0.810, 0.350, 0.790, 0.803, 0.359, + 0.793, 0.796, 0.367, 0.796, 0.789, 0.374, 0.798, 0.782, 0.381, 0.801, + 0.774, 0.387, 0.803, 0.766, 0.393, 0.804, 0.757, 0.397, 0.806, 0.748, + 0.402, 0.807, 0.739, 0.405, 0.808, 0.729, 0.408, 0.809, 0.719, 0.411, + 0.809, 0.709, 0.413, 0.809, 0.698, 0.415, 0.808, 0.687, 0.416, 0.808, + 0.675, 0.417, 0.807, 0.663, 0.417, 0.806, 0.650, 0.417, 0.804, 0.637, + 0.418, 0.802, 0.623, 0.417, 0.800, 0.609, 0.417, 0.798, 0.594, 0.416, + 0.795, 0.579, 0.416, 0.792, 0.564, 0.415, 0.789, 0.548, 0.414, 0.785, + 0.532, 0.414, 0.781, 0.515, 0.413, 0.777, 0.499, 0.412, 0.773, 0.481, + 0.412, 0.769, 0.464, 0.411, 0.764, 0.446, 0.410, 0.759, 0.428, 0.410, + 0.754, 0.409, 0.409, 0.749, 0.390, 0.409, 0.743, 0.371, 0.409, 0.738, + 0.351, 0.409, 0.732, 0.331, 0.408, 0.726, 0.310, 0.408, 0.720, 0.289, + 0.408, 0.714, 0.268, 0.408, 0.708, 0.245, 0.409, 0.702, 0.222, 0.409, + 0.695, 0.197, 0.409, 0.689, 0.170, 0.000, 0.681, 0.950, 0.000, 0.688, + 0.943, 0.000, 0.694, 0.936, 0.000, 0.700, 0.929, 0.000, 0.706, 0.922, + 0.000, 0.712, 0.915, 0.074, 0.718, 0.908, 0.124, 0.724, 0.902, 0.159, + 0.730, 0.896, 0.188, 0.735, 0.890, 0.213, 0.740, 0.884, 0.235, 0.746, + 0.878, 0.255, 0.751, 0.872, 0.273, 0.756, 0.866, 0.289, 0.761, 0.860, + 0.305, 0.766, 0.854, 0.319, 0.770, 0.848, 0.332, 0.775, 0.842, 0.344, + 0.779, 0.836, 0.356, 0.783, 0.830, 0.366, 0.787, 0.823, 0.376, 0.790, + 0.817, 0.385, 0.794, 0.810, 0.394, 0.797, 0.803, 0.401, 0.800, 0.796, + 0.408, 0.802, 0.789, 0.414, 0.805, 0.781, 0.420, 0.807, 0.773, 0.425, + 0.809, 0.765, 0.430, 0.810, 0.756, 0.433, 0.812, 0.747, 0.437, 0.813, + 0.738, 0.440, 0.813, 0.728, 0.442, 0.814, 0.717, 0.444, 0.813, 0.706, + 0.445, 0.813, 0.695, 0.446, 0.812, 0.683, 0.446, 0.811, 0.671, 0.447, + 0.810, 0.658, 0.447, 0.809, 0.645, 0.446, 0.807, 0.631, 0.446, 0.804, + 0.617, 0.445, 0.802, 0.602, 0.444, 0.799, 0.587, 0.443, 0.796, 0.571, + 0.442, 0.792, 0.555, 0.441, 0.789, 0.539, 0.440, 0.785, 0.522, 0.439, + 0.781, 0.505, 0.438, 0.776, 0.488, 0.437, 0.772, 0.470, 0.436, 0.767, + 0.452, 0.435, 0.762, 0.433, 0.435, 0.757, 0.415, 0.434, 0.751, 0.396, + 0.433, 0.746, 0.376, 0.432, 0.740, 0.356, 0.432, 0.734, 0.336, 0.431, + 0.728, 0.315, 0.431, 0.722, 0.294, 0.431, 0.716, 0.272, 0.430, 0.710, + 0.250, 0.430, 0.703, 0.226, 0.430, 0.697, 0.201, 0.430, 0.690, 0.175, + 0.000, 0.682, 0.953, 0.000, 0.689, 0.946, 0.000, 0.695, 0.938, 0.002, + 0.701, 0.932, 0.086, 0.708, 0.925, 0.133, 0.714, 0.918, 0.167, 0.720, + 0.912, 0.196, 0.726, 0.906, 0.221, 0.731, 0.900, 0.243, 0.737, 0.894, + 0.263, 0.743, 0.888, 0.281, 0.748, 0.882, 0.298, 0.753, 0.876, 0.314, + 0.759, 0.870, 0.329, 0.764, 0.865, 0.342, 0.768, 0.859, 0.355, 0.773, + 0.853, 0.368, 0.778, 0.847, 0.379, 0.782, 0.842, 0.390, 0.786, 0.836, + 0.400, 0.790, 0.830, 0.409, 0.794, 0.823, 0.417, 0.798, 0.817, 0.425, + 0.801, 0.810, 0.433, 0.804, 0.803, 0.439, 0.807, 0.796, 0.445, 0.809, + 0.789, 0.451, 0.811, 0.781, 0.456, 0.813, 0.773, 0.460, 0.815, 0.764, + 0.463, 0.816, 0.755, 0.466, 0.817, 0.746, 0.469, 0.818, 0.736, 0.471, + 0.818, 0.725, 0.472, 0.818, 0.715, 0.473, 0.818, 0.703, 0.474, 0.817, + 0.691, 0.474, 0.816, 0.679, 0.474, 0.815, 0.666, 0.474, 0.813, 0.653, + 0.473, 0.811, 0.639, 0.473, 0.809, 0.624, 0.472, 0.806, 0.610, 0.471, + 0.803, 0.594, 0.469, 0.800, 0.579, 0.468, 0.796, 0.562, 0.467, 0.792, + 0.546, 0.466, 0.788, 0.529, 0.464, 0.784, 0.512, 0.463, 0.780, 0.494, + 0.462, 0.775, 0.476, 0.460, 0.770, 0.458, 0.459, 0.765, 0.439, 0.458, + 0.759, 0.420, 0.457, 0.754, 0.401, 0.456, 0.748, 0.381, 0.455, 0.742, + 0.361, 0.454, 0.736, 0.341, 0.453, 0.730, 0.320, 0.453, 0.724, 0.299, + 0.452, 0.718, 0.277, 0.452, 0.711, 0.254, 0.451, 0.705, 0.231, 0.451, + 0.698, 0.206, 0.450, 0.691, 0.179, 0.000, 0.683, 0.955, 0.013, 0.689, + 0.948, 0.092, 0.696, 0.941, 0.137, 0.702, 0.935, 0.171, 0.709, 0.928, + 0.200, 0.715, 0.922, 0.225, 0.721, 0.916, 0.247, 0.727, 0.909, 0.267, + 0.733, 0.904, 0.286, 0.739, 0.898, 0.303, 0.745, 0.892, 0.320, 0.750, + 0.886, 0.335, 0.756, 0.881, 0.350, 0.761, 0.875, 0.363, 0.766, 0.870, + 0.376, 0.771, 0.864, 0.388, 0.776, 0.859, 0.400, 0.781, 0.853, 0.411, + 0.785, 0.847, 0.421, 0.790, 0.842, 0.430, 0.794, 0.836, 0.439, 0.798, + 0.830, 0.448, 0.802, 0.824, 0.455, 0.805, 0.817, 0.462, 0.808, 0.810, + 0.469, 0.811, 0.804, 0.475, 0.814, 0.796, 0.480, 0.816, 0.789, 0.484, + 0.818, 0.781, 0.488, 0.820, 0.772, 0.492, 0.821, 0.763, 0.495, 0.822, + 0.754, 0.497, 0.823, 0.744, 0.499, 0.823, 0.734, 0.500, 0.823, 0.723, + 0.501, 0.823, 0.712, 0.501, 0.822, 0.700, 0.501, 0.821, 0.687, 0.501, + 0.819, 0.674, 0.500, 0.818, 0.661, 0.499, 0.815, 0.647, 0.498, 0.813, + 0.632, 0.497, 0.810, 0.617, 0.496, 0.807, 0.602, 0.494, 0.804, 0.586, + 0.493, 0.800, 0.569, 0.491, 0.796, 0.553, 0.490, 0.792, 0.536, 0.488, + 0.787, 0.518, 0.486, 0.783, 0.500, 0.485, 0.778, 0.482, 0.483, 0.773, + 0.463, 0.482, 0.767, 0.445, 0.480, 0.762, 0.425, 0.479, 0.756, 0.406, + 0.478, 0.750, 0.386, 0.477, 0.744, 0.366, 0.476, 0.738, 0.345, 0.475, + 0.732, 0.325, 0.474, 0.726, 0.303, 0.473, 0.719, 0.281, 0.472, 0.713, + 0.258, 0.471, 0.706, 0.235, 0.470, 0.699, 0.210, 0.469, 0.692, 0.184, + 0.095, 0.683, 0.958, 0.139, 0.690, 0.951, 0.173, 0.697, 0.944, 0.201, + 0.703, 0.938, 0.226, 0.710, 0.931, 0.249, 0.716, 0.925, 0.269, 0.723, + 0.919, 0.288, 0.729, 0.913, 0.306, 0.735, 0.907, 0.323, 0.741, 0.902, + 0.339, 0.747, 0.896, 0.354, 0.752, 0.891, 0.368, 0.758, 0.885, 0.382, + 0.764, 0.880, 0.394, 0.769, 0.875, 0.407, 0.774, 0.869, 0.418, 0.779, + 0.864, 0.429, 0.784, 0.859, 0.440, 0.789, 0.853, 0.450, 0.793, 0.848, + 0.459, 0.798, 0.842, 0.468, 0.802, 0.836, 0.476, 0.806, 0.830, 0.483, + 0.809, 0.824, 0.490, 0.812, 0.818, 0.496, 0.815, 0.811, 0.502, 0.818, + 0.804, 0.507, 0.821, 0.796, 0.512, 0.823, 0.789, 0.515, 0.825, 0.780, + 0.519, 0.826, 0.772, 0.521, 0.827, 0.762, 0.524, 0.828, 0.753, 0.525, + 0.828, 0.742, 0.526, 0.828, 0.732, 0.527, 0.828, 0.720, 0.527, 0.827, + 0.708, 0.527, 0.826, 0.696, 0.526, 0.824, 0.683, 0.525, 0.822, 0.669, + 0.524, 0.820, 0.655, 0.523, 0.817, 0.640, 0.522, 0.814, 0.625, 0.520, + 0.811, 0.609, 0.518, 0.808, 0.593, 0.516, 0.804, 0.576, 0.515, 0.800, + 0.559, 0.513, 0.795, 0.542, 0.511, 0.791, 0.524, 0.509, 0.786, 0.506, + 0.507, 0.781, 0.488, 0.505, 0.775, 0.469, 0.504, 0.770, 0.450, 0.502, + 0.764, 0.431, 0.500, 0.759, 0.411, 0.499, 0.753, 0.391, 0.497, 0.746, + 0.371, 0.496, 0.740, 0.350, 0.495, 0.734, 0.329, 0.494, 0.727, 0.307, + 0.492, 0.721, 0.285, 0.491, 0.714, 0.262, 0.490, 0.707, 0.239, 0.489, + 0.700, 0.214, 0.488, 0.693, 0.188, 0.172, 0.684, 0.961, 0.201, 0.691, + 0.954, 0.226, 0.698, 0.947, 0.248, 0.704, 0.941, 0.269, 0.711, 0.934, + 0.289, 0.717, 0.928, 0.307, 0.724, 0.922, 0.324, 0.730, 0.917, 0.340, + 0.736, 0.911, 0.356, 0.743, 0.906, 0.370, 0.749, 0.900, 0.384, 0.755, + 0.895, 0.398, 0.760, 0.890, 0.411, 0.766, 0.885, 0.423, 0.772, 0.880, + 0.435, 0.777, 0.874, 0.446, 0.782, 0.869, 0.457, 0.787, 0.864, 0.467, + 0.792, 0.859, 0.477, 0.797, 0.854, 0.486, 0.801, 0.848, 0.494, 0.806, + 0.843, 0.502, 0.810, 0.837, 0.510, 0.813, 0.831, 0.517, 0.817, 0.825, + 0.523, 0.820, 0.818, 0.528, 0.823, 0.811, 0.533, 0.825, 0.804, 0.538, + 0.828, 0.797, 0.542, 0.829, 0.788, 0.545, 0.831, 0.780, 0.547, 0.832, + 0.771, 0.549, 0.833, 0.761, 0.551, 0.833, 0.751, 0.552, 0.833, 0.740, + 0.552, 0.833, 0.729, 0.552, 0.832, 0.717, 0.551, 0.830, 0.704, 0.551, + 0.829, 0.691, 0.550, 0.827, 0.677, 0.548, 0.824, 0.663, 0.547, 0.822, + 0.648, 0.545, 0.819, 0.632, 0.543, 0.815, 0.617, 0.541, 0.812, 0.600, + 0.539, 0.808, 0.583, 0.537, 0.803, 0.566, 0.535, 0.799, 0.549, 0.533, + 0.794, 0.531, 0.531, 0.789, 0.512, 0.529, 0.784, 0.494, 0.527, 0.778, + 0.475, 0.525, 0.773, 0.455, 0.523, 0.767, 0.436, 0.521, 0.761, 0.416, + 0.519, 0.755, 0.396, 0.517, 0.748, 0.375, 0.516, 0.742, 0.354, 0.514, + 0.735, 0.333, 0.513, 0.729, 0.311, 0.511, 0.722, 0.289, 0.510, 0.715, + 0.266, 0.509, 0.708, 0.242, 0.507, 0.701, 0.218, 0.506, 0.694, 0.191, + 0.224, 0.684, 0.963, 0.247, 0.691, 0.956, 0.268, 0.698, 0.950, 0.287, + 0.705, 0.943, 0.305, 0.712, 0.937, 0.323, 0.719, 0.931, 0.339, 0.725, + 0.926, 0.355, 0.732, 0.920, 0.370, 0.738, 0.915, 0.385, 0.744, 0.909, + 0.399, 0.751, 0.904, 0.412, 0.757, 0.899, 0.425, 0.763, 0.894, 0.438, + 0.768, 0.889, 0.450, 0.774, 0.884, 0.461, 0.780, 0.879, 0.472, 0.785, + 0.875, 0.483, 0.790, 0.870, 0.493, 0.795, 0.865, 0.502, 0.800, 0.860, + 0.511, 0.805, 0.855, 0.520, 0.809, 0.849, 0.528, 0.814, 0.844, 0.535, + 0.818, 0.838, 0.542, 0.821, 0.832, 0.548, 0.824, 0.826, 0.554, 0.827, + 0.819, 0.559, 0.830, 0.812, 0.563, 0.832, 0.805, 0.567, 0.834, 0.797, + 0.570, 0.836, 0.788, 0.572, 0.837, 0.779, 0.574, 0.838, 0.770, 0.575, + 0.838, 0.760, 0.576, 0.838, 0.749, 0.576, 0.838, 0.737, 0.576, 0.837, + 0.725, 0.575, 0.835, 0.713, 0.574, 0.834, 0.699, 0.573, 0.831, 0.685, + 0.571, 0.829, 0.671, 0.570, 0.826, 0.656, 0.568, 0.823, 0.640, 0.566, + 0.819, 0.624, 0.563, 0.815, 0.607, 0.561, 0.811, 0.590, 0.559, 0.807, + 0.573, 0.556, 0.802, 0.555, 0.554, 0.797, 0.537, 0.552, 0.792, 0.518, + 0.549, 0.786, 0.499, 0.547, 0.781, 0.480, 0.545, 0.775, 0.460, 0.543, + 0.769, 0.441, 0.541, 0.763, 0.420, 0.539, 0.756, 0.400, 0.537, 0.750, + 0.379, 0.535, 0.743, 0.358, 0.533, 0.737, 0.337, 0.531, 0.730, 0.315, + 0.530, 0.723, 0.293, 0.528, 0.716, 0.270, 0.527, 0.709, 0.246, 0.525, + 0.702, 0.221, 0.524, 0.694, 0.195, 0.265, 0.685, 0.965, 0.284, 0.692, + 0.959, 0.303, 0.699, 0.952, 0.320, 0.706, 0.946, 0.337, 0.713, 0.940, + 0.353, 0.720, 0.935, 0.369, 0.726, 0.929, 0.384, 0.733, 0.924, 0.398, + 0.739, 0.918, 0.412, 0.746, 0.913, 0.425, 0.752, 0.908, 0.438, 0.759, + 0.903, 0.451, 0.765, 0.899, 0.463, 0.771, 0.894, 0.475, 0.777, 0.889, + 0.486, 0.782, 0.884, 0.497, 0.788, 0.880, 0.507, 0.793, 0.875, 0.517, + 0.799, 0.870, 0.527, 0.804, 0.866, 0.536, 0.809, 0.861, 0.544, 0.813, + 0.856, 0.552, 0.818, 0.850, 0.560, 0.822, 0.845, 0.566, 0.826, 0.839, + 0.573, 0.829, 0.833, 0.578, 0.832, 0.827, 0.583, 0.835, 0.820, 0.587, + 0.837, 0.813, 0.591, 0.839, 0.805, 0.594, 0.841, 0.797, 0.596, 0.842, + 0.788, 0.598, 0.843, 0.778, 0.599, 0.843, 0.768, 0.600, 0.843, 0.758, + 0.600, 0.843, 0.746, 0.599, 0.842, 0.734, 0.599, 0.840, 0.721, 0.597, + 0.838, 0.708, 0.596, 0.836, 0.694, 0.594, 0.834, 0.679, 0.592, 0.831, + 0.663, 0.590, 0.827, 0.648, 0.587, 0.823, 0.631, 0.585, 0.819, 0.614, + 0.582, 0.815, 0.597, 0.580, 0.810, 0.579, 0.577, 0.805, 0.561, 0.575, + 0.800, 0.542, 0.572, 0.795, 0.524, 0.569, 0.789, 0.504, 0.567, 0.783, + 0.485, 0.565, 0.777, 0.465, 0.562, 0.771, 0.445, 0.560, 0.765, 0.425, + 0.558, 0.758, 0.404, 0.556, 0.752, 0.383, 0.554, 0.745, 0.362, 0.552, + 0.738, 0.341, 0.550, 0.731, 0.319, 0.548, 0.724, 0.296, 0.546, 0.717, + 0.273, 0.544, 0.709, 0.249, 0.542, 0.702, 0.224, 0.541, 0.695, 0.198, + 0.299, 0.685, 0.968, 0.317, 0.692, 0.961, 0.334, 0.699, 0.955, 0.350, + 0.706, 0.949, 0.366, 0.713, 0.943, 0.381, 0.720, 0.938, 0.395, 0.727, + 0.932, 0.410, 0.734, 0.927, 0.423, 0.741, 0.922, 0.437, 0.747, 0.917, + 0.450, 0.754, 0.912, 0.463, 0.760, 0.907, 0.475, 0.767, 0.903, 0.487, + 0.773, 0.898, 0.498, 0.779, 0.894, 0.509, 0.785, 0.889, 0.520, 0.791, + 0.885, 0.531, 0.796, 0.880, 0.540, 0.802, 0.876, 0.550, 0.807, 0.871, + 0.559, 0.812, 0.867, 0.568, 0.817, 0.862, 0.576, 0.822, 0.857, 0.583, + 0.826, 0.852, 0.590, 0.830, 0.847, 0.596, 0.834, 0.841, 0.602, 0.837, + 0.835, 0.607, 0.840, 0.828, 0.611, 0.843, 0.821, 0.615, 0.845, 0.814, + 0.618, 0.846, 0.805, 0.620, 0.848, 0.797, 0.622, 0.848, 0.787, 0.623, + 0.849, 0.777, 0.623, 0.849, 0.766, 0.623, 0.848, 0.755, 0.622, 0.847, + 0.743, 0.621, 0.845, 0.730, 0.620, 0.843, 0.716, 0.618, 0.841, 0.702, + 0.616, 0.838, 0.687, 0.613, 0.835, 0.671, 0.611, 0.831, 0.655, 0.608, + 0.827, 0.638, 0.606, 0.823, 0.621, 0.603, 0.818, 0.604, 0.600, 0.814, + 0.585, 0.597, 0.808, 0.567, 0.594, 0.803, 0.548, 0.592, 0.797, 0.529, + 0.589, 0.792, 0.510, 0.586, 0.785, 0.490, 0.584, 0.779, 0.470, 0.581, + 0.773, 0.450, 0.579, 0.766, 0.429, 0.576, 0.760, 0.408, 0.574, 0.753, + 0.387, 0.572, 0.746, 0.366, 0.569, 0.739, 0.344, 0.567, 0.732, 0.322, + 0.565, 0.725, 0.299, 0.563, 0.717, 0.276, 0.561, 0.710, 0.252, 0.559, + 0.703, 0.227, 0.557, 0.695, 0.201, 0.329, 0.685, 0.970, 0.346, 0.692, + 0.964, 0.362, 0.699, 0.958, 0.377, 0.707, 0.952, 0.392, 0.714, 0.946, + 0.406, 0.721, 0.941, 0.420, 0.728, 0.935, 0.434, 0.735, 0.930, 0.447, + 0.742, 0.925, 0.460, 0.749, 0.920, 0.473, 0.756, 0.916, 0.485, 0.762, + 0.911, 0.497, 0.769, 0.907, 0.509, 0.775, 0.903, 0.521, 0.781, 0.898, + 0.532, 0.788, 0.894, 0.542, 0.794, 0.890, 0.553, 0.799, 0.886, 0.563, + 0.805, 0.882, 0.572, 0.811, 0.877, 0.581, 0.816, 0.873, 0.590, 0.821, + 0.868, 0.598, 0.826, 0.864, 0.606, 0.830, 0.859, 0.613, 0.834, 0.854, + 0.619, 0.838, 0.848, 0.625, 0.842, 0.842, 0.630, 0.845, 0.836, 0.634, + 0.848, 0.829, 0.638, 0.850, 0.822, 0.641, 0.852, 0.814, 0.643, 0.853, + 0.805, 0.645, 0.854, 0.796, 0.645, 0.854, 0.786, 0.646, 0.854, 0.775, + 0.645, 0.853, 0.764, 0.645, 0.852, 0.751, 0.643, 0.851, 0.738, 0.642, + 0.848, 0.725, 0.639, 0.846, 0.710, 0.637, 0.843, 0.695, 0.635, 0.839, + 0.679, 0.632, 0.836, 0.662, 0.629, 0.831, 0.645, 0.626, 0.827, 0.628, + 0.623, 0.822, 0.610, 0.620, 0.817, 0.592, 0.617, 0.811, 0.573, 0.614, + 0.806, 0.554, 0.611, 0.800, 0.534, 0.608, 0.794, 0.515, 0.605, 0.788, + 0.495, 0.602, 0.781, 0.474, 0.599, 0.775, 0.454, 0.597, 0.768, 0.433, + 0.594, 0.761, 0.412, 0.592, 0.754, 0.391, 0.589, 0.747, 0.369, 0.587, + 0.740, 0.347, 0.584, 0.733, 0.325, 0.582, 0.725, 0.302, 0.580, 0.718, + 0.279, 0.577, 0.710, 0.255, 0.575, 0.703, 0.230, 0.573, 0.695, 0.204, + 0.357, 0.685, 0.972, 0.372, 0.692, 0.966, 0.387, 0.700, 0.960, 0.401, + 0.707, 0.954, 0.416, 0.714, 0.949, 0.429, 0.722, 0.943, 0.443, 0.729, + 0.938, 0.456, 0.736, 0.933, 0.469, 0.743, 0.929, 0.482, 0.750, 0.924, + 0.494, 0.757, 0.919, 0.507, 0.764, 0.915, 0.519, 0.771, 0.911, 0.530, + 0.777, 0.907, 0.542, 0.784, 0.903, 0.553, 0.790, 0.899, 0.563, 0.796, + 0.895, 0.574, 0.802, 0.891, 0.584, 0.808, 0.887, 0.593, 0.814, 0.883, + 0.603, 0.820, 0.879, 0.611, 0.825, 0.875, 0.620, 0.830, 0.870, 0.627, + 0.835, 0.866, 0.635, 0.839, 0.861, 0.641, 0.843, 0.856, 0.647, 0.847, + 0.850, 0.652, 0.850, 0.844, 0.657, 0.853, 0.838, 0.660, 0.855, 0.831, + 0.663, 0.857, 0.823, 0.666, 0.859, 0.814, 0.667, 0.859, 0.805, 0.668, + 0.860, 0.795, 0.668, 0.860, 0.784, 0.667, 0.859, 0.773, 0.666, 0.858, + 0.760, 0.665, 0.856, 0.747, 0.663, 0.853, 0.733, 0.661, 0.851, 0.718, + 0.658, 0.847, 0.703, 0.655, 0.844, 0.687, 0.652, 0.840, 0.670, 0.649, + 0.835, 0.652, 0.646, 0.830, 0.635, 0.642, 0.825, 0.616, 0.639, 0.820, + 0.598, 0.636, 0.814, 0.579, 0.633, 0.808, 0.559, 0.629, 0.802, 0.539, + 0.626, 0.796, 0.519, 0.623, 0.790, 0.499, 0.620, 0.783, 0.479, 0.617, + 0.776, 0.458, 0.614, 0.769, 0.437, 0.611, 0.762, 0.416, 0.609, 0.755, + 0.394, 0.606, 0.748, 0.372, 0.603, 0.740, 0.350, 0.601, 0.733, 0.328, + 0.598, 0.726, 0.305, 0.596, 0.718, 0.282, 0.593, 0.710, 0.257, 0.591, + 0.703, 0.232, 0.589, 0.695, 0.206, 0.381, 0.684, 0.974, 0.396, 0.692, + 0.968, 0.410, 0.700, 0.962, 0.424, 0.707, 0.957, 0.438, 0.715, 0.951, + 0.451, 0.722, 0.946, 0.464, 0.729, 0.941, 0.477, 0.737, 0.936, 0.490, + 0.744, 0.932, 0.503, 0.751, 0.927, 0.515, 0.758, 0.923, 0.527, 0.765, + 0.919, 0.539, 0.772, 0.915, 0.550, 0.779, 0.911, 0.562, 0.786, 0.907, + 0.573, 0.792, 0.903, 0.584, 0.799, 0.900, 0.594, 0.805, 0.896, 0.604, + 0.811, 0.892, 0.614, 0.817, 0.889, 0.623, 0.823, 0.885, 0.632, 0.829, + 0.881, 0.641, 0.834, 0.877, 0.649, 0.839, 0.873, 0.656, 0.844, 0.868, + 0.663, 0.848, 0.863, 0.669, 0.852, 0.858, 0.674, 0.855, 0.852, 0.679, + 0.858, 0.846, 0.682, 0.861, 0.839, 0.685, 0.863, 0.832, 0.688, 0.864, + 0.823, 0.689, 0.865, 0.814, 0.690, 0.865, 0.804, 0.690, 0.865, 0.794, + 0.689, 0.864, 0.782, 0.688, 0.863, 0.769, 0.686, 0.861, 0.756, 0.684, + 0.858, 0.742, 0.681, 0.855, 0.726, 0.678, 0.852, 0.711, 0.675, 0.848, + 0.694, 0.672, 0.844, 0.677, 0.668, 0.839, 0.659, 0.665, 0.834, 0.641, + 0.662, 0.829, 0.622, 0.658, 0.823, 0.603, 0.655, 0.817, 0.584, 0.651, + 0.811, 0.564, 0.648, 0.805, 0.544, 0.644, 0.798, 0.524, 0.641, 0.791, + 0.503, 0.638, 0.785, 0.483, 0.635, 0.778, 0.462, 0.631, 0.770, 0.440, + 0.628, 0.763, 0.419, 0.625, 0.756, 0.397, 0.623, 0.748, 0.375, 0.620, + 0.741, 0.353, 0.617, 0.733, 0.330, 0.614, 0.726, 0.307, 0.612, 0.718, + 0.284, 0.609, 0.710, 0.260, 0.606, 0.702, 0.235, 0.604, 0.694, 0.208, + 0.404, 0.684, 0.977, 0.418, 0.692, 0.971, 0.432, 0.699, 0.965, 0.445, + 0.707, 0.959, 0.458, 0.715, 0.954, 0.472, 0.722, 0.949, 0.484, 0.730, + 0.944, 0.497, 0.737, 0.939, 0.510, 0.745, 0.935, 0.522, 0.752, 0.931, + 0.534, 0.759, 0.926, 0.546, 0.767, 0.922, 0.558, 0.774, 0.919, 0.569, + 0.781, 0.915, 0.581, 0.788, 0.911, 0.592, 0.794, 0.908, 0.603, 0.801, + 0.904, 0.613, 0.808, 0.901, 0.624, 0.814, 0.897, 0.633, 0.820, 0.894, + 0.643, 0.826, 0.891, 0.652, 0.832, 0.887, 0.661, 0.838, 0.883, 0.669, + 0.843, 0.879, 0.677, 0.848, 0.875, 0.684, 0.853, 0.871, 0.690, 0.857, + 0.866, 0.695, 0.860, 0.860, 0.700, 0.864, 0.855, 0.704, 0.866, 0.848, + 0.707, 0.869, 0.841, 0.709, 0.870, 0.833, 0.711, 0.871, 0.824, 0.711, + 0.871, 0.814, 0.711, 0.871, 0.803, 0.710, 0.870, 0.791, 0.709, 0.868, + 0.778, 0.707, 0.866, 0.765, 0.704, 0.864, 0.750, 0.701, 0.860, 0.735, + 0.698, 0.857, 0.718, 0.695, 0.852, 0.702, 0.691, 0.848, 0.684, 0.688, + 0.843, 0.666, 0.684, 0.837, 0.647, 0.680, 0.832, 0.628, 0.676, 0.826, + 0.609, 0.673, 0.820, 0.589, 0.669, 0.813, 0.569, 0.665, 0.807, 0.549, + 0.662, 0.800, 0.528, 0.658, 0.793, 0.507, 0.655, 0.786, 0.486, 0.651, + 0.779, 0.465, 0.648, 0.771, 0.444, 0.645, 0.764, 0.422, 0.642, 0.756, + 0.400, 0.639, 0.749, 0.378, 0.636, 0.741, 0.356, 0.633, 0.733, 0.333, + 0.630, 0.726, 0.310, 0.627, 0.718, 0.286, 0.624, 0.710, 0.262, 0.621, + 0.702, 0.237, 0.619, 0.694, 0.210, 0.425, 0.683, 0.979, 0.439, 0.691, + 0.973, 0.452, 0.699, 0.967, 0.465, 0.707, 0.962, 0.478, 0.715, 0.956, + 0.491, 0.722, 0.951, 0.503, 0.730, 0.947, 0.516, 0.738, 0.942, 0.528, + 0.745, 0.938, 0.540, 0.753, 0.934, 0.552, 0.760, 0.930, 0.564, 0.768, + 0.926, 0.576, 0.775, 0.922, 0.588, 0.782, 0.919, 0.599, 0.789, 0.915, + 0.610, 0.797, 0.912, 0.621, 0.803, 0.909, 0.632, 0.810, 0.906, 0.642, + 0.817, 0.902, 0.652, 0.823, 0.899, 0.662, 0.830, 0.896, 0.671, 0.836, + 0.893, 0.680, 0.842, 0.890, 0.689, 0.847, 0.886, 0.697, 0.853, 0.882, + 0.704, 0.857, 0.878, 0.710, 0.862, 0.874, 0.716, 0.866, 0.869, 0.721, + 0.869, 0.863, 0.725, 0.872, 0.857, 0.729, 0.874, 0.850, 0.731, 0.876, + 0.842, 0.732, 0.877, 0.833, 0.733, 0.877, 0.823, 0.732, 0.877, 0.812, + 0.731, 0.876, 0.800, 0.729, 0.874, 0.787, 0.727, 0.872, 0.773, 0.724, + 0.869, 0.759, 0.721, 0.865, 0.743, 0.718, 0.861, 0.726, 0.714, 0.857, + 0.709, 0.710, 0.852, 0.691, 0.706, 0.846, 0.672, 0.702, 0.841, 0.653, + 0.698, 0.835, 0.634, 0.694, 0.828, 0.614, 0.690, 0.822, 0.594, 0.686, + 0.815, 0.574, 0.683, 0.808, 0.553, 0.679, 0.801, 0.532, 0.675, 0.794, + 0.511, 0.672, 0.787, 0.490, 0.668, 0.779, 0.468, 0.665, 0.772, 0.446, + 0.661, 0.764, 0.425, 0.658, 0.757, 0.403, 0.654, 0.749, 0.380, 0.651, + 0.741, 0.358, 0.648, 0.733, 0.335, 0.645, 0.725, 0.312, 0.642, 0.717, + 0.288, 0.639, 0.709, 0.264, 0.636, 0.701, 0.238, 0.633, 0.693, 0.212, + 0.445, 0.682, 0.981, 0.458, 0.691, 0.975, 0.471, 0.699, 0.969, 0.484, + 0.707, 0.964, 0.496, 0.715, 0.959, 0.509, 0.722, 0.954, 0.521, 0.730, + 0.949, 0.534, 0.738, 0.945, 0.546, 0.746, 0.941, 0.558, 0.753, 0.937, + 0.570, 0.761, 0.933, 0.582, 0.769, 0.929, 0.593, 0.776, 0.926, 0.605, + 0.784, 0.922, 0.616, 0.791, 0.919, 0.628, 0.798, 0.916, 0.639, 0.806, + 0.913, 0.649, 0.813, 0.910, 0.660, 0.820, 0.907, 0.670, 0.826, 0.904, + 0.680, 0.833, 0.902, 0.690, 0.839, 0.899, 0.699, 0.846, 0.896, 0.708, + 0.851, 0.893, 0.716, 0.857, 0.889, 0.724, 0.862, 0.885, 0.731, 0.867, + 0.881, 0.737, 0.871, 0.877, 0.742, 0.875, 0.872, 0.746, 0.878, 0.866, + 0.750, 0.880, 0.859, 0.752, 0.882, 0.851, 0.753, 0.883, 0.843, 0.754, + 0.883, 0.833, 0.753, 0.883, 0.822, 0.752, 0.882, 0.810, 0.750, 0.880, + 0.797, 0.747, 0.877, 0.782, 0.744, 0.874, 0.767, 0.740, 0.870, 0.751, + 0.737, 0.866, 0.734, 0.733, 0.861, 0.716, 0.729, 0.855, 0.697, 0.724, + 0.850, 0.678, 0.720, 0.844, 0.659, 0.716, 0.837, 0.639, 0.712, 0.831, + 0.619, 0.708, 0.824, 0.598, 0.704, 0.817, 0.578, 0.699, 0.810, 0.557, + 0.696, 0.803, 0.535, 0.692, 0.795, 0.514, 0.688, 0.788, 0.493, 0.684, + 0.780, 0.471, 0.680, 0.772, 0.449, 0.677, 0.765, 0.427, 0.673, 0.757, + 0.405, 0.670, 0.749, 0.382, 0.666, 0.741, 0.360, 0.663, 0.733, 0.337, + 0.660, 0.725, 0.313, 0.657, 0.716, 0.289, 0.653, 0.708, 0.265, 0.650, + 0.700, 0.240, 0.647, 0.692, 0.213, 0.464, 0.681, 0.982, 0.476, 0.690, + 0.977, 0.489, 0.698, 0.971, 0.501, 0.706, 0.966, 0.514, 0.714, 0.961, + 0.526, 0.722, 0.956, 0.538, 0.730, 0.952, 0.550, 0.738, 0.947, 0.562, + 0.746, 0.943, 0.574, 0.754, 0.939, 0.586, 0.762, 0.936, 0.598, 0.769, + 0.932, 0.610, 0.777, 0.929, 0.621, 0.785, 0.926, 0.633, 0.792, 0.923, + 0.644, 0.800, 0.920, 0.655, 0.807, 0.917, 0.666, 0.815, 0.915, 0.677, + 0.822, 0.912, 0.688, 0.829, 0.909, 0.698, 0.836, 0.907, 0.708, 0.843, + 0.904, 0.717, 0.849, 0.902, 0.727, 0.855, 0.899, 0.735, 0.861, 0.896, + 0.743, 0.867, 0.893, 0.750, 0.872, 0.889, 0.757, 0.877, 0.885, 0.762, + 0.881, 0.880, 0.767, 0.884, 0.875, 0.770, 0.887, 0.868, 0.773, 0.888, + 0.861, 0.774, 0.889, 0.852, 0.774, 0.890, 0.842, 0.774, 0.889, 0.831, + 0.772, 0.888, 0.819, 0.770, 0.885, 0.806, 0.767, 0.883, 0.791, 0.763, + 0.879, 0.775, 0.759, 0.875, 0.759, 0.755, 0.870, 0.741, 0.751, 0.865, + 0.723, 0.747, 0.859, 0.704, 0.742, 0.853, 0.684, 0.738, 0.847, 0.664, + 0.733, 0.840, 0.644, 0.729, 0.833, 0.623, 0.724, 0.826, 0.603, 0.720, + 0.819, 0.581, 0.716, 0.811, 0.560, 0.712, 0.804, 0.539, 0.708, 0.796, + 0.517, 0.704, 0.788, 0.495, 0.700, 0.780, 0.473, 0.696, 0.772, 0.451, + 0.692, 0.764, 0.429, 0.688, 0.756, 0.407, 0.685, 0.748, 0.384, 0.681, + 0.740, 0.361, 0.678, 0.732, 0.338, 0.674, 0.724, 0.315, 0.671, 0.715, + 0.291, 0.667, 0.707, 0.266, 0.664, 0.699, 0.241, 0.661, 0.691, 0.214, + 0.481, 0.680, 0.984, 0.494, 0.689, 0.978, 0.506, 0.697, 0.973, 0.518, + 0.705, 0.968, 0.530, 0.713, 0.963, 0.542, 0.722, 0.958, 0.554, 0.730, + 0.954, 0.566, 0.738, 0.950, 0.578, 0.746, 0.946, 0.590, 0.754, 0.942, + 0.602, 0.762, 0.939, 0.614, 0.770, 0.935, 0.626, 0.778, 0.932, 0.637, + 0.786, 0.929, 0.649, 0.794, 0.926, 0.660, 0.801, 0.924, 0.671, 0.809, + 0.921, 0.683, 0.817, 0.919, 0.694, 0.824, 0.916, 0.704, 0.832, 0.914, + 0.715, 0.839, 0.912, 0.725, 0.846, 0.910, 0.735, 0.853, 0.908, 0.744, + 0.859, 0.905, 0.753, 0.866, 0.903, 0.762, 0.872, 0.900, 0.770, 0.877, + 0.897, 0.776, 0.882, 0.893, 0.782, 0.886, 0.889, 0.787, 0.890, 0.884, + 0.791, 0.893, 0.878, 0.794, 0.895, 0.871, 0.795, 0.896, 0.862, 0.795, + 0.896, 0.852, 0.794, 0.895, 0.841, 0.792, 0.894, 0.829, 0.789, 0.891, + 0.815, 0.786, 0.888, 0.800, 0.782, 0.884, 0.783, 0.778, 0.879, 0.766, + 0.774, 0.874, 0.748, 0.769, 0.868, 0.729, 0.764, 0.862, 0.710, 0.760, + 0.856, 0.690, 0.755, 0.849, 0.669, 0.750, 0.842, 0.649, 0.745, 0.835, + 0.628, 0.741, 0.827, 0.606, 0.736, 0.820, 0.585, 0.732, 0.812, 0.563, + 0.728, 0.804, 0.542, 0.723, 0.796, 0.520, 0.719, 0.788, 0.498, 0.715, + 0.780, 0.475, 0.711, 0.772, 0.453, 0.707, 0.764, 0.431, 0.703, 0.756, + 0.408, 0.699, 0.748, 0.386, 0.696, 0.739, 0.363, 0.692, 0.731, 0.339, + 0.688, 0.723, 0.316, 0.685, 0.714, 0.292, 0.681, 0.706, 0.267, 0.678, + 0.697, 0.242, 0.674, 0.689, 0.215, 0.498, 0.679, 0.986, 0.510, 0.687, + 0.980, 0.522, 0.696, 0.975, 0.534, 0.704, 0.970, 0.546, 0.712, 0.965, + 0.558, 0.721, 0.961, 0.570, 0.729, 0.956, 0.581, 0.737, 0.952, 0.593, + 0.746, 0.948, 0.605, 0.754, 0.945, 0.617, 0.762, 0.941, 0.629, 0.770, + 0.938, 0.640, 0.778, 0.935, 0.652, 0.786, 0.932, 0.664, 0.794, 0.930, + 0.675, 0.802, 0.927, 0.687, 0.810, 0.925, 0.698, 0.818, 0.923, 0.709, + 0.826, 0.921, 0.720, 0.834, 0.919, 0.731, 0.841, 0.917, 0.742, 0.849, + 0.915, 0.752, 0.856, 0.913, 0.762, 0.863, 0.911, 0.771, 0.870, 0.909, + 0.780, 0.876, 0.907, 0.788, 0.882, 0.904, 0.796, 0.887, 0.901, 0.802, + 0.892, 0.897, 0.807, 0.896, 0.893, 0.811, 0.899, 0.887, 0.814, 0.902, + 0.880, 0.815, 0.903, 0.872, 0.815, 0.903, 0.862, 0.814, 0.902, 0.851, + 0.812, 0.900, 0.838, 0.809, 0.897, 0.824, 0.805, 0.893, 0.808, 0.801, + 0.889, 0.791, 0.796, 0.884, 0.774, 0.791, 0.878, 0.755, 0.786, 0.872, + 0.735, 0.781, 0.865, 0.715, 0.776, 0.858, 0.695, 0.771, 0.851, 0.674, + 0.767, 0.844, 0.653, 0.762, 0.836, 0.631, 0.757, 0.829, 0.610, 0.752, + 0.821, 0.588, 0.748, 0.813, 0.566, 0.743, 0.805, 0.544, 0.739, 0.796, + 0.522, 0.734, 0.788, 0.500, 0.730, 0.780, 0.477, 0.726, 0.772, 0.455, + 0.722, 0.763, 0.432, 0.718, 0.755, 0.410, 0.714, 0.746, 0.387, 0.710, + 0.738, 0.364, 0.706, 0.730, 0.340, 0.702, 0.721, 0.317, 0.698, 0.713, + 0.293, 0.694, 0.704, 0.268, 0.691, 0.696, 0.243, 0.687, 0.687, 0.216, + 0.513, 0.677, 0.987, 0.525, 0.686, 0.982, 0.537, 0.694, 0.977, 0.549, + 0.703, 0.972, 0.561, 0.711, 0.967, 0.572, 0.720, 0.962, 0.584, 0.728, + 0.958, 0.596, 0.737, 0.954, 0.608, 0.745, 0.951, 0.619, 0.753, 0.947, + 0.631, 0.762, 0.944, 0.643, 0.770, 0.941, 0.655, 0.778, 0.938, 0.666, + 0.787, 0.935, 0.678, 0.795, 0.933, 0.689, 0.803, 0.930, 0.701, 0.811, + 0.928, 0.713, 0.820, 0.926, 0.724, 0.828, 0.925, 0.735, 0.836, 0.923, + 0.746, 0.844, 0.921, 0.757, 0.852, 0.920, 0.768, 0.859, 0.918, 0.778, + 0.867, 0.917, 0.788, 0.874, 0.915, 0.797, 0.881, 0.913, 0.806, 0.887, + 0.911, 0.814, 0.893, 0.909, 0.821, 0.898, 0.906, 0.827, 0.902, 0.902, + 0.831, 0.906, 0.897, 0.834, 0.908, 0.890, 0.836, 0.910, 0.882, 0.836, + 0.910, 0.873, 0.834, 0.909, 0.861, 0.832, 0.906, 0.848, 0.828, 0.903, + 0.833, 0.824, 0.899, 0.817, 0.819, 0.894, 0.799, 0.814, 0.888, 0.781, + 0.809, 0.882, 0.761, 0.804, 0.875, 0.741, 0.798, 0.868, 0.720, 0.793, + 0.861, 0.699, 0.788, 0.853, 0.678, 0.783, 0.845, 0.656, 0.777, 0.837, + 0.635, 0.772, 0.829, 0.613, 0.768, 0.821, 0.590, 0.763, 0.813, 0.568, + 0.758, 0.804, 0.546, 0.753, 0.796, 0.524, 0.749, 0.788, 0.501, 0.744, + 0.779, 0.479, 0.740, 0.771, 0.456, 0.736, 0.762, 0.433, 0.731, 0.754, + 0.411, 0.727, 0.745, 0.388, 0.723, 0.736, 0.365, 0.719, 0.728, 0.341, + 0.715, 0.719, 0.317, 0.711, 0.711, 0.293, 0.707, 0.702, 0.268, 0.704, + 0.694, 0.243, 0.700, 0.685, 0.216, 0.528, 0.675, 0.989, 0.540, 0.684, + 0.983, 0.551, 0.693, 0.978, 0.563, 0.701, 0.973, 0.575, 0.710, 0.969, + 0.586, 0.718, 0.964, 0.598, 0.727, 0.960, 0.610, 0.736, 0.956, 0.621, + 0.744, 0.953, 0.633, 0.753, 0.949, 0.645, 0.761, 0.946, 0.656, 0.770, + 0.943, 0.668, 0.778, 0.940, 0.680, 0.787, 0.938, 0.691, 0.795, 0.936, + 0.703, 0.804, 0.933, 0.715, 0.812, 0.932, 0.726, 0.821, 0.930, 0.738, + 0.829, 0.928, 0.749, 0.837, 0.927, 0.761, 0.846, 0.926, 0.772, 0.854, + 0.924, 0.783, 0.862, 0.923, 0.794, 0.870, 0.922, 0.804, 0.877, 0.921, + 0.814, 0.885, 0.920, 0.824, 0.892, 0.918, 0.832, 0.898, 0.917, 0.840, + 0.904, 0.914, 0.846, 0.909, 0.911, 0.851, 0.913, 0.906, 0.855, 0.915, + 0.901, 0.856, 0.917, 0.893, 0.856, 0.917, 0.883, 0.854, 0.915, 0.871, + 0.851, 0.913, 0.858, 0.847, 0.909, 0.842, 0.842, 0.904, 0.825, 0.837, + 0.898, 0.806, 0.831, 0.892, 0.787, 0.826, 0.885, 0.767, 0.820, 0.878, + 0.746, 0.814, 0.870, 0.725, 0.809, 0.862, 0.703, 0.803, 0.854, 0.681, + 0.798, 0.846, 0.659, 0.793, 0.838, 0.637, 0.788, 0.829, 0.615, 0.782, + 0.821, 0.592, 0.777, 0.812, 0.570, 0.773, 0.804, 0.548, 0.768, 0.795, + 0.525, 0.763, 0.787, 0.502, 0.758, 0.778, 0.480, 0.754, 0.769, 0.457, + 0.749, 0.761, 0.434, 0.745, 0.752, 0.411, 0.741, 0.743, 0.388, 0.737, + 0.735, 0.365, 0.732, 0.726, 0.342, 0.728, 0.717, 0.318, 0.724, 0.709, + 0.293, 0.720, 0.700, 0.269, 0.716, 0.691, 0.243, 0.712, 0.683, 0.216, + 0.542, 0.673, 0.990, 0.554, 0.682, 0.985, 0.565, 0.691, 0.980, 0.577, + 0.700, 0.975, 0.588, 0.708, 0.970, 0.600, 0.717, 0.966, 0.611, 0.726, + 0.962, 0.623, 0.734, 0.958, 0.634, 0.743, 0.955, 0.646, 0.752, 0.951, + 0.657, 0.760, 0.948, 0.669, 0.769, 0.945, 0.681, 0.778, 0.943, 0.692, + 0.786, 0.940, 0.704, 0.795, 0.938, 0.716, 0.804, 0.936, 0.728, 0.812, + 0.934, 0.739, 0.821, 0.933, 0.751, 0.830, 0.932, 0.763, 0.838, 0.930, + 0.774, 0.847, 0.929, 0.786, 0.856, 0.929, 0.797, 0.864, 0.928, 0.809, + 0.873, 0.927, 0.819, 0.881, 0.927, 0.830, 0.889, 0.926, 0.840, 0.896, + 0.925, 0.850, 0.903, 0.924, 0.858, 0.910, 0.922, 0.865, 0.915, 0.920, + 0.871, 0.920, 0.916, 0.875, 0.923, 0.911, 0.876, 0.924, 0.903, 0.876, + 0.924, 0.894, 0.873, 0.922, 0.882, 0.870, 0.919, 0.867, 0.865, 0.914, + 0.851, 0.860, 0.909, 0.832, 0.854, 0.902, 0.813, 0.848, 0.895, 0.793, + 0.842, 0.888, 0.772, 0.836, 0.880, 0.750, 0.830, 0.872, 0.729, 0.824, + 0.864, 0.707, 0.819, 0.855, 0.684, 0.813, 0.847, 0.662, 0.808, 0.838, + 0.639, 0.802, 0.829, 0.617, 0.797, 0.820, 0.594, 0.792, 0.812, 0.571, + 0.787, 0.803, 0.549, 0.782, 0.794, 0.526, 0.777, 0.785, 0.503, 0.772, + 0.776, 0.480, 0.767, 0.768, 0.458, 0.763, 0.759, 0.435, 0.758, 0.750, + 0.412, 0.754, 0.741, 0.388, 0.749, 0.732, 0.365, 0.745, 0.724, 0.342, + 0.741, 0.715, 0.318, 0.737, 0.706, 0.293, 0.732, 0.697, 0.269, 0.728, + 0.689, 0.243, 0.724, 0.680, 0.216, 0.556, 0.671, 0.992, 0.567, 0.680, + 0.986, 0.578, 0.689, 0.981, 0.590, 0.697, 0.976, 0.601, 0.706, 0.972, + 0.612, 0.715, 0.968, 0.624, 0.724, 0.964, 0.635, 0.733, 0.960, 0.646, + 0.741, 0.956, 0.658, 0.750, 0.953, 0.670, 0.759, 0.950, 0.681, 0.768, + 0.947, 0.693, 0.777, 0.945, 0.704, 0.786, 0.943, 0.716, 0.794, 0.941, + 0.728, 0.803, 0.939, 0.740, 0.812, 0.937, 0.752, 0.821, 0.936, 0.763, + 0.830, 0.935, 0.775, 0.839, 0.934, 0.787, 0.848, 0.933, 0.799, 0.857, + 0.932, 0.811, 0.866, 0.932, 0.822, 0.875, 0.932, 0.834, 0.883, 0.932, + 0.845, 0.892, 0.931, 0.856, 0.900, 0.931, 0.866, 0.908, 0.931, 0.876, + 0.915, 0.930, 0.884, 0.922, 0.929, 0.890, 0.927, 0.926, 0.895, 0.930, + 0.921, 0.896, 0.932, 0.914, 0.896, 0.932, 0.905, 0.893, 0.929, 0.892, + 0.888, 0.925, 0.876, 0.883, 0.920, 0.859, 0.877, 0.913, 0.840, 0.871, + 0.906, 0.819, 0.864, 0.898, 0.798, 0.858, 0.890, 0.776, 0.852, 0.882, + 0.754, 0.845, 0.873, 0.732, 0.839, 0.864, 0.709, 0.833, 0.855, 0.686, + 0.828, 0.846, 0.664, 0.822, 0.837, 0.641, 0.816, 0.828, 0.618, 0.811, + 0.819, 0.595, 0.806, 0.810, 0.572, 0.800, 0.801, 0.549, 0.795, 0.792, + 0.526, 0.790, 0.783, 0.504, 0.785, 0.774, 0.481, 0.781, 0.765, 0.458, + 0.776, 0.756, 0.435, 0.771, 0.748, 0.412, 0.766, 0.739, 0.388, 0.762, + 0.730, 0.365, 0.757, 0.721, 0.341, 0.753, 0.712, 0.318, 0.749, 0.703, + 0.293, 0.744, 0.695, 0.268, 0.740, 0.686, 0.243, 0.736, 0.677, 0.216, + 0.569, 0.668, 0.993, 0.580, 0.677, 0.987, 0.591, 0.686, 0.982, 0.602, + 0.695, 0.978, 0.613, 0.704, 0.973, 0.624, 0.713, 0.969, 0.635, 0.722, + 0.965, 0.647, 0.731, 0.961, 0.658, 0.740, 0.958, 0.670, 0.748, 0.955, + 0.681, 0.757, 0.952, 0.693, 0.766, 0.949, 0.704, 0.775, 0.947, 0.716, + 0.784, 0.945, 0.728, 0.793, 0.943, 0.739, 0.802, 0.941, 0.751, 0.812, + 0.939, 0.763, 0.821, 0.938, 0.775, 0.830, 0.937, 0.787, 0.839, 0.936, + 0.799, 0.848, 0.936, 0.811, 0.858, 0.936, 0.823, 0.867, 0.935, 0.835, + 0.876, 0.936, 0.847, 0.886, 0.936, 0.859, 0.895, 0.936, 0.870, 0.904, + 0.937, 0.882, 0.912, 0.937, 0.892, 0.920, 0.937, 0.901, 0.928, 0.937, + 0.909, 0.934, 0.936, 0.914, 0.938, 0.932, 0.917, 0.940, 0.926, 0.915, + 0.940, 0.916, 0.912, 0.936, 0.902, 0.906, 0.931, 0.885, 0.900, 0.925, + 0.866, 0.893, 0.917, 0.846, 0.887, 0.909, 0.824, 0.880, 0.900, 0.802, + 0.873, 0.891, 0.780, 0.867, 0.882, 0.757, 0.860, 0.873, 0.734, 0.854, + 0.864, 0.711, 0.848, 0.855, 0.688, 0.842, 0.845, 0.665, 0.836, 0.836, + 0.642, 0.830, 0.827, 0.619, 0.824, 0.818, 0.596, 0.819, 0.808, 0.573, + 0.814, 0.799, 0.549, 0.808, 0.790, 0.527, 0.803, 0.781, 0.504, 0.798, + 0.772, 0.481, 0.793, 0.763, 0.458, 0.788, 0.754, 0.434, 0.783, 0.745, + 0.411, 0.779, 0.736, 0.388, 0.774, 0.727, 0.365, 0.769, 0.718, 0.341, + 0.765, 0.709, 0.317, 0.760, 0.700, 0.293, 0.756, 0.691, 0.268, 0.751, + 0.683, 0.242, 0.747, 0.674, 0.215, 0.581, 0.665, 0.994, 0.592, 0.674, + 0.989, 0.603, 0.683, 0.984, 0.614, 0.692, 0.979, 0.625, 0.701, 0.974, + 0.636, 0.710, 0.970, 0.647, 0.719, 0.966, 0.658, 0.728, 0.963, 0.669, + 0.737, 0.959, 0.681, 0.746, 0.956, 0.692, 0.755, 0.953, 0.703, 0.765, + 0.951, 0.715, 0.774, 0.948, 0.727, 0.783, 0.946, 0.738, 0.792, 0.944, + 0.750, 0.801, 0.943, 0.762, 0.810, 0.941, 0.774, 0.820, 0.940, 0.786, + 0.829, 0.939, 0.798, 0.839, 0.939, 0.810, 0.848, 0.938, 0.822, 0.858, + 0.938, 0.834, 0.867, 0.939, 0.847, 0.877, 0.939, 0.859, 0.887, 0.940, + 0.871, 0.897, 0.940, 0.883, 0.906, 0.942, 0.896, 0.916, 0.943, 0.907, + 0.925, 0.944, 0.918, 0.933, 0.945, 0.927, 0.941, 0.945, 0.934, 0.946, + 0.943, 0.937, 0.949, 0.937, 0.935, 0.948, 0.927, 0.930, 0.943, 0.912, + 0.924, 0.937, 0.893, 0.916, 0.929, 0.872, 0.909, 0.920, 0.850, 0.902, + 0.911, 0.828, 0.895, 0.901, 0.805, 0.888, 0.892, 0.782, 0.881, 0.882, + 0.759, 0.874, 0.872, 0.735, 0.868, 0.863, 0.712, 0.861, 0.853, 0.689, + 0.855, 0.844, 0.665, 0.849, 0.834, 0.642, 0.843, 0.825, 0.619, 0.838, + 0.815, 0.596, 0.832, 0.806, 0.572, 0.826, 0.797, 0.549, 0.821, 0.787, + 0.526, 0.816, 0.778, 0.503, 0.811, 0.769, 0.480, 0.805, 0.760, 0.457, + 0.800, 0.751, 0.434, 0.795, 0.742, 0.411, 0.791, 0.733, 0.387, 0.786, + 0.724, 0.364, 0.781, 0.715, 0.340, 0.776, 0.706, 0.316, 0.772, 0.697, + 0.292, 0.767, 0.688, 0.267, 0.762, 0.679, 0.241, 0.758, 0.670, 0.215, + 0.593, 0.662, 0.995, 0.603, 0.671, 0.990, 0.614, 0.680, 0.985, 0.625, + 0.689, 0.980, 0.636, 0.699, 0.975, 0.647, 0.708, 0.971, 0.658, 0.717, + 0.967, 0.669, 0.726, 0.964, 0.680, 0.735, 0.960, 0.691, 0.744, 0.957, + 0.702, 0.753, 0.955, 0.714, 0.762, 0.952, 0.725, 0.771, 0.950, 0.737, + 0.781, 0.948, 0.748, 0.790, 0.946, 0.760, 0.799, 0.944, 0.772, 0.809, + 0.943, 0.784, 0.818, 0.942, 0.796, 0.828, 0.941, 0.808, 0.837, 0.941, + 0.820, 0.847, 0.940, 0.832, 0.857, 0.941, 0.844, 0.867, 0.941, 0.857, + 0.877, 0.942, 0.869, 0.887, 0.943, 0.882, 0.897, 0.944, 0.895, 0.908, + 0.945, 0.908, 0.918, 0.947, 0.920, 0.928, 0.949, 0.933, 0.938, 0.951, + 0.944, 0.947, 0.953, 0.953, 0.955, 0.954, 0.957, 0.958, 0.950, 0.954, + 0.956, 0.938, 0.948, 0.949, 0.920, 0.940, 0.941, 0.899, 0.932, 0.931, + 0.877, 0.924, 0.921, 0.854, 0.916, 0.911, 0.830, 0.909, 0.901, 0.807, + 0.901, 0.891, 0.783, 0.894, 0.881, 0.759, 0.887, 0.871, 0.736, 0.881, + 0.861, 0.712, 0.874, 0.851, 0.688, 0.868, 0.841, 0.665, 0.862, 0.832, + 0.642, 0.856, 0.822, 0.618, 0.850, 0.812, 0.595, 0.844, 0.803, 0.572, + 0.839, 0.793, 0.549, 0.833, 0.784, 0.525, 0.828, 0.775, 0.502, 0.822, + 0.766, 0.479, 0.817, 0.756, 0.456, 0.812, 0.747, 0.433, 0.807, 0.738, + 0.410, 0.802, 0.729, 0.387, 0.797, 0.720, 0.363, 0.792, 0.711, 0.339, + 0.787, 0.702, 0.315, 0.782, 0.693, 0.291, 0.778, 0.684, 0.266, 0.773, + 0.675, 0.240, 0.768, 0.666, 0.214, 0.604, 0.659, 0.996, 0.614, 0.668, + 0.990, 0.625, 0.677, 0.985, 0.636, 0.686, 0.981, 0.646, 0.695, 0.976, + 0.657, 0.704, 0.972, 0.668, 0.714, 0.968, 0.679, 0.723, 0.965, 0.690, + 0.732, 0.961, 0.701, 0.741, 0.958, 0.712, 0.750, 0.956, 0.723, 0.759, + 0.953, 0.735, 0.769, 0.951, 0.746, 0.778, 0.949, 0.758, 0.787, 0.947, + 0.769, 0.797, 0.945, 0.781, 0.806, 0.944, 0.793, 0.816, 0.943, 0.805, + 0.826, 0.943, 0.817, 0.836, 0.942, 0.829, 0.845, 0.942, 0.841, 0.855, + 0.942, 0.853, 0.866, 0.943, 0.866, 0.876, 0.943, 0.879, 0.886, 0.945, + 0.892, 0.897, 0.946, 0.905, 0.907, 0.948, 0.918, 0.918, 0.950, 0.931, + 0.930, 0.953, 0.945, 0.941, 0.956, 0.958, 0.952, 0.960, 0.971, 0.963, + 0.963, 0.978, 0.968, 0.963, 0.972, 0.963, 0.948, 0.963, 0.954, 0.926, + 0.954, 0.943, 0.903, 0.945, 0.931, 0.879, 0.937, 0.921, 0.855, 0.929, + 0.910, 0.831, 0.921, 0.899, 0.807, 0.914, 0.889, 0.783, 0.907, 0.878, + 0.759, 0.900, 0.868, 0.735, 0.893, 0.858, 0.711, 0.887, 0.848, 0.688, + 0.880, 0.838, 0.664, 0.874, 0.828, 0.641, 0.868, 0.818, 0.617, 0.862, + 0.809, 0.594, 0.856, 0.799, 0.571, 0.850, 0.790, 0.547, 0.845, 0.780, + 0.524, 0.839, 0.771, 0.501, 0.834, 0.762, 0.478, 0.828, 0.752, 0.455, + 0.823, 0.743, 0.432, 0.818, 0.734, 0.409, 0.813, 0.725, 0.385, 0.808, + 0.716, 0.362, 0.803, 0.707, 0.338, 0.798, 0.698, 0.314, 0.793, 0.689, + 0.290, 0.788, 0.680, 0.265, 0.783, 0.671, 0.239, 0.778, 0.662, 0.213, + 0.615, 0.655, 0.996, 0.625, 0.664, 0.991, 0.635, 0.673, 0.986, 0.646, + 0.683, 0.982, 0.656, 0.692, 0.977, 0.667, 0.701, 0.973, 0.678, 0.710, + 0.969, 0.688, 0.719, 0.966, 0.699, 0.729, 0.962, 0.710, 0.738, 0.959, + 0.721, 0.747, 0.956, 0.732, 0.756, 0.954, 0.744, 0.766, 0.952, 0.755, + 0.775, 0.950, 0.766, 0.784, 0.948, 0.778, 0.794, 0.946, 0.789, 0.804, + 0.945, 0.801, 0.813, 0.944, 0.813, 0.823, 0.943, 0.825, 0.833, 0.943, + 0.837, 0.843, 0.943, 0.849, 0.853, 0.943, 0.861, 0.863, 0.944, 0.874, + 0.873, 0.944, 0.886, 0.884, 0.946, 0.899, 0.895, 0.947, 0.912, 0.906, + 0.949, 0.926, 0.917, 0.952, 0.939, 0.928, 0.955, 0.953, 0.940, 0.958, + 0.967, 0.953, 0.963, 0.982, 0.966, 0.969, 0.994, 0.976, 0.972, 0.986, + 0.966, 0.952, 0.976, 0.953, 0.928, 0.966, 0.941, 0.903, 0.957, 0.929, + 0.878, 0.949, 0.918, 0.854, 0.941, 0.906, 0.830, 0.933, 0.896, 0.806, + 0.926, 0.885, 0.782, 0.918, 0.874, 0.758, 0.911, 0.864, 0.734, 0.905, + 0.854, 0.710, 0.898, 0.844, 0.686, 0.892, 0.834, 0.663, 0.885, 0.824, + 0.639, 0.879, 0.814, 0.616, 0.873, 0.804, 0.592, 0.867, 0.795, 0.569, + 0.861, 0.785, 0.546, 0.856, 0.776, 0.523, 0.850, 0.766, 0.500, 0.845, + 0.757, 0.477, 0.839, 0.748, 0.454, 0.834, 0.739, 0.430, 0.829, 0.729, + 0.407, 0.823, 0.720, 0.384, 0.818, 0.711, 0.361, 0.813, 0.702, 0.337, + 0.808, 0.693, 0.313, 0.803, 0.684, 0.289, 0.798, 0.675, 0.264, 0.793, + 0.666, 0.238, 0.788, 0.657, 0.211, 0.625, 0.651, 0.997, 0.635, 0.660, + 0.992, 0.645, 0.669, 0.987, 0.656, 0.679, 0.982, 0.666, 0.688, 0.978, + 0.676, 0.697, 0.974, 0.687, 0.706, 0.970, 0.698, 0.716, 0.966, 0.708, + 0.725, 0.963, 0.719, 0.734, 0.960, 0.730, 0.743, 0.957, 0.741, 0.753, + 0.954, 0.752, 0.762, 0.952, 0.763, 0.771, 0.950, 0.774, 0.781, 0.948, + 0.786, 0.790, 0.947, 0.797, 0.800, 0.945, 0.809, 0.810, 0.945, 0.820, + 0.819, 0.944, 0.832, 0.829, 0.943, 0.844, 0.839, 0.943, 0.856, 0.849, + 0.943, 0.868, 0.860, 0.944, 0.880, 0.870, 0.945, 0.893, 0.880, 0.946, + 0.905, 0.891, 0.947, 0.918, 0.902, 0.949, 0.931, 0.913, 0.951, 0.944, + 0.924, 0.954, 0.957, 0.936, 0.957, 0.971, 0.947, 0.961, 0.983, 0.958, + 0.964, 0.991, 0.963, 0.962, 0.990, 0.957, 0.946, 0.983, 0.946, 0.924, + 0.975, 0.935, 0.900, 0.967, 0.923, 0.876, 0.959, 0.912, 0.851, 0.951, + 0.901, 0.827, 0.943, 0.890, 0.803, 0.936, 0.880, 0.779, 0.929, 0.869, + 0.755, 0.922, 0.859, 0.732, 0.915, 0.849, 0.708, 0.909, 0.839, 0.684, + 0.902, 0.829, 0.661, 0.896, 0.819, 0.637, 0.890, 0.809, 0.614, 0.884, + 0.800, 0.591, 0.878, 0.790, 0.567, 0.872, 0.780, 0.544, 0.866, 0.771, + 0.521, 0.861, 0.762, 0.498, 0.855, 0.752, 0.475, 0.850, 0.743, 0.452, + 0.844, 0.734, 0.429, 0.839, 0.725, 0.406, 0.834, 0.715, 0.382, 0.828, + 0.706, 0.359, 0.823, 0.697, 0.335, 0.818, 0.688, 0.311, 0.813, 0.679, + 0.287, 0.808, 0.670, 0.262, 0.803, 0.662, 0.236, 0.798, 0.653, 0.210, + 0.635, 0.646, 0.998, 0.645, 0.656, 0.992, 0.655, 0.665, 0.987, 0.665, + 0.674, 0.983, 0.675, 0.684, 0.978, 0.685, 0.693, 0.974, 0.696, 0.702, + 0.970, 0.706, 0.711, 0.966, 0.717, 0.721, 0.963, 0.727, 0.730, 0.960, + 0.738, 0.739, 0.957, 0.749, 0.748, 0.955, 0.760, 0.758, 0.952, 0.771, + 0.767, 0.950, 0.782, 0.777, 0.948, 0.793, 0.786, 0.947, 0.804, 0.796, + 0.946, 0.816, 0.805, 0.945, 0.827, 0.815, 0.944, 0.839, 0.825, 0.943, + 0.850, 0.835, 0.943, 0.862, 0.845, 0.943, 0.874, 0.855, 0.943, 0.886, + 0.865, 0.944, 0.898, 0.875, 0.945, 0.910, 0.886, 0.946, 0.923, 0.896, + 0.948, 0.935, 0.907, 0.949, 0.947, 0.917, 0.951, 0.959, 0.927, 0.953, + 0.970, 0.937, 0.955, 0.980, 0.944, 0.955, 0.987, 0.946, 0.949, 0.989, + 0.942, 0.936, 0.985, 0.935, 0.916, 0.980, 0.925, 0.894, 0.973, 0.915, + 0.871, 0.966, 0.904, 0.847, 0.959, 0.894, 0.824, 0.952, 0.883, 0.800, + 0.945, 0.873, 0.776, 0.938, 0.863, 0.752, 0.932, 0.853, 0.729, 0.925, + 0.843, 0.705, 0.919, 0.833, 0.682, 0.912, 0.823, 0.658, 0.906, 0.813, + 0.635, 0.900, 0.803, 0.611, 0.894, 0.794, 0.588, 0.888, 0.784, 0.565, + 0.882, 0.775, 0.542, 0.876, 0.765, 0.519, 0.871, 0.756, 0.496, 0.865, + 0.747, 0.473, 0.859, 0.738, 0.450, 0.854, 0.728, 0.427, 0.848, 0.719, + 0.404, 0.843, 0.710, 0.380, 0.838, 0.701, 0.357, 0.833, 0.692, 0.333, + 0.827, 0.683, 0.310, 0.822, 0.674, 0.285, 0.817, 0.665, 0.260, 0.812, + 0.656, 0.235, 0.807, 0.648, 0.208, 0.644, 0.642, 0.998, 0.654, 0.651, + 0.993, 0.664, 0.660, 0.988, 0.674, 0.670, 0.983, 0.684, 0.679, 0.978, + 0.694, 0.688, 0.974, 0.704, 0.697, 0.970, 0.715, 0.707, 0.967, 0.725, + 0.716, 0.963, 0.735, 0.725, 0.960, 0.746, 0.735, 0.957, 0.757, 0.744, + 0.955, 0.767, 0.753, 0.952, 0.778, 0.763, 0.950, 0.789, 0.772, 0.948, + 0.800, 0.781, 0.947, 0.811, 0.791, 0.945, 0.822, 0.801, 0.944, 0.833, + 0.810, 0.943, 0.845, 0.820, 0.943, 0.856, 0.830, 0.942, 0.868, 0.839, + 0.942, 0.879, 0.849, 0.942, 0.891, 0.859, 0.943, 0.902, 0.869, 0.943, + 0.914, 0.879, 0.944, 0.926, 0.889, 0.945, 0.937, 0.899, 0.946, 0.949, + 0.908, 0.947, 0.959, 0.917, 0.948, 0.969, 0.924, 0.947, 0.978, 0.929, + 0.944, 0.984, 0.930, 0.937, 0.986, 0.927, 0.924, 0.986, 0.921, 0.907, + 0.982, 0.913, 0.887, 0.978, 0.904, 0.865, 0.972, 0.895, 0.842, 0.966, + 0.885, 0.819, 0.959, 0.875, 0.795, 0.953, 0.865, 0.772, 0.946, 0.855, + 0.749, 0.940, 0.846, 0.725, 0.934, 0.836, 0.702, 0.927, 0.826, 0.678, + 0.921, 0.816, 0.655, 0.915, 0.807, 0.632, 0.909, 0.797, 0.609, 0.903, + 0.788, 0.586, 0.897, 0.778, 0.562, 0.891, 0.769, 0.539, 0.886, 0.759, + 0.516, 0.880, 0.750, 0.493, 0.874, 0.741, 0.471, 0.869, 0.732, 0.448, + 0.863, 0.723, 0.425, 0.858, 0.713, 0.402, 0.852, 0.704, 0.378, 0.847, + 0.695, 0.355, 0.842, 0.686, 0.331, 0.836, 0.677, 0.308, 0.831, 0.669, + 0.283, 0.826, 0.660, 0.258, 0.821, 0.651, 0.233, 0.815, 0.642, 0.206, + 0.653, 0.636, 0.998, 0.663, 0.646, 0.993, 0.673, 0.655, 0.988, 0.682, + 0.665, 0.983, 0.692, 0.674, 0.979, 0.702, 0.683, 0.974, 0.712, 0.692, + 0.970, 0.722, 0.702, 0.967, 0.733, 0.711, 0.963, 0.743, 0.720, 0.960, + 0.753, 0.730, 0.957, 0.764, 0.739, 0.954, 0.774, 0.748, 0.952, 0.785, + 0.757, 0.950, 0.796, 0.767, 0.948, 0.806, 0.776, 0.946, 0.817, 0.786, + 0.945, 0.828, 0.795, 0.943, 0.839, 0.804, 0.942, 0.850, 0.814, 0.941, + 0.861, 0.824, 0.941, 0.872, 0.833, 0.940, 0.884, 0.843, 0.940, 0.895, + 0.852, 0.940, 0.906, 0.862, 0.940, 0.917, 0.871, 0.941, 0.928, 0.880, + 0.941, 0.939, 0.889, 0.941, 0.949, 0.897, 0.941, 0.959, 0.904, 0.940, + 0.968, 0.910, 0.938, 0.975, 0.914, 0.933, 0.981, 0.914, 0.925, 0.984, + 0.912, 0.913, 0.985, 0.907, 0.897, 0.983, 0.900, 0.878, 0.980, 0.893, + 0.857, 0.976, 0.884, 0.836, 0.971, 0.875, 0.813, 0.965, 0.866, 0.790, + 0.959, 0.856, 0.767, 0.954, 0.847, 0.744, 0.947, 0.837, 0.721, 0.941, + 0.828, 0.698, 0.935, 0.818, 0.675, 0.929, 0.809, 0.652, 0.923, 0.800, + 0.629, 0.917, 0.790, 0.605, 0.912, 0.781, 0.582, 0.906, 0.771, 0.560, + 0.900, 0.762, 0.537, 0.894, 0.753, 0.514, 0.889, 0.744, 0.491, 0.883, + 0.735, 0.468, 0.877, 0.725, 0.445, 0.872, 0.716, 0.422, 0.866, 0.707, + 0.399, 0.861, 0.698, 0.376, 0.856, 0.689, 0.353, 0.850, 0.680, 0.329, + 0.845, 0.672, 0.305, 0.840, 0.663, 0.281, 0.834, 0.654, 0.256, 0.829, + 0.645, 0.231, 0.824, 0.636, 0.204, 0.662, 0.631, 0.998, 0.672, 0.641, + 0.993, 0.681, 0.650, 0.988, 0.691, 0.659, 0.983, 0.700, 0.669, 0.979, + 0.710, 0.678, 0.974, 0.720, 0.687, 0.970, 0.730, 0.696, 0.966, 0.740, + 0.706, 0.963, 0.750, 0.715, 0.960, 0.760, 0.724, 0.957, 0.771, 0.733, + 0.954, 0.781, 0.742, 0.951, 0.791, 0.752, 0.949, 0.802, 0.761, 0.947, + 0.812, 0.770, 0.945, 0.823, 0.779, 0.943, 0.834, 0.789, 0.942, 0.844, + 0.798, 0.941, 0.855, 0.807, 0.940, 0.866, 0.817, 0.939, 0.877, 0.826, + 0.938, 0.887, 0.835, 0.938, 0.898, 0.844, 0.937, 0.909, 0.853, 0.937, + 0.919, 0.862, 0.937, 0.930, 0.870, 0.936, 0.940, 0.878, 0.936, 0.950, + 0.885, 0.934, 0.958, 0.891, 0.932, 0.967, 0.896, 0.928, 0.974, 0.898, + 0.922, 0.979, 0.899, 0.914, 0.982, 0.897, 0.902, 0.984, 0.893, 0.886, + 0.984, 0.887, 0.869, 0.982, 0.880, 0.849, 0.979, 0.872, 0.828, 0.975, + 0.864, 0.807, 0.970, 0.856, 0.785, 0.965, 0.847, 0.762, 0.959, 0.838, + 0.739, 0.954, 0.829, 0.717, 0.948, 0.819, 0.694, 0.943, 0.810, 0.671, + 0.937, 0.801, 0.648, 0.931, 0.792, 0.625, 0.925, 0.782, 0.602, 0.919, + 0.773, 0.579, 0.914, 0.764, 0.556, 0.908, 0.755, 0.533, 0.902, 0.746, + 0.511, 0.897, 0.737, 0.488, 0.891, 0.728, 0.465, 0.886, 0.719, 0.442, + 0.880, 0.710, 0.420, 0.875, 0.701, 0.397, 0.869, 0.692, 0.374, 0.864, + 0.683, 0.350, 0.858, 0.674, 0.327, 0.853, 0.665, 0.303, 0.848, 0.657, + 0.279, 0.842, 0.648, 0.254, 0.837, 0.639, 0.229, 0.832, 0.630, 0.202, + 0.671, 0.625, 0.998, 0.680, 0.635, 0.993, 0.689, 0.644, 0.988, 0.698, + 0.654, 0.983, 0.708, 0.663, 0.978, 0.718, 0.672, 0.974, 0.727, 0.681, + 0.970, 0.737, 0.691, 0.966, 0.747, 0.700, 0.962, 0.757, 0.709, 0.959, + 0.767, 0.718, 0.956, 0.777, 0.727, 0.953, 0.787, 0.736, 0.950, 0.797, + 0.745, 0.948, 0.807, 0.755, 0.946, 0.818, 0.764, 0.944, 0.828, 0.773, + 0.942, 0.839, 0.782, 0.940, 0.849, 0.791, 0.939, 0.859, 0.800, 0.938, + 0.870, 0.809, 0.937, 0.880, 0.818, 0.936, 0.891, 0.827, 0.935, 0.901, + 0.835, 0.934, 0.911, 0.844, 0.933, 0.921, 0.852, 0.932, 0.931, 0.859, + 0.931, 0.941, 0.866, 0.929, 0.950, 0.873, 0.927, 0.958, 0.878, 0.923, + 0.966, 0.881, 0.919, 0.972, 0.883, 0.912, 0.977, 0.883, 0.903, 0.981, + 0.882, 0.891, 0.983, 0.878, 0.876, 0.984, 0.873, 0.859, 0.983, 0.867, + 0.841, 0.980, 0.860, 0.821, 0.977, 0.853, 0.800, 0.974, 0.845, 0.778, + 0.969, 0.836, 0.756, 0.964, 0.828, 0.734, 0.959, 0.819, 0.712, 0.954, + 0.810, 0.689, 0.949, 0.801, 0.666, 0.943, 0.792, 0.644, 0.938, 0.783, + 0.621, 0.932, 0.774, 0.598, 0.927, 0.765, 0.575, 0.921, 0.756, 0.553, + 0.916, 0.747, 0.530, 0.910, 0.738, 0.507, 0.904, 0.729, 0.485, 0.899, + 0.721, 0.462, 0.893, 0.712, 0.439, 0.888, 0.703, 0.417, 0.882, 0.694, + 0.394, 0.877, 0.685, 0.371, 0.871, 0.676, 0.348, 0.866, 0.668, 0.324, + 0.861, 0.659, 0.301, 0.855, 0.650, 0.277, 0.850, 0.641, 0.252, 0.845, + 0.633, 0.226, 0.839, 0.624, 0.200, 0.679, 0.619, 0.998, 0.688, 0.629, + 0.993, 0.697, 0.638, 0.988, 0.706, 0.648, 0.983, 0.715, 0.657, 0.978, + 0.725, 0.666, 0.974, 0.734, 0.675, 0.969, 0.744, 0.684, 0.965, 0.754, + 0.693, 0.962, 0.763, 0.703, 0.958, 0.773, 0.712, 0.955, 0.783, 0.721, + 0.952, 0.793, 0.730, 0.949, 0.803, 0.739, 0.947, 0.813, 0.748, 0.944, + 0.823, 0.757, 0.942, 0.833, 0.766, 0.940, 0.843, 0.774, 0.938, 0.853, + 0.783, 0.937, 0.863, 0.792, 0.935, 0.874, 0.801, 0.934, 0.884, 0.809, + 0.932, 0.894, 0.818, 0.931, 0.904, 0.826, 0.930, 0.913, 0.833, 0.928, + 0.923, 0.841, 0.927, 0.932, 0.848, 0.925, 0.941, 0.854, 0.922, 0.950, + 0.859, 0.919, 0.957, 0.864, 0.915, 0.965, 0.867, 0.909, 0.971, 0.868, + 0.901, 0.976, 0.868, 0.892, 0.980, 0.867, 0.880, 0.982, 0.863, 0.866, + 0.983, 0.859, 0.849, 0.983, 0.854, 0.832, 0.982, 0.847, 0.812, 0.979, + 0.840, 0.792, 0.976, 0.833, 0.771, 0.973, 0.825, 0.750, 0.969, 0.817, + 0.728, 0.964, 0.809, 0.706, 0.959, 0.800, 0.684, 0.954, 0.792, 0.661, + 0.949, 0.783, 0.639, 0.944, 0.774, 0.617, 0.939, 0.766, 0.594, 0.933, + 0.757, 0.572, 0.928, 0.748, 0.549, 0.922, 0.739, 0.527, 0.917, 0.731, + 0.504, 0.911, 0.722, 0.481, 0.906, 0.713, 0.459, 0.901, 0.704, 0.436, + 0.895, 0.695, 0.414, 0.890, 0.687, 0.391, 0.884, 0.678, 0.368, 0.879, + 0.669, 0.345, 0.873, 0.661, 0.322, 0.868, 0.652, 0.298, 0.863, 0.643, + 0.274, 0.857, 0.635, 0.249, 0.852, 0.626, 0.224, 0.846, 0.617, 0.197, + 0.686, 0.613, 0.998, 0.695, 0.622, 0.993, 0.704, 0.632, 0.987, 0.713, + 0.641, 0.982, 0.722, 0.650, 0.977, 0.732, 0.660, 0.973, 0.741, 0.669, + 0.969, 0.750, 0.678, 0.965, 0.760, 0.687, 0.961, 0.769, 0.696, 0.957, + 0.779, 0.705, 0.954, 0.789, 0.714, 0.951, 0.798, 0.723, 0.948, 0.808, + 0.731, 0.945, 0.818, 0.740, 0.943, 0.828, 0.749, 0.940, 0.838, 0.758, + 0.938, 0.847, 0.766, 0.936, 0.857, 0.775, 0.934, 0.867, 0.783, 0.932, + 0.877, 0.792, 0.930, 0.887, 0.800, 0.929, 0.896, 0.808, 0.927, 0.906, + 0.815, 0.925, 0.915, 0.823, 0.923, 0.924, 0.829, 0.921, 0.933, 0.836, + 0.918, 0.942, 0.841, 0.915, 0.950, 0.846, 0.911, 0.957, 0.850, 0.906, + 0.964, 0.852, 0.899, 0.970, 0.853, 0.891, 0.975, 0.853, 0.881, 0.979, + 0.852, 0.869, 0.981, 0.849, 0.855, 0.983, 0.845, 0.840, 0.983, 0.840, + 0.822, 0.982, 0.834, 0.804, 0.981, 0.828, 0.784, 0.978, 0.821, 0.764, + 0.975, 0.814, 0.743, 0.972, 0.806, 0.722, 0.968, 0.798, 0.700, 0.964, + 0.790, 0.678, 0.959, 0.782, 0.656, 0.954, 0.773, 0.634, 0.949, 0.765, + 0.612, 0.944, 0.757, 0.590, 0.939, 0.748, 0.567, 0.934, 0.739, 0.545, + 0.929, 0.731, 0.523, 0.923, 0.722, 0.500, 0.918, 0.714, 0.478, 0.913, + 0.705, 0.456, 0.907, 0.696, 0.433, 0.902, 0.688, 0.411, 0.896, 0.679, + 0.388, 0.891, 0.670, 0.365, 0.886, 0.662, 0.342, 0.880, 0.653, 0.319, + 0.875, 0.645, 0.295, 0.869, 0.636, 0.271, 0.864, 0.628, 0.247, 0.859, + 0.619, 0.221, 0.853, 0.611, 0.195, 0.694, 0.606, 0.998, 0.703, 0.616, + 0.992, 0.711, 0.625, 0.987, 0.720, 0.634, 0.982, 0.729, 0.644, 0.977, + 0.738, 0.653, 0.972, 0.747, 0.662, 0.968, 0.757, 0.671, 0.964, 0.766, + 0.680, 0.960, 0.775, 0.689, 0.956, 0.785, 0.698, 0.953, 0.794, 0.706, + 0.949, 0.804, 0.715, 0.946, 0.813, 0.724, 0.943, 0.823, 0.732, 0.941, + 0.832, 0.741, 0.938, 0.842, 0.749, 0.936, 0.851, 0.758, 0.933, 0.861, + 0.766, 0.931, 0.870, 0.774, 0.929, 0.880, 0.782, 0.927, 0.889, 0.790, + 0.924, 0.899, 0.797, 0.922, 0.908, 0.805, 0.920, 0.917, 0.811, 0.917, + 0.925, 0.818, 0.914, 0.934, 0.823, 0.911, 0.942, 0.828, 0.907, 0.950, + 0.832, 0.902, 0.957, 0.836, 0.896, 0.963, 0.838, 0.889, 0.969, 0.839, + 0.881, 0.974, 0.838, 0.871, 0.978, 0.837, 0.859, 0.980, 0.834, 0.845, + 0.982, 0.831, 0.830, 0.983, 0.826, 0.813, 0.983, 0.821, 0.795, 0.982, + 0.815, 0.776, 0.980, 0.809, 0.757, 0.978, 0.802, 0.736, 0.975, 0.794, + 0.715, 0.971, 0.787, 0.694, 0.967, 0.779, 0.673, 0.963, 0.771, 0.651, + 0.959, 0.763, 0.629, 0.954, 0.755, 0.607, 0.949, 0.747, 0.585, 0.944, + 0.739, 0.563, 0.939, 0.730, 0.541, 0.934, 0.722, 0.519, 0.929, 0.714, + 0.496, 0.924, 0.705, 0.474, 0.919, 0.697, 0.452, 0.913, 0.688, 0.430, + 0.908, 0.680, 0.407, 0.903, 0.671, 0.385, 0.897, 0.663, 0.362, 0.892, + 0.654, 0.339, 0.887, 0.646, 0.316, 0.881, 0.637, 0.293, 0.876, 0.629, + 0.269, 0.870, 0.620, 0.244, 0.865, 0.612, 0.219, 0.860, 0.604, 0.192, + 0.701, 0.599, 0.997, 0.710, 0.609, 0.992, 0.718, 0.618, 0.986, 0.727, + 0.627, 0.981, 0.736, 0.636, 0.976, 0.745, 0.645, 0.971, 0.754, 0.655, + 0.967, 0.763, 0.663, 0.963, 0.772, 0.672, 0.959, 0.781, 0.681, 0.955, + 0.790, 0.690, 0.951, 0.799, 0.699, 0.948, 0.808, 0.707, 0.944, 0.818, + 0.716, 0.941, 0.827, 0.724, 0.938, 0.836, 0.732, 0.935, 0.846, 0.741, + 0.933, 0.855, 0.749, 0.930, 0.864, 0.757, 0.928, 0.874, 0.765, 0.925, + 0.883, 0.772, 0.923, 0.892, 0.780, 0.920, 0.901, 0.787, 0.917, 0.910, + 0.793, 0.914, 0.918, 0.800, 0.911, 0.927, 0.805, 0.908, 0.935, 0.811, + 0.904, 0.942, 0.815, 0.899, 0.950, 0.819, 0.894, 0.956, 0.821, 0.887, + 0.963, 0.823, 0.880, 0.968, 0.824, 0.871, 0.973, 0.824, 0.860, 0.977, + 0.822, 0.849, 0.980, 0.820, 0.835, 0.982, 0.817, 0.820, 0.983, 0.812, + 0.804, 0.983, 0.808, 0.786, 0.983, 0.802, 0.768, 0.981, 0.796, 0.749, + 0.979, 0.789, 0.729, 0.977, 0.783, 0.709, 0.974, 0.776, 0.688, 0.970, + 0.768, 0.667, 0.967, 0.761, 0.645, 0.963, 0.753, 0.624, 0.958, 0.745, + 0.602, 0.954, 0.737, 0.580, 0.949, 0.729, 0.558, 0.944, 0.721, 0.536, + 0.939, 0.713, 0.514, 0.934, 0.704, 0.492, 0.929, 0.696, 0.470, 0.924, + 0.688, 0.448, 0.919, 0.680, 0.426, 0.914, 0.671, 0.404, 0.908, 0.663, + 0.381, 0.903, 0.655, 0.359, 0.898, 0.646, 0.336, 0.893, 0.638, 0.313, + 0.887, 0.629, 0.290, 0.882, 0.621, 0.266, 0.876, 0.613, 0.241, 0.871, + 0.604, 0.216, 0.866, 0.596, 0.189, 0.708, 0.592, 0.997, 0.716, 0.601, + 0.991, 0.725, 0.611, 0.985, 0.733, 0.620, 0.980, 0.742, 0.629, 0.975, + 0.751, 0.638, 0.970, 0.759, 0.647, 0.966, 0.768, 0.656, 0.961, 0.777, + 0.665, 0.957, 0.786, 0.673, 0.953, 0.795, 0.682, 0.949, 0.804, 0.690, + 0.946, 0.813, 0.699, 0.942, 0.822, 0.707, 0.939, 0.831, 0.715, 0.936, + 0.840, 0.723, 0.933, 0.849, 0.731, 0.930, 0.859, 0.739, 0.927, 0.868, + 0.747, 0.924, 0.876, 0.754, 0.921, 0.885, 0.762, 0.918, 0.894, 0.769, + 0.915, 0.903, 0.775, 0.912, 0.911, 0.782, 0.909, 0.920, 0.787, 0.905, + 0.928, 0.793, 0.901, 0.935, 0.798, 0.896, 0.943, 0.802, 0.891, 0.950, + 0.805, 0.885, 0.956, 0.807, 0.878, 0.962, 0.809, 0.870, 0.967, 0.809, + 0.861, 0.972, 0.809, 0.850, 0.976, 0.808, 0.838, 0.979, 0.805, 0.825, + 0.981, 0.802, 0.810, 0.983, 0.799, 0.795, 0.983, 0.794, 0.778, 0.983, + 0.789, 0.760, 0.982, 0.783, 0.741, 0.981, 0.777, 0.721, 0.979, 0.771, + 0.701, 0.976, 0.764, 0.681, 0.973, 0.757, 0.660, 0.970, 0.750, 0.639, + 0.966, 0.742, 0.618, 0.962, 0.735, 0.597, 0.958, 0.727, 0.575, 0.953, + 0.719, 0.553, 0.949, 0.711, 0.532, 0.944, 0.703, 0.510, 0.939, 0.695, + 0.488, 0.934, 0.687, 0.466, 0.929, 0.679, 0.444, 0.924, 0.671, 0.422, + 0.919, 0.663, 0.400, 0.914, 0.654, 0.378, 0.909, 0.646, 0.355, 0.903, + 0.638, 0.333, 0.898, 0.630, 0.310, 0.893, 0.621, 0.287, 0.887, 0.613, + 0.263, 0.882, 0.605, 0.238, 0.877, 0.597, 0.213, 0.871, 0.589, 0.186, + 0.715, 0.584, 0.996, 0.723, 0.593, 0.990, 0.731, 0.603, 0.984, 0.739, + 0.612, 0.979, 0.748, 0.621, 0.974, 0.756, 0.630, 0.969, 0.765, 0.639, + 0.964, 0.774, 0.648, 0.960, 0.782, 0.656, 0.955, 0.791, 0.665, 0.951, + 0.800, 0.673, 0.947, 0.809, 0.682, 0.943, 0.818, 0.690, 0.940, 0.826, + 0.698, 0.936, 0.835, 0.706, 0.933, 0.844, 0.714, 0.930, 0.853, 0.722, + 0.926, 0.862, 0.729, 0.923, 0.871, 0.737, 0.920, 0.879, 0.744, 0.917, + 0.888, 0.751, 0.913, 0.896, 0.758, 0.910, 0.905, 0.764, 0.906, 0.913, + 0.770, 0.903, 0.921, 0.775, 0.898, 0.929, 0.780, 0.894, 0.936, 0.784, + 0.889, 0.943, 0.788, 0.883, 0.950, 0.791, 0.877, 0.956, 0.793, 0.869, + 0.962, 0.794, 0.861, 0.967, 0.795, 0.851, 0.971, 0.794, 0.841, 0.975, + 0.793, 0.829, 0.978, 0.791, 0.815, 0.981, 0.788, 0.801, 0.982, 0.785, + 0.785, 0.983, 0.780, 0.769, 0.983, 0.776, 0.751, 0.983, 0.770, 0.733, + 0.982, 0.764, 0.714, 0.980, 0.758, 0.694, 0.978, 0.752, 0.674, 0.975, + 0.745, 0.654, 0.972, 0.738, 0.633, 0.969, 0.731, 0.612, 0.965, 0.724, + 0.591, 0.961, 0.716, 0.570, 0.957, 0.709, 0.548, 0.953, 0.701, 0.527, + 0.948, 0.693, 0.505, 0.943, 0.685, 0.484, 0.939, 0.678, 0.462, 0.934, + 0.670, 0.440, 0.929, 0.662, 0.418, 0.924, 0.654, 0.396, 0.919, 0.646, + 0.374, 0.914, 0.637, 0.352, 0.908, 0.629, 0.329, 0.903, 0.621, 0.307, + 0.898, 0.613, 0.283, 0.893, 0.605, 0.260, 0.887, 0.597, 0.235, 0.882, + 0.589, 0.210, 0.876, 0.581, 0.183, 0.721, 0.576, 0.995, 0.729, 0.585, + 0.989, 0.737, 0.595, 0.983, 0.745, 0.604, 0.978, 0.754, 0.613, 0.973, + 0.762, 0.622, 0.968, 0.770, 0.631, 0.963, 0.779, 0.639, 0.958, 0.787, + 0.648, 0.954, 0.796, 0.656, 0.949, 0.805, 0.665, 0.945, 0.813, 0.673, + 0.941, 0.822, 0.681, 0.937, 0.830, 0.689, 0.933, 0.839, 0.697, 0.930, + 0.848, 0.704, 0.926, 0.856, 0.712, 0.923, 0.865, 0.719, 0.919, 0.873, + 0.726, 0.916, 0.882, 0.733, 0.912, 0.890, 0.740, 0.908, 0.898, 0.746, + 0.905, 0.906, 0.752, 0.901, 0.914, 0.757, 0.896, 0.922, 0.762, 0.892, + 0.929, 0.767, 0.887, 0.937, 0.771, 0.881, 0.943, 0.774, 0.875, 0.950, + 0.777, 0.868, 0.956, 0.779, 0.860, 0.961, 0.780, 0.851, 0.966, 0.780, + 0.842, 0.971, 0.780, 0.831, 0.975, 0.779, 0.819, 0.978, 0.777, 0.806, + 0.980, 0.774, 0.791, 0.982, 0.771, 0.776, 0.983, 0.767, 0.760, 0.984, + 0.762, 0.742, 0.983, 0.757, 0.725, 0.983, 0.752, 0.706, 0.981, 0.746, + 0.687, 0.979, 0.740, 0.667, 0.977, 0.733, 0.647, 0.974, 0.727, 0.627, + 0.971, 0.720, 0.606, 0.968, 0.713, 0.585, 0.964, 0.706, 0.564, 0.960, + 0.698, 0.543, 0.956, 0.691, 0.522, 0.952, 0.683, 0.501, 0.947, 0.676, + 0.479, 0.943, 0.668, 0.458, 0.938, 0.660, 0.436, 0.933, 0.652, 0.414, + 0.928, 0.644, 0.392, 0.923, 0.636, 0.370, 0.918, 0.629, 0.348, 0.913, + 0.621, 0.326, 0.908, 0.613, 0.303, 0.903, 0.605, 0.280, 0.897, 0.597, + 0.257, 0.892, 0.589, 0.232, 0.887, 0.581, 0.207, 0.881, 0.573, 0.180, + 0.727, 0.568, 0.994, 0.735, 0.577, 0.988, 0.743, 0.586, 0.982, 0.751, + 0.595, 0.977, 0.759, 0.604, 0.971, 0.767, 0.613, 0.966, 0.776, 0.622, + 0.961, 0.784, 0.630, 0.956, 0.792, 0.639, 0.951, 0.801, 0.647, 0.947, + 0.809, 0.655, 0.943, 0.817, 0.663, 0.938, 0.826, 0.671, 0.934, 0.834, + 0.679, 0.930, 0.843, 0.687, 0.927, 0.851, 0.694, 0.923, 0.859, 0.702, + 0.919, 0.868, 0.709, 0.915, 0.876, 0.715, 0.911, 0.884, 0.722, 0.907, + 0.892, 0.728, 0.903, 0.900, 0.734, 0.899, 0.908, 0.740, 0.895, 0.916, + 0.745, 0.890, 0.923, 0.750, 0.885, 0.930, 0.754, 0.879, 0.937, 0.758, + 0.873, 0.944, 0.761, 0.867, 0.950, 0.763, 0.859, 0.956, 0.765, 0.851, + 0.961, 0.766, 0.842, 0.966, 0.766, 0.832, 0.970, 0.766, 0.821, 0.974, + 0.764, 0.809, 0.977, 0.762, 0.796, 0.980, 0.760, 0.782, 0.982, 0.757, + 0.767, 0.983, 0.753, 0.751, 0.984, 0.749, 0.734, 0.984, 0.744, 0.716, + 0.983, 0.739, 0.698, 0.982, 0.733, 0.679, 0.980, 0.727, 0.660, 0.978, + 0.721, 0.640, 0.976, 0.715, 0.620, 0.973, 0.708, 0.600, 0.970, 0.701, + 0.579, 0.967, 0.694, 0.559, 0.963, 0.687, 0.538, 0.959, 0.680, 0.517, + 0.955, 0.673, 0.496, 0.951, 0.665, 0.474, 0.946, 0.658, 0.453, 0.942, + 0.650, 0.432, 0.937, 0.643, 0.410, 0.932, 0.635, 0.388, 0.927, 0.627, + 0.367, 0.922, 0.619, 0.345, 0.917, 0.612, 0.322, 0.912, 0.604, 0.300, + 0.907, 0.596, 0.277, 0.902, 0.588, 0.253, 0.897, 0.580, 0.229, 0.891, + 0.572, 0.204, 0.886, 0.564, 0.177, 0.733, 0.559, 0.993, 0.741, 0.568, + 0.987, 0.749, 0.577, 0.981, 0.757, 0.587, 0.975, 0.765, 0.595, 0.970, + 0.773, 0.604, 0.964, 0.781, 0.613, 0.959, 0.789, 0.621, 0.954, 0.797, + 0.630, 0.949, 0.805, 0.638, 0.945, 0.813, 0.646, 0.940, 0.821, 0.654, + 0.936, 0.830, 0.662, 0.931, 0.838, 0.669, 0.927, 0.846, 0.677, 0.923, + 0.854, 0.684, 0.919, 0.862, 0.691, 0.915, 0.870, 0.698, 0.911, 0.879, + 0.704, 0.907, 0.886, 0.711, 0.902, 0.894, 0.717, 0.898, 0.902, 0.722, + 0.893, 0.910, 0.727, 0.889, 0.917, 0.732, 0.883, 0.924, 0.737, 0.878, + 0.931, 0.741, 0.872, 0.938, 0.744, 0.866, 0.944, 0.747, 0.859, 0.950, + 0.749, 0.851, 0.956, 0.751, 0.842, 0.961, 0.751, 0.833, 0.966, 0.752, + 0.823, 0.970, 0.751, 0.812, 0.974, 0.750, 0.800, 0.977, 0.748, 0.787, + 0.979, 0.746, 0.773, 0.981, 0.743, 0.758, 0.983, 0.739, 0.742, 0.984, + 0.735, 0.725, 0.984, 0.731, 0.708, 0.983, 0.726, 0.690, 0.983, 0.721, + 0.672, 0.981, 0.715, 0.653, 0.980, 0.709, 0.633, 0.977, 0.703, 0.614, + 0.975, 0.697, 0.594, 0.972, 0.690, 0.573, 0.969, 0.683, 0.553, 0.965, + 0.676, 0.532, 0.962, 0.669, 0.512, 0.958, 0.662, 0.491, 0.954, 0.655, + 0.470, 0.949, 0.648, 0.448, 0.945, 0.640, 0.427, 0.941, 0.633, 0.406, + 0.936, 0.625, 0.384, 0.931, 0.618, 0.363, 0.926, 0.610, 0.341, 0.921, + 0.602, 0.319, 0.916, 0.595, 0.296, 0.911, 0.587, 0.273, 0.906, 0.579, + 0.250, 0.901, 0.571, 0.226, 0.896, 0.563, 0.201, 0.890, 0.556, 0.174, + 0.739, 0.550, 0.992, 0.747, 0.559, 0.986, 0.754, 0.568, 0.980, 0.762, + 0.577, 0.974, 0.770, 0.586, 0.968, 0.778, 0.595, 0.962, 0.785, 0.603, + 0.957, 0.793, 0.612, 0.952, 0.801, 0.620, 0.947, 0.809, 0.628, 0.942, + 0.817, 0.636, 0.937, 0.825, 0.644, 0.933, 0.833, 0.651, 0.928, 0.841, + 0.659, 0.924, 0.849, 0.666, 0.919, 0.857, 0.673, 0.915, 0.865, 0.680, + 0.911, 0.873, 0.686, 0.906, 0.881, 0.693, 0.902, 0.889, 0.699, 0.897, + 0.896, 0.705, 0.892, 0.904, 0.710, 0.887, 0.911, 0.715, 0.882, 0.918, + 0.720, 0.877, 0.925, 0.724, 0.871, 0.932, 0.727, 0.865, 0.938, 0.730, + 0.858, 0.944, 0.733, 0.850, 0.950, 0.735, 0.842, 0.956, 0.736, 0.834, + 0.961, 0.737, 0.824, 0.965, 0.737, 0.814, 0.970, 0.737, 0.802, 0.973, + 0.736, 0.790, 0.976, 0.734, 0.777, 0.979, 0.732, 0.763, 0.981, 0.729, + 0.749, 0.982, 0.726, 0.733, 0.983, 0.722, 0.717, 0.984, 0.717, 0.700, + 0.984, 0.713, 0.682, 0.983, 0.708, 0.664, 0.982, 0.702, 0.645, 0.980, + 0.697, 0.626, 0.979, 0.691, 0.607, 0.976, 0.685, 0.587, 0.974, 0.678, + 0.567, 0.971, 0.672, 0.547, 0.967, 0.665, 0.527, 0.964, 0.658, 0.506, + 0.960, 0.651, 0.485, 0.956, 0.644, 0.465, 0.952, 0.637, 0.444, 0.948, + 0.630, 0.423, 0.944, 0.623, 0.401, 0.939, 0.615, 0.380, 0.934, 0.608, + 0.358, 0.930, 0.600, 0.337, 0.925, 0.593, 0.315, 0.920, 0.585, 0.292, + 0.915, 0.578, 0.270, 0.910, 0.570, 0.246, 0.905, 0.562, 0.222, 0.899, + 0.555, 0.197, 0.894, 0.547, 0.171, 0.745, 0.540, 0.991, 0.752, 0.550, + 0.984, 0.760, 0.559, 0.978, 0.767, 0.568, 0.972, 0.775, 0.577, 0.966, + 0.782, 0.585, 0.960, 0.790, 0.594, 0.955, 0.798, 0.602, 0.950, 0.806, + 0.610, 0.944, 0.813, 0.618, 0.939, 0.821, 0.626, 0.934, 0.829, 0.634, + 0.930, 0.837, 0.641, 0.925, 0.845, 0.648, 0.920, 0.852, 0.655, 0.915, + 0.860, 0.662, 0.911, 0.868, 0.669, 0.906, 0.876, 0.675, 0.901, 0.883, + 0.681, 0.897, 0.891, 0.687, 0.892, 0.898, 0.692, 0.887, 0.905, 0.697, + 0.881, 0.912, 0.702, 0.876, 0.919, 0.707, 0.870, 0.926, 0.710, 0.864, + 0.932, 0.714, 0.857, 0.939, 0.717, 0.850, 0.945, 0.719, 0.842, 0.950, + 0.721, 0.834, 0.956, 0.722, 0.825, 0.961, 0.723, 0.815, 0.965, 0.723, + 0.804, 0.969, 0.723, 0.793, 0.973, 0.722, 0.781, 0.976, 0.720, 0.768, + 0.978, 0.718, 0.754, 0.981, 0.715, 0.739, 0.982, 0.712, 0.724, 0.983, + 0.708, 0.708, 0.984, 0.704, 0.691, 0.984, 0.700, 0.674, 0.983, 0.695, + 0.656, 0.982, 0.690, 0.638, 0.981, 0.684, 0.619, 0.979, 0.679, 0.600, + 0.977, 0.673, 0.581, 0.975, 0.666, 0.561, 0.972, 0.660, 0.541, 0.969, + 0.654, 0.521, 0.966, 0.647, 0.501, 0.962, 0.640, 0.480, 0.959, 0.634, + 0.459, 0.955, 0.627, 0.439, 0.951, 0.620, 0.418, 0.946, 0.612, 0.397, + 0.942, 0.605, 0.376, 0.937, 0.598, 0.354, 0.933, 0.591, 0.333, 0.928, + 0.583, 0.311, 0.923, 0.576, 0.289, 0.918, 0.568, 0.266, 0.913, 0.561, + 0.243, 0.908, 0.553, 0.219, 0.903, 0.546, 0.194, 0.898, 0.538, 0.167, + 0.750, 0.531, 0.989, 0.757, 0.540, 0.983, 0.765, 0.549, 0.976, 0.772, + 0.558, 0.970, 0.779, 0.567, 0.964, 0.787, 0.575, 0.958, 0.794, 0.584, + 0.953, 0.802, 0.592, 0.947, 0.810, 0.600, 0.942, 0.817, 0.608, 0.936, + 0.825, 0.615, 0.931, 0.833, 0.623, 0.926, 0.840, 0.630, 0.921, 0.848, + 0.637, 0.916, 0.855, 0.644, 0.911, 0.863, 0.651, 0.907, 0.870, 0.657, + 0.902, 0.878, 0.663, 0.897, 0.885, 0.669, 0.891, 0.893, 0.675, 0.886, + 0.900, 0.680, 0.881, 0.907, 0.685, 0.875, 0.914, 0.689, 0.869, 0.920, + 0.693, 0.863, 0.927, 0.697, 0.857, 0.933, 0.700, 0.850, 0.939, 0.703, + 0.842, 0.945, 0.705, 0.834, 0.950, 0.707, 0.825, 0.956, 0.708, 0.816, + 0.960, 0.709, 0.806, 0.965, 0.709, 0.795, 0.969, 0.708, 0.784, 0.972, + 0.707, 0.772, 0.975, 0.706, 0.759, 0.978, 0.704, 0.745, 0.980, 0.701, + 0.731, 0.982, 0.698, 0.715, 0.983, 0.694, 0.699, 0.984, 0.691, 0.683, + 0.984, 0.686, 0.666, 0.983, 0.682, 0.648, 0.983, 0.677, 0.630, 0.982, + 0.672, 0.612, 0.980, 0.666, 0.593, 0.978, 0.660, 0.574, 0.976, 0.655, + 0.554, 0.973, 0.648, 0.535, 0.971, 0.642, 0.515, 0.967, 0.636, 0.495, + 0.964, 0.629, 0.475, 0.961, 0.623, 0.454, 0.957, 0.616, 0.434, 0.953, + 0.609, 0.413, 0.949, 0.602, 0.392, 0.944, 0.595, 0.371, 0.940, 0.588, + 0.350, 0.936, 0.580, 0.328, 0.931, 0.573, 0.307, 0.926, 0.566, 0.285, + 0.921, 0.559, 0.262, 0.916, 0.551, 0.239, 0.911, 0.544, 0.215, 0.906, + 0.536, 0.190, 0.901, 0.529, 0.164, 0.755, 0.521, 0.988, 0.762, 0.530, + 0.981, 0.770, 0.539, 0.975, 0.777, 0.548, 0.968, 0.784, 0.557, 0.962, + 0.791, 0.565, 0.956, 0.799, 0.573, 0.950, 0.806, 0.582, 0.944, 0.814, + 0.589, 0.939, 0.821, 0.597, 0.933, 0.828, 0.605, 0.928, 0.836, 0.612, + 0.923, 0.843, 0.619, 0.918, 0.851, 0.626, 0.912, 0.858, 0.633, 0.907, + 0.866, 0.639, 0.902, 0.873, 0.645, 0.897, 0.880, 0.651, 0.892, 0.887, + 0.657, 0.886, 0.894, 0.662, 0.881, 0.901, 0.667, 0.875, 0.908, 0.672, + 0.869, 0.915, 0.676, 0.863, 0.921, 0.680, 0.856, 0.928, 0.684, 0.849, + 0.934, 0.687, 0.842, 0.940, 0.689, 0.834, 0.945, 0.691, 0.826, 0.951, + 0.693, 0.817, 0.956, 0.694, 0.807, 0.960, 0.695, 0.797, 0.964, 0.695, + 0.787, 0.968, 0.694, 0.775, 0.972, 0.693, 0.763, 0.975, 0.692, 0.750, + 0.977, 0.690, 0.736, 0.980, 0.687, 0.722, 0.981, 0.684, 0.707, 0.982, + 0.681, 0.691, 0.983, 0.677, 0.675, 0.984, 0.673, 0.658, 0.983, 0.669, + 0.641, 0.983, 0.664, 0.623, 0.982, 0.659, 0.605, 0.980, 0.654, 0.586, + 0.979, 0.648, 0.567, 0.977, 0.642, 0.548, 0.974, 0.637, 0.529, 0.972, + 0.630, 0.509, 0.969, 0.624, 0.489, 0.966, 0.618, 0.469, 0.962, 0.611, + 0.449, 0.959, 0.605, 0.429, 0.955, 0.598, 0.408, 0.951, 0.591, 0.387, + 0.947, 0.584, 0.367, 0.942, 0.577, 0.346, 0.938, 0.570, 0.324, 0.933, + 0.563, 0.303, 0.929, 0.556, 0.281, 0.924, 0.549, 0.258, 0.919, 0.541, + 0.235, 0.914, 0.534, 0.212, 0.909, 0.527, 0.187, 0.904, 0.519, 0.160, + 0.760, 0.510, 0.986, 0.767, 0.520, 0.979, 0.774, 0.529, 0.973, 0.781, + 0.538, 0.966, 0.788, 0.546, 0.960, 0.796, 0.555, 0.954, 0.803, 0.563, + 0.948, 0.810, 0.571, 0.942, 0.817, 0.579, 0.936, 0.825, 0.586, 0.930, + 0.832, 0.594, 0.925, 0.839, 0.601, 0.919, 0.846, 0.608, 0.914, 0.854, + 0.615, 0.908, 0.861, 0.621, 0.903, 0.868, 0.627, 0.897, 0.875, 0.633, + 0.892, 0.882, 0.639, 0.886, 0.889, 0.645, 0.881, 0.896, 0.650, 0.875, + 0.903, 0.655, 0.869, 0.909, 0.659, 0.863, 0.916, 0.663, 0.856, 0.922, + 0.667, 0.849, 0.928, 0.670, 0.842, 0.934, 0.673, 0.834, 0.940, 0.675, + 0.826, 0.945, 0.677, 0.818, 0.951, 0.679, 0.809, 0.955, 0.680, 0.799, + 0.960, 0.680, 0.789, 0.964, 0.680, 0.778, 0.968, 0.680, 0.766, 0.971, + 0.679, 0.754, 0.974, 0.677, 0.741, 0.977, 0.676, 0.727, 0.979, 0.673, + 0.713, 0.981, 0.670, 0.698, 0.982, 0.667, 0.682, 0.983, 0.664, 0.666, + 0.983, 0.660, 0.650, 0.983, 0.655, 0.633, 0.983, 0.651, 0.615, 0.982, + 0.646, 0.597, 0.981, 0.641, 0.579, 0.979, 0.636, 0.560, 0.977, 0.630, + 0.541, 0.975, 0.625, 0.522, 0.973, 0.619, 0.503, 0.970, 0.613, 0.483, + 0.967, 0.606, 0.463, 0.964, 0.600, 0.443, 0.960, 0.594, 0.423, 0.956, + 0.587, 0.403, 0.953, 0.580, 0.383, 0.949, 0.574, 0.362, 0.944, 0.567, + 0.341, 0.940, 0.560, 0.320, 0.936, 0.553, 0.298, 0.931, 0.546, 0.277, + 0.926, 0.539, 0.254, 0.922, 0.532, 0.232, 0.917, 0.524, 0.208, 0.912, + 0.517, 0.183, 0.906, 0.510, 0.156, 0.765, 0.499, 0.985, 0.772, 0.509, + 0.978, 0.779, 0.518, 0.971, 0.786, 0.527, 0.964, 0.793, 0.535, 0.957, + 0.800, 0.544, 0.951, 0.807, 0.552, 0.945, 0.814, 0.560, 0.939, 0.821, + 0.568, 0.933, 0.828, 0.575, 0.927, 0.835, 0.582, 0.921, 0.842, 0.589, + 0.915, 0.849, 0.596, 0.910, 0.856, 0.603, 0.904, 0.863, 0.609, 0.898, + 0.870, 0.615, 0.893, 0.877, 0.621, 0.887, 0.884, 0.627, 0.881, 0.891, + 0.632, 0.875, 0.898, 0.637, 0.869, 0.904, 0.642, 0.863, 0.911, 0.646, + 0.856, 0.917, 0.650, 0.849, 0.923, 0.653, 0.842, 0.929, 0.657, 0.835, + 0.935, 0.659, 0.827, 0.940, 0.662, 0.818, 0.946, 0.663, 0.810, 0.951, + 0.665, 0.800, 0.955, 0.666, 0.790, 0.960, 0.666, 0.780, 0.964, 0.666, + 0.769, 0.967, 0.666, 0.757, 0.971, 0.665, 0.745, 0.974, 0.663, 0.732, + 0.976, 0.662, 0.718, 0.978, 0.659, 0.704, 0.980, 0.657, 0.689, 0.981, + 0.654, 0.674, 0.982, 0.650, 0.658, 0.983, 0.646, 0.642, 0.983, 0.642, + 0.625, 0.983, 0.638, 0.608, 0.982, 0.633, 0.590, 0.981, 0.628, 0.572, + 0.979, 0.623, 0.553, 0.978, 0.618, 0.535, 0.976, 0.612, 0.516, 0.973, + 0.607, 0.497, 0.971, 0.601, 0.477, 0.968, 0.595, 0.458, 0.965, 0.589, + 0.438, 0.961, 0.582, 0.418, 0.958, 0.576, 0.398, 0.954, 0.569, 0.378, + 0.950, 0.563, 0.357, 0.946, 0.556, 0.336, 0.942, 0.549, 0.315, 0.938, + 0.542, 0.294, 0.933, 0.536, 0.273, 0.928, 0.529, 0.250, 0.924, 0.522, + 0.228, 0.919, 0.514, 0.204, 0.914, 0.507, 0.179, 0.909, 0.500, 0.153, + 0.770, 0.488, 0.983, 0.777, 0.498, 0.976, 0.783, 0.507, 0.969, 0.790, + 0.516, 0.962, 0.797, 0.524, 0.955, 0.804, 0.533, 0.948, 0.811, 0.541, + 0.942, 0.817, 0.549, 0.936, 0.824, 0.556, 0.930, 0.831, 0.564, 0.924, + 0.838, 0.571, 0.918, 0.845, 0.578, 0.912, 0.852, 0.584, 0.906, 0.859, + 0.591, 0.900, 0.866, 0.597, 0.894, 0.873, 0.603, 0.888, 0.879, 0.609, + 0.882, 0.886, 0.614, 0.876, 0.893, 0.619, 0.869, 0.899, 0.624, 0.863, + 0.906, 0.629, 0.856, 0.912, 0.633, 0.850, 0.918, 0.637, 0.843, 0.924, + 0.640, 0.835, 0.930, 0.643, 0.827, 0.935, 0.646, 0.819, 0.941, 0.648, + 0.811, 0.946, 0.649, 0.802, 0.951, 0.651, 0.792, 0.955, 0.652, 0.782, + 0.959, 0.652, 0.771, 0.963, 0.652, 0.760, 0.967, 0.652, 0.749, 0.970, + 0.651, 0.736, 0.973, 0.649, 0.723, 0.976, 0.647, 0.710, 0.978, 0.645, + 0.696, 0.980, 0.643, 0.681, 0.981, 0.640, 0.666, 0.982, 0.637, 0.650, + 0.982, 0.633, 0.634, 0.983, 0.629, 0.617, 0.982, 0.625, 0.600, 0.982, + 0.620, 0.582, 0.981, 0.616, 0.565, 0.979, 0.611, 0.546, 0.978, 0.605, + 0.528, 0.976, 0.600, 0.509, 0.974, 0.595, 0.490, 0.971, 0.589, 0.471, + 0.969, 0.583, 0.452, 0.966, 0.577, 0.432, 0.962, 0.571, 0.413, 0.959, + 0.565, 0.393, 0.955, 0.558, 0.373, 0.952, 0.552, 0.352, 0.948, 0.545, + 0.332, 0.943, 0.539, 0.311, 0.939, 0.532, 0.290, 0.935, 0.525, 0.268, + 0.930, 0.518, 0.246, 0.926, 0.511, 0.224, 0.921, 0.504, 0.200, 0.916, + 0.497, 0.175, 0.911, 0.490, 0.149, 0.775, 0.477, 0.981, 0.781, 0.486, + 0.974, 0.788, 0.495, 0.966, 0.794, 0.504, 0.959, 0.801, 0.513, 0.952, + 0.808, 0.521, 0.946, 0.814, 0.529, 0.939, 0.821, 0.537, 0.933, 0.828, + 0.545, 0.926, 0.835, 0.552, 0.920, 0.841, 0.559, 0.914, 0.848, 0.566, + 0.908, 0.855, 0.572, 0.901, 0.862, 0.579, 0.895, 0.868, 0.585, 0.889, + 0.875, 0.591, 0.883, 0.881, 0.596, 0.877, 0.888, 0.601, 0.870, 0.894, + 0.606, 0.864, 0.901, 0.611, 0.857, 0.907, 0.615, 0.850, 0.913, 0.619, + 0.843, 0.919, 0.623, 0.836, 0.925, 0.626, 0.828, 0.930, 0.629, 0.820, + 0.936, 0.632, 0.812, 0.941, 0.634, 0.803, 0.946, 0.635, 0.794, 0.951, + 0.637, 0.784, 0.955, 0.637, 0.774, 0.959, 0.638, 0.763, 0.963, 0.638, + 0.752, 0.967, 0.637, 0.740, 0.970, 0.636, 0.728, 0.973, 0.635, 0.715, + 0.975, 0.633, 0.701, 0.977, 0.631, 0.687, 0.979, 0.629, 0.672, 0.980, + 0.626, 0.657, 0.981, 0.623, 0.642, 0.982, 0.619, 0.626, 0.982, 0.616, + 0.609, 0.982, 0.612, 0.592, 0.981, 0.607, 0.575, 0.981, 0.603, 0.557, + 0.979, 0.598, 0.540, 0.978, 0.593, 0.521, 0.976, 0.588, 0.503, 0.974, + 0.582, 0.484, 0.972, 0.577, 0.465, 0.969, 0.571, 0.446, 0.966, 0.565, + 0.427, 0.963, 0.559, 0.407, 0.960, 0.553, 0.387, 0.956, 0.547, 0.368, + 0.953, 0.541, 0.347, 0.949, 0.534, 0.327, 0.945, 0.528, 0.306, 0.941, + 0.521, 0.285, 0.936, 0.514, 0.264, 0.932, 0.508, 0.242, 0.927, 0.501, + 0.220, 0.922, 0.494, 0.196, 0.918, 0.487, 0.172, 0.913, 0.480, 0.145, + 0.779, 0.465, 0.979, 0.785, 0.474, 0.971, 0.792, 0.484, 0.964, 0.798, + 0.492, 0.957, 0.805, 0.501, 0.950, 0.811, 0.509, 0.943, 0.818, 0.517, + 0.936, 0.824, 0.525, 0.929, 0.831, 0.533, 0.923, 0.838, 0.540, 0.916, + 0.844, 0.547, 0.910, 0.851, 0.554, 0.904, 0.857, 0.560, 0.897, 0.864, + 0.566, 0.891, 0.870, 0.572, 0.884, 0.877, 0.578, 0.878, 0.883, 0.583, + 0.871, 0.890, 0.589, 0.865, 0.896, 0.593, 0.858, 0.902, 0.598, 0.851, + 0.908, 0.602, 0.844, 0.914, 0.606, 0.837, 0.920, 0.609, 0.829, 0.925, + 0.613, 0.821, 0.931, 0.615, 0.813, 0.936, 0.618, 0.804, 0.941, 0.620, + 0.795, 0.946, 0.621, 0.786, 0.950, 0.622, 0.776, 0.955, 0.623, 0.765, + 0.959, 0.624, 0.755, 0.963, 0.624, 0.743, 0.966, 0.623, 0.731, 0.969, + 0.622, 0.719, 0.972, 0.621, 0.706, 0.974, 0.619, 0.693, 0.976, 0.617, + 0.679, 0.978, 0.615, 0.664, 0.979, 0.612, 0.649, 0.980, 0.609, 0.634, + 0.981, 0.606, 0.618, 0.981, 0.602, 0.601, 0.981, 0.598, 0.585, 0.981, + 0.594, 0.568, 0.980, 0.590, 0.550, 0.979, 0.585, 0.533, 0.978, 0.580, + 0.515, 0.976, 0.575, 0.496, 0.974, 0.570, 0.478, 0.972, 0.565, 0.459, + 0.969, 0.559, 0.440, 0.967, 0.553, 0.421, 0.964, 0.547, 0.402, 0.960, + 0.542, 0.382, 0.957, 0.535, 0.362, 0.953, 0.529, 0.342, 0.950, 0.523, + 0.322, 0.946, 0.517, 0.302, 0.942, 0.510, 0.281, 0.937, 0.504, 0.260, + 0.933, 0.497, 0.238, 0.929, 0.490, 0.216, 0.924, 0.484, 0.192, 0.919, + 0.477, 0.168, 0.914, 0.470, 0.141, 0.783, 0.453, 0.977, 0.789, 0.462, + 0.969, 0.796, 0.471, 0.962, 0.802, 0.480, 0.954, 0.808, 0.489, 0.947, + 0.815, 0.497, 0.940, 0.821, 0.505, 0.933, 0.828, 0.513, 0.926, 0.834, + 0.520, 0.919, 0.840, 0.527, 0.913, 0.847, 0.534, 0.906, 0.853, 0.541, + 0.899, 0.860, 0.548, 0.893, 0.866, 0.554, 0.886, 0.873, 0.560, 0.880, + 0.879, 0.565, 0.873, 0.885, 0.570, 0.866, 0.891, 0.575, 0.859, 0.897, + 0.580, 0.852, 0.903, 0.585, 0.845, 0.909, 0.589, 0.838, 0.915, 0.592, + 0.830, 0.921, 0.596, 0.822, 0.926, 0.599, 0.814, 0.931, 0.601, 0.805, + 0.936, 0.604, 0.797, 0.941, 0.606, 0.787, 0.946, 0.607, 0.778, 0.950, + 0.608, 0.768, 0.955, 0.609, 0.757, 0.958, 0.609, 0.746, 0.962, 0.609, + 0.735, 0.965, 0.609, 0.723, 0.968, 0.608, 0.710, 0.971, 0.607, 0.698, + 0.974, 0.605, 0.684, 0.976, 0.603, 0.670, 0.977, 0.601, 0.656, 0.979, + 0.598, 0.641, 0.980, 0.596, 0.626, 0.980, 0.592, 0.610, 0.981, 0.589, + 0.594, 0.981, 0.585, 0.577, 0.980, 0.581, 0.560, 0.980, 0.577, 0.543, + 0.979, 0.572, 0.526, 0.977, 0.568, 0.508, 0.976, 0.563, 0.490, 0.974, + 0.558, 0.471, 0.972, 0.552, 0.453, 0.969, 0.547, 0.434, 0.967, 0.541, + 0.415, 0.964, 0.536, 0.396, 0.961, 0.530, 0.377, 0.958, 0.524, 0.357, + 0.954, 0.518, 0.337, 0.950, 0.512, 0.317, 0.947, 0.505, 0.297, 0.943, + 0.499, 0.276, 0.938, 0.493, 0.255, 0.934, 0.486, 0.234, 0.930, 0.480, + 0.211, 0.925, 0.473, 0.188, 0.920, 0.466, 0.164, 0.916, 0.459, 0.137, + 0.787, 0.440, 0.975, 0.793, 0.450, 0.967, 0.799, 0.459, 0.959, 0.806, + 0.468, 0.952, 0.812, 0.476, 0.944, 0.818, 0.485, 0.937, 0.824, 0.493, + 0.930, 0.831, 0.500, 0.923, 0.837, 0.508, 0.916, 0.843, 0.515, 0.909, + 0.850, 0.522, 0.902, 0.856, 0.528, 0.895, 0.862, 0.535, 0.888, 0.868, + 0.541, 0.881, 0.874, 0.547, 0.875, 0.881, 0.552, 0.868, 0.887, 0.557, + 0.861, 0.893, 0.562, 0.853, 0.899, 0.567, 0.846, 0.904, 0.571, 0.839, + 0.910, 0.575, 0.831, 0.916, 0.579, 0.823, 0.921, 0.582, 0.815, 0.926, + 0.585, 0.807, 0.932, 0.587, 0.798, 0.937, 0.590, 0.789, 0.941, 0.591, + 0.780, 0.946, 0.593, 0.770, 0.950, 0.594, 0.760, 0.954, 0.595, 0.749, + 0.958, 0.595, 0.738, 0.962, 0.595, 0.727, 0.965, 0.595, 0.715, 0.968, + 0.594, 0.702, 0.970, 0.593, 0.689, 0.973, 0.591, 0.676, 0.975, 0.589, + 0.662, 0.976, 0.587, 0.648, 0.978, 0.585, 0.633, 0.979, 0.582, 0.618, + 0.980, 0.579, 0.602, 0.980, 0.575, 0.586, 0.980, 0.572, 0.570, 0.980, + 0.568, 0.553, 0.979, 0.564, 0.536, 0.978, 0.559, 0.518, 0.977, 0.555, + 0.501, 0.975, 0.550, 0.483, 0.974, 0.545, 0.465, 0.972, 0.540, 0.447, + 0.969, 0.535, 0.428, 0.967, 0.529, 0.409, 0.964, 0.524, 0.390, 0.961, + 0.518, 0.371, 0.958, 0.512, 0.352, 0.954, 0.506, 0.332, 0.951, 0.500, + 0.312, 0.947, 0.494, 0.292, 0.943, 0.488, 0.272, 0.939, 0.481, 0.251, + 0.935, 0.475, 0.229, 0.931, 0.469, 0.207, 0.926, 0.462, 0.184, 0.921, + 0.455, 0.159, 0.917, 0.449, 0.133, 0.791, 0.427, 0.973, 0.797, 0.437, + 0.964, 0.803, 0.446, 0.957, 0.809, 0.455, 0.949, 0.815, 0.464, 0.941, + 0.821, 0.472, 0.934, 0.827, 0.480, 0.926, 0.834, 0.488, 0.919, 0.840, + 0.495, 0.912, 0.846, 0.502, 0.905, 0.852, 0.509, 0.898, 0.858, 0.515, + 0.891, 0.864, 0.522, 0.884, 0.870, 0.528, 0.877, 0.876, 0.533, 0.870, + 0.882, 0.539, 0.862, 0.888, 0.544, 0.855, 0.894, 0.549, 0.848, 0.900, + 0.553, 0.840, 0.906, 0.557, 0.833, 0.911, 0.561, 0.825, 0.916, 0.565, + 0.817, 0.922, 0.568, 0.808, 0.927, 0.571, 0.800, 0.932, 0.573, 0.791, + 0.937, 0.575, 0.782, 0.941, 0.577, 0.772, 0.946, 0.579, 0.762, 0.950, + 0.580, 0.752, 0.954, 0.580, 0.741, 0.958, 0.581, 0.730, 0.961, 0.581, + 0.718, 0.964, 0.580, 0.706, 0.967, 0.580, 0.694, 0.970, 0.578, 0.681, + 0.972, 0.577, 0.667, 0.974, 0.575, 0.654, 0.976, 0.573, 0.639, 0.977, + 0.571, 0.625, 0.978, 0.568, 0.610, 0.979, 0.565, 0.594, 0.979, 0.562, + 0.578, 0.979, 0.558, 0.562, 0.979, 0.554, 0.545, 0.978, 0.550, 0.529, + 0.977, 0.546, 0.511, 0.976, 0.542, 0.494, 0.975, 0.537, 0.476, 0.973, + 0.532, 0.458, 0.971, 0.527, 0.440, 0.969, 0.522, 0.422, 0.967, 0.517, + 0.403, 0.964, 0.511, 0.385, 0.961, 0.506, 0.366, 0.958, 0.500, 0.347, + 0.955, 0.494, 0.327, 0.951, 0.488, 0.307, 0.947, 0.482, 0.287, 0.944, + 0.476, 0.267, 0.940, 0.470, 0.246, 0.935, 0.464, 0.225, 0.931, 0.458, + 0.203, 0.927, 0.451, 0.180, 0.922, 0.445, 0.155, 0.917, 0.438, 0.129, + 0.795, 0.413, 0.970, 0.801, 0.423, 0.962, 0.807, 0.433, 0.954, 0.813, + 0.442, 0.946, 0.818, 0.450, 0.938, 0.824, 0.459, 0.930, 0.830, 0.467, + 0.923, 0.836, 0.474, 0.915, 0.842, 0.482, 0.908, 0.848, 0.489, 0.901, + 0.854, 0.496, 0.894, 0.860, 0.502, 0.886, 0.866, 0.508, 0.879, 0.872, + 0.514, 0.872, 0.878, 0.520, 0.864, 0.884, 0.525, 0.857, 0.890, 0.530, + 0.850, 0.895, 0.535, 0.842, 0.901, 0.539, 0.834, 0.906, 0.543, 0.826, + 0.912, 0.547, 0.818, 0.917, 0.551, 0.810, 0.922, 0.554, 0.801, 0.927, + 0.557, 0.793, 0.932, 0.559, 0.783, 0.937, 0.561, 0.774, 0.941, 0.563, + 0.764, 0.946, 0.564, 0.754, 0.950, 0.565, 0.744, 0.953, 0.566, 0.733, + 0.957, 0.566, 0.722, 0.960, 0.566, 0.710, 0.963, 0.566, 0.698, 0.966, + 0.565, 0.685, 0.969, 0.564, 0.673, 0.971, 0.563, 0.659, 0.973, 0.561, + 0.645, 0.975, 0.559, 0.631, 0.976, 0.557, 0.617, 0.977, 0.554, 0.602, + 0.978, 0.551, 0.586, 0.978, 0.548, 0.571, 0.978, 0.545, 0.554, 0.978, + 0.541, 0.538, 0.977, 0.537, 0.521, 0.977, 0.533, 0.504, 0.976, 0.529, + 0.487, 0.974, 0.524, 0.470, 0.973, 0.519, 0.452, 0.971, 0.515, 0.434, + 0.969, 0.510, 0.416, 0.966, 0.504, 0.398, 0.964, 0.499, 0.379, 0.961, + 0.494, 0.360, 0.958, 0.488, 0.341, 0.955, 0.482, 0.322, 0.951, 0.477, + 0.302, 0.948, 0.471, 0.283, 0.944, 0.465, 0.262, 0.940, 0.459, 0.242, + 0.936, 0.452, 0.220, 0.932, 0.446, 0.198, 0.927, 0.440, 0.175, 0.923, + 0.434, 0.151, 0.918, 0.427, 0.124, 0.799, 0.399, 0.968, 0.804, 0.409, + 0.959, 0.810, 0.419, 0.951, 0.816, 0.428, 0.943, 0.822, 0.437, 0.935, + 0.827, 0.445, 0.927, 0.833, 0.453, 0.919, 0.839, 0.461, 0.912, 0.845, + 0.468, 0.904, 0.851, 0.475, 0.897, 0.857, 0.482, 0.889, 0.862, 0.489, + 0.882, 0.868, 0.495, 0.874, 0.874, 0.501, 0.867, 0.880, 0.506, 0.859, + 0.885, 0.511, 0.852, 0.891, 0.516, 0.844, 0.897, 0.521, 0.836, 0.902, + 0.525, 0.828, 0.907, 0.529, 0.820, 0.913, 0.533, 0.812, 0.918, 0.537, + 0.803, 0.923, 0.540, 0.794, 0.928, 0.542, 0.785, 0.932, 0.545, 0.776, + 0.937, 0.547, 0.767, 0.941, 0.549, 0.757, 0.945, 0.550, 0.747, 0.949, + 0.551, 0.736, 0.953, 0.552, 0.725, 0.956, 0.552, 0.714, 0.960, 0.552, + 0.702, 0.963, 0.552, 0.690, 0.965, 0.551, 0.677, 0.968, 0.550, 0.664, + 0.970, 0.549, 0.651, 0.972, 0.547, 0.637, 0.974, 0.545, 0.623, 0.975, + 0.543, 0.609, 0.976, 0.540, 0.594, 0.977, 0.537, 0.578, 0.977, 0.534, + 0.563, 0.977, 0.531, 0.547, 0.977, 0.527, 0.531, 0.976, 0.524, 0.514, + 0.976, 0.520, 0.497, 0.975, 0.516, 0.480, 0.973, 0.511, 0.463, 0.972, + 0.507, 0.446, 0.970, 0.502, 0.428, 0.968, 0.497, 0.410, 0.966, 0.492, + 0.392, 0.963, 0.487, 0.373, 0.960, 0.481, 0.355, 0.957, 0.476, 0.336, + 0.954, 0.470, 0.317, 0.951, 0.465, 0.297, 0.947, 0.459, 0.278, 0.944, + 0.453, 0.258, 0.940, 0.447, 0.237, 0.936, 0.441, 0.216, 0.932, 0.435, + 0.194, 0.927, 0.429, 0.171, 0.923, 0.422, 0.147, 0.918, 0.416, 0.120, + 0.802, 0.385, 0.965, 0.808, 0.395, 0.957, 0.813, 0.405, 0.948, 0.819, + 0.414, 0.940, 0.825, 0.423, 0.932, 0.830, 0.431, 0.924, 0.836, 0.439, + 0.916, 0.842, 0.447, 0.908, 0.847, 0.454, 0.900, 0.853, 0.462, 0.892, + 0.859, 0.468, 0.885, 0.864, 0.475, 0.877, 0.870, 0.481, 0.869, 0.876, + 0.487, 0.862, 0.881, 0.492, 0.854, 0.887, 0.498, 0.846, 0.892, 0.502, + 0.838, 0.898, 0.507, 0.830, 0.903, 0.511, 0.822, 0.908, 0.515, 0.814, + 0.913, 0.519, 0.805, 0.918, 0.522, 0.797, 0.923, 0.525, 0.788, 0.928, + 0.528, 0.778, 0.932, 0.530, 0.769, 0.937, 0.532, 0.759, 0.941, 0.534, + 0.749, 0.945, 0.535, 0.739, 0.949, 0.536, 0.728, 0.952, 0.537, 0.717, + 0.956, 0.537, 0.706, 0.959, 0.537, 0.694, 0.962, 0.537, 0.682, 0.964, + 0.536, 0.669, 0.967, 0.536, 0.656, 0.969, 0.534, 0.643, 0.971, 0.533, + 0.629, 0.972, 0.531, 0.615, 0.974, 0.529, 0.601, 0.975, 0.526, 0.586, + 0.975, 0.523, 0.571, 0.976, 0.521, 0.555, 0.976, 0.517, 0.540, 0.976, + 0.514, 0.523, 0.975, 0.510, 0.507, 0.975, 0.506, 0.490, 0.974, 0.502, + 0.474, 0.972, 0.498, 0.456, 0.971, 0.494, 0.439, 0.969, 0.489, 0.421, + 0.967, 0.484, 0.404, 0.965, 0.479, 0.386, 0.963, 0.474, 0.367, 0.960, + 0.469, 0.349, 0.957, 0.464, 0.330, 0.954, 0.458, 0.311, 0.951, 0.453, + 0.292, 0.947, 0.447, 0.273, 0.944, 0.441, 0.253, 0.940, 0.435, 0.232, + 0.936, 0.429, 0.211, 0.932, 0.423, 0.190, 0.927, 0.417, 0.167, 0.923, + 0.411, 0.142, 0.919, 0.405, 0.116, 0.806, 0.370, 0.963, 0.811, 0.380, + 0.954, 0.816, 0.390, 0.945, 0.822, 0.399, 0.937, 0.827, 0.408, 0.929, + 0.833, 0.417, 0.920, 0.838, 0.425, 0.912, 0.844, 0.433, 0.904, 0.850, + 0.440, 0.896, 0.855, 0.447, 0.888, 0.861, 0.454, 0.880, 0.866, 0.461, + 0.872, 0.872, 0.467, 0.865, 0.877, 0.473, 0.857, 0.883, 0.478, 0.849, + 0.888, 0.483, 0.841, 0.893, 0.488, 0.833, 0.899, 0.493, 0.824, 0.904, + 0.497, 0.816, 0.909, 0.501, 0.807, 0.914, 0.505, 0.799, 0.919, 0.508, + 0.790, 0.923, 0.511, 0.781, 0.928, 0.514, 0.771, 0.932, 0.516, 0.762, + 0.937, 0.518, 0.752, 0.941, 0.520, 0.742, 0.945, 0.521, 0.731, 0.948, + 0.522, 0.720, 0.952, 0.523, 0.709, 0.955, 0.523, 0.698, 0.958, 0.523, + 0.686, 0.961, 0.523, 0.674, 0.964, 0.522, 0.661, 0.966, 0.521, 0.648, + 0.968, 0.520, 0.635, 0.970, 0.518, 0.621, 0.971, 0.517, 0.607, 0.972, + 0.514, 0.593, 0.973, 0.512, 0.578, 0.974, 0.509, 0.563, 0.975, 0.507, + 0.548, 0.975, 0.503, 0.532, 0.975, 0.500, 0.516, 0.974, 0.497, 0.500, + 0.974, 0.493, 0.483, 0.973, 0.489, 0.467, 0.971, 0.485, 0.450, 0.970, + 0.480, 0.433, 0.968, 0.476, 0.415, 0.966, 0.471, 0.397, 0.964, 0.466, + 0.380, 0.962, 0.461, 0.362, 0.959, 0.456, 0.343, 0.956, 0.451, 0.325, + 0.953, 0.446, 0.306, 0.950, 0.440, 0.287, 0.947, 0.435, 0.268, 0.943, + 0.429, 0.248, 0.939, 0.423, 0.228, 0.936, 0.417, 0.207, 0.931, 0.411, + 0.185, 0.927, 0.405, 0.162, 0.923, 0.399, 0.138, 0.918, 0.393, 0.111, + 0.809, 0.354, 0.960, 0.814, 0.364, 0.951, 0.819, 0.375, 0.942, 0.825, + 0.384, 0.934, 0.830, 0.393, 0.925, 0.835, 0.402, 0.917, 0.841, 0.410, + 0.908, 0.846, 0.418, 0.900, 0.852, 0.426, 0.892, 0.857, 0.433, 0.884, + 0.863, 0.440, 0.876, 0.868, 0.446, 0.868, 0.873, 0.452, 0.860, 0.879, + 0.458, 0.852, 0.884, 0.464, 0.843, 0.889, 0.469, 0.835, 0.894, 0.474, + 0.827, 0.899, 0.478, 0.818, 0.904, 0.483, 0.810, 0.909, 0.486, 0.801, + 0.914, 0.490, 0.792, 0.919, 0.493, 0.783, 0.923, 0.496, 0.774, 0.928, + 0.499, 0.764, 0.932, 0.501, 0.755, 0.936, 0.503, 0.745, 0.940, 0.505, + 0.734, 0.944, 0.506, 0.724, 0.948, 0.507, 0.713, 0.951, 0.508, 0.701, + 0.954, 0.508, 0.690, 0.957, 0.508, 0.678, 0.960, 0.508, 0.666, 0.962, + 0.507, 0.653, 0.965, 0.507, 0.640, 0.967, 0.505, 0.627, 0.968, 0.504, + 0.613, 0.970, 0.502, 0.599, 0.971, 0.500, 0.585, 0.972, 0.498, 0.570, + 0.973, 0.495, 0.555, 0.973, 0.493, 0.540, 0.973, 0.490, 0.525, 0.973, + 0.486, 0.509, 0.973, 0.483, 0.493, 0.972, 0.479, 0.476, 0.971, 0.475, + 0.460, 0.970, 0.471, 0.443, 0.969, 0.467, 0.426, 0.967, 0.463, 0.409, + 0.965, 0.458, 0.391, 0.963, 0.453, 0.374, 0.961, 0.449, 0.356, 0.958, + 0.444, 0.338, 0.956, 0.438, 0.319, 0.953, 0.433, 0.301, 0.949, 0.428, + 0.282, 0.946, 0.422, 0.263, 0.943, 0.417, 0.243, 0.939, 0.411, 0.223, + 0.935, 0.405, 0.202, 0.931, 0.399, 0.181, 0.927, 0.393, 0.158, 0.923, + 0.387, 0.134, 0.918, 0.381, 0.107, + ]).reshape((65, 65, 3)) + +BiOrangeBlue = np.array( + [0.000, 0.000, 0.000, 0.000, 0.062, 0.125, 0.000, 0.125, 0.250, 0.000, + 0.188, 0.375, 0.000, 0.250, 0.500, 0.000, 0.312, 0.625, 0.000, 0.375, + 0.750, 0.000, 0.438, 0.875, 0.000, 0.500, 1.000, 0.125, 0.062, 0.000, + 0.125, 0.125, 0.125, 0.125, 0.188, 0.250, 0.125, 0.250, 0.375, 0.125, + 0.312, 0.500, 0.125, 0.375, 0.625, 0.125, 0.438, 0.750, 0.125, 0.500, + 0.875, 0.125, 0.562, 1.000, 0.250, 0.125, 0.000, 0.250, 0.188, 0.125, + 0.250, 0.250, 0.250, 0.250, 0.312, 0.375, 0.250, 0.375, 0.500, 0.250, + 0.438, 0.625, 0.250, 0.500, 0.750, 0.250, 0.562, 0.875, 0.250, 0.625, + 1.000, 0.375, 0.188, 0.000, 0.375, 0.250, 0.125, 0.375, 0.312, 0.250, + 0.375, 0.375, 0.375, 0.375, 0.438, 0.500, 0.375, 0.500, 0.625, 0.375, + 0.562, 0.750, 0.375, 0.625, 0.875, 0.375, 0.688, 1.000, 0.500, 0.250, + 0.000, 0.500, 0.312, 0.125, 0.500, 0.375, 0.250, 0.500, 0.438, 0.375, + 0.500, 0.500, 0.500, 0.500, 0.562, 0.625, 0.500, 0.625, 0.750, 0.500, + 0.688, 0.875, 0.500, 0.750, 1.000, 0.625, 0.312, 0.000, 0.625, 0.375, + 0.125, 0.625, 0.438, 0.250, 0.625, 0.500, 0.375, 0.625, 0.562, 0.500, + 0.625, 0.625, 0.625, 0.625, 0.688, 0.750, 0.625, 0.750, 0.875, 0.625, + 0.812, 1.000, 0.750, 0.375, 0.000, 0.750, 0.438, 0.125, 0.750, 0.500, + 0.250, 0.750, 0.562, 0.375, 0.750, 0.625, 0.500, 0.750, 0.688, 0.625, + 0.750, 0.750, 0.750, 0.750, 0.812, 0.875, 0.750, 0.875, 1.000, 0.875, + 0.438, 0.000, 0.875, 0.500, 0.125, 0.875, 0.562, 0.250, 0.875, 0.625, + 0.375, 0.875, 0.688, 0.500, 0.875, 0.750, 0.625, 0.875, 0.812, 0.750, + 0.875, 0.875, 0.875, 0.875, 0.938, 1.000, 1.000, 0.500, 0.000, 1.000, + 0.562, 0.125, 1.000, 0.625, 0.250, 1.000, 0.688, 0.375, 1.000, 0.750, + 0.500, 1.000, 0.812, 0.625, 1.000, 0.875, 0.750, 1.000, 0.938, 0.875, + 1.000, 1.000, 1.000, + ]).reshape((9, 9, 3)) + +cmaps = { + "BiPeak": SegmentedBivarColormap( + BiPeak, 256, "square", (.5, .5), name="BiPeak"), + "BiOrangeBlue": SegmentedBivarColormap( + BiOrangeBlue, 256, "square", (0, 0), name="BiOrangeBlue"), + "BiCone": SegmentedBivarColormap(BiPeak, 256, "circle", (.5, .5), name="BiCone"), +} diff --git a/lib/matplotlib/_cm_multivar.py b/lib/matplotlib/_cm_multivar.py new file mode 100644 index 000000000000..610d7c40935b --- /dev/null +++ b/lib/matplotlib/_cm_multivar.py @@ -0,0 +1,166 @@ +# auto-generated by https://github.com/trygvrad/multivariate_colormaps +# date: 2024-05-28 + +from .colors import LinearSegmentedColormap, MultivarColormap +import matplotlib as mpl +_LUTSIZE = mpl.rcParams['image.lut'] + +_2VarAddA0_data = [[0.000, 0.000, 0.000], + [0.020, 0.026, 0.031], + [0.049, 0.068, 0.085], + [0.075, 0.107, 0.135], + [0.097, 0.144, 0.183], + [0.116, 0.178, 0.231], + [0.133, 0.212, 0.279], + [0.148, 0.244, 0.326], + [0.161, 0.276, 0.374], + [0.173, 0.308, 0.422], + [0.182, 0.339, 0.471], + [0.190, 0.370, 0.521], + [0.197, 0.400, 0.572], + [0.201, 0.431, 0.623], + [0.204, 0.461, 0.675], + [0.204, 0.491, 0.728], + [0.202, 0.520, 0.783], + [0.197, 0.549, 0.838], + [0.187, 0.577, 0.895]] + +_2VarAddA1_data = [[0.000, 0.000, 0.000], + [0.030, 0.023, 0.018], + [0.079, 0.060, 0.043], + [0.125, 0.093, 0.065], + [0.170, 0.123, 0.083], + [0.213, 0.151, 0.098], + [0.255, 0.177, 0.110], + [0.298, 0.202, 0.120], + [0.341, 0.226, 0.128], + [0.384, 0.249, 0.134], + [0.427, 0.271, 0.138], + [0.472, 0.292, 0.141], + [0.517, 0.313, 0.142], + [0.563, 0.333, 0.141], + [0.610, 0.353, 0.139], + [0.658, 0.372, 0.134], + [0.708, 0.390, 0.127], + [0.759, 0.407, 0.118], + [0.813, 0.423, 0.105]] + +_2VarSubA0_data = [[1.000, 1.000, 1.000], + [0.959, 0.973, 0.986], + [0.916, 0.948, 0.974], + [0.874, 0.923, 0.965], + [0.832, 0.899, 0.956], + [0.790, 0.875, 0.948], + [0.748, 0.852, 0.940], + [0.707, 0.829, 0.934], + [0.665, 0.806, 0.927], + [0.624, 0.784, 0.921], + [0.583, 0.762, 0.916], + [0.541, 0.740, 0.910], + [0.500, 0.718, 0.905], + [0.457, 0.697, 0.901], + [0.414, 0.675, 0.896], + [0.369, 0.652, 0.892], + [0.320, 0.629, 0.888], + [0.266, 0.604, 0.884], + [0.199, 0.574, 0.881]] + +_2VarSubA1_data = [[1.000, 1.000, 1.000], + [0.982, 0.967, 0.955], + [0.966, 0.935, 0.908], + [0.951, 0.902, 0.860], + [0.937, 0.870, 0.813], + [0.923, 0.838, 0.765], + [0.910, 0.807, 0.718], + [0.898, 0.776, 0.671], + [0.886, 0.745, 0.624], + [0.874, 0.714, 0.577], + [0.862, 0.683, 0.530], + [0.851, 0.653, 0.483], + [0.841, 0.622, 0.435], + [0.831, 0.592, 0.388], + [0.822, 0.561, 0.340], + [0.813, 0.530, 0.290], + [0.806, 0.498, 0.239], + [0.802, 0.464, 0.184], + [0.801, 0.426, 0.119]] + +_3VarAddA0_data = [[0.000, 0.000, 0.000], + [0.018, 0.023, 0.028], + [0.040, 0.056, 0.071], + [0.059, 0.087, 0.110], + [0.074, 0.114, 0.147], + [0.086, 0.139, 0.183], + [0.095, 0.163, 0.219], + [0.101, 0.187, 0.255], + [0.105, 0.209, 0.290], + [0.107, 0.230, 0.326], + [0.105, 0.251, 0.362], + [0.101, 0.271, 0.398], + [0.091, 0.291, 0.434], + [0.075, 0.309, 0.471], + [0.046, 0.325, 0.507], + [0.021, 0.341, 0.546], + [0.021, 0.363, 0.584], + [0.022, 0.385, 0.622], + [0.023, 0.408, 0.661]] + +_3VarAddA1_data = [[0.000, 0.000, 0.000], + [0.020, 0.024, 0.016], + [0.047, 0.058, 0.034], + [0.072, 0.088, 0.048], + [0.093, 0.116, 0.059], + [0.113, 0.142, 0.067], + [0.131, 0.167, 0.071], + [0.149, 0.190, 0.074], + [0.166, 0.213, 0.074], + [0.182, 0.235, 0.072], + [0.198, 0.256, 0.068], + [0.215, 0.276, 0.061], + [0.232, 0.296, 0.051], + [0.249, 0.314, 0.037], + [0.270, 0.330, 0.018], + [0.288, 0.347, 0.000], + [0.302, 0.369, 0.000], + [0.315, 0.391, 0.000], + [0.328, 0.414, 0.000]] + +_3VarAddA2_data = [[0.000, 0.000, 0.000], + [0.029, 0.020, 0.023], + [0.072, 0.045, 0.055], + [0.111, 0.067, 0.084], + [0.148, 0.085, 0.109], + [0.184, 0.101, 0.133], + [0.219, 0.115, 0.155], + [0.254, 0.127, 0.176], + [0.289, 0.138, 0.195], + [0.323, 0.147, 0.214], + [0.358, 0.155, 0.232], + [0.393, 0.161, 0.250], + [0.429, 0.166, 0.267], + [0.467, 0.169, 0.283], + [0.507, 0.168, 0.298], + [0.546, 0.168, 0.313], + [0.580, 0.172, 0.328], + [0.615, 0.175, 0.341], + [0.649, 0.178, 0.355]] + +cmaps = { + name: LinearSegmentedColormap.from_list(name, data, _LUTSIZE) for name, data in [ + ('2VarAddA0', _2VarAddA0_data), + ('2VarAddA1', _2VarAddA1_data), + ('2VarSubA0', _2VarSubA0_data), + ('2VarSubA1', _2VarSubA1_data), + ('3VarAddA0', _3VarAddA0_data), + ('3VarAddA1', _3VarAddA1_data), + ('3VarAddA2', _3VarAddA2_data), + ]} + +cmap_families = { + '2VarAddA': MultivarColormap([cmaps[f'2VarAddA{i}'] for i in range(2)], + 'sRGB_add', name='2VarAddA'), + '2VarSubA': MultivarColormap([cmaps[f'2VarSubA{i}'] for i in range(2)], + 'sRGB_sub', name='2VarSubA'), + '3VarAddA': MultivarColormap([cmaps[f'3VarAddA{i}'] for i in range(3)], + 'sRGB_add', name='3VarAddA'), +} diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index f5bc455df1f7..025cb84db1d7 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -24,6 +24,8 @@ from matplotlib import _api, colors, cbook, scale from matplotlib._cm import datad from matplotlib._cm_listed import cmaps as cmaps_listed +from matplotlib._cm_multivar import cmap_families as multivar_cmaps +from matplotlib._cm_bivar import cmaps as bivar_cmaps _LUTSIZE = mpl.rcParams['image.lut'] @@ -238,6 +240,10 @@ def get_cmap(self, cmap): _colormaps = ColormapRegistry(_gen_cmap_registry()) globals().update(_colormaps) +_multivar_colormaps = ColormapRegistry(multivar_cmaps) + +_bivar_colormaps = ColormapRegistry(bivar_cmaps) + # This is an exact copy of pyplot.get_cmap(). It was removed in 3.9, but apparently # caused more user trouble than expected. Re-added for 3.9.1 and extended the diff --git a/lib/matplotlib/cm.pyi b/lib/matplotlib/cm.pyi index be8f10b39cb6..40e841d829ab 100644 --- a/lib/matplotlib/cm.pyi +++ b/lib/matplotlib/cm.pyi @@ -18,6 +18,8 @@ class ColormapRegistry(Mapping[str, colors.Colormap]): def get_cmap(self, cmap: str | colors.Colormap) -> colors.Colormap: ... _colormaps: ColormapRegistry = ... +_multivar_colormaps: ColormapRegistry = ... +_bivar_colormaps: ColormapRegistry = ... def get_cmap(name: str | colors.Colormap | None = ..., lut: int | None = ...) -> colors.Colormap: ... diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 7c127fce7819..2dbbb51f6196 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -54,7 +54,7 @@ import matplotlib as mpl import numpy as np -from matplotlib import _api, _cm, cbook, scale +from matplotlib import _api, _cm, cbook, scale, _image from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS @@ -87,6 +87,7 @@ def __delitem__(self, key): _colors_full_map = _ColorMapping(_colors_full_map) _REPR_PNG_SIZE = (512, 64) +_BIVAR_REPR_PNG_SIZE = 256 def get_named_colors_mapping(): @@ -705,6 +706,7 @@ def __init__(self, name, N=256): self._i_over = self.N + 1 self._i_bad = self.N + 2 self._isinit = False + self.n_variates = 1 #: When this colormap exists on a scalar mappable and colorbar_extend #: is not False, colorbar creation will pick up ``colorbar_extend`` as #: the default value for the ``extend`` keyword in the @@ -724,7 +726,7 @@ def __call__(self, X, alpha=None, bytes=False): alpha : float or array-like or None Alpha must be a scalar between 0 and 1, a sequence of such floats with shape matching X, or None. - bytes : bool + bytes : bool, default: False If False (default), the returned RGBA values will be floats in the interval ``[0, 1]`` otherwise they will be `numpy.uint8`\s in the interval ``[0, 255]``. @@ -734,6 +736,36 @@ def __call__(self, X, alpha=None, bytes=False): Tuple of RGBA values if X is scalar, otherwise an array of RGBA values with a shape of ``X.shape + (4, )``. """ + rgba, mask = self._get_rgba_and_mask(X, alpha=alpha, bytes=bytes) + if not np.iterable(X): + rgba = tuple(rgba) + return rgba + + def _get_rgba_and_mask(self, X, alpha=None, bytes=False): + r""" + Parameters + ---------- + X : float or int, `~numpy.ndarray` or scalar + The data value(s) to convert to RGBA. + For floats, *X* should be in the interval ``[0.0, 1.0]`` to + return the RGBA values ``X*100`` percent along the Colormap line. + For integers, *X* should be in the interval ``[0, Colormap.N)`` to + return RGBA values *indexed* from the Colormap with index ``X``. + alpha : float or array-like or None + Alpha must be a scalar between 0 and 1, a sequence of such + floats with shape matching X, or None. + bytes : bool, default: False + If False (default), the returned RGBA values will be floats in the + interval ``[0, 1]`` otherwise they will be `numpy.uint8`\s in the + interval ``[0, 255]``. + + Returns + ------- + colors : np.ndarray + Array of RGBA values with a shape of ``X.shape + (4, )``. + mask : np.ndarray + Boolean array with True where the input is ``np.nan`` or masked. + """ if not self._isinit: self._init() @@ -777,9 +809,7 @@ def __call__(self, X, alpha=None, bytes=False): if (lut[-1] == 0).all(): rgba[mask_bad] = (0, 0, 0, 0) - if not np.iterable(X): - rgba = tuple(rgba) - return rgba + return rgba, mask_bad def __copy__(self): cls = self.__class__ @@ -1227,6 +1257,864 @@ def reversed(self, name=None): return new_cmap +class MultivarColormap: + """ + Class for holding multiple `~matplotlib.colors.Colormap` for use in a + `~matplotlib.cm.ScalarMappable` object + """ + def __init__(self, colormaps, combination_mode, name='multivariate colormap'): + """ + Parameters + ---------- + colormaps: list or tuple of `~matplotlib.colors.Colormap` objects + The individual colormaps that are combined + combination_mode: str, 'sRGB_add' or 'sRGB_sub' + Describe how colormaps are combined in sRGB space + + - If 'sRGB_add' -> Mixing produces brighter colors + `sRGB = sum(colors)` + - If 'sRGB_sub' -> Mixing produces darker colors + `sRGB = 1 - sum(1 - colors)` + name : str, optional + The name of the colormap family. + """ + self.name = name + + if not np.iterable(colormaps) \ + or len(colormaps) == 1 \ + or isinstance(colormaps, str): + raise ValueError("A MultivarColormap must have more than one colormap.") + colormaps = list(colormaps) # ensure cmaps is a list, i.e. not a tuple + for i, cmap in enumerate(colormaps): + if isinstance(cmap, str): + colormaps[i] = mpl.colormaps[cmap] + elif not isinstance(cmap, Colormap): + raise ValueError("colormaps must be a list of objects that subclass" + " Colormap or a name found in the colormap registry.") + + self._colormaps = colormaps + _api.check_in_list(['sRGB_add', 'sRGB_sub'], combination_mode=combination_mode) + self._combination_mode = combination_mode + self.n_variates = len(colormaps) + self._rgba_bad = (0.0, 0.0, 0.0, 0.0) # If bad, don't paint anything. + + def __call__(self, X, alpha=None, bytes=False, clip=True): + r""" + Parameters + ---------- + X : tuple (X0, X1, ...) of length equal to the number of colormaps + X0, X1 ...: + float or int, `~numpy.ndarray` or scalar + The data value(s) to convert to RGBA. + For floats, *Xi...* should be in the interval ``[0.0, 1.0]`` to + return the RGBA values ``X*100`` percent along the Colormap line. + For integers, *Xi...* should be in the interval ``[0, self[i].N)`` to + return RGBA values *indexed* from colormap [i] with index ``Xi``, where + self[i] is colormap i. + alpha : float or array-like or None + Alpha must be a scalar between 0 and 1, a sequence of such + floats with shape matching *Xi*, or None. + bytes : bool, default: False + If False (default), the returned RGBA values will be floats in the + interval ``[0, 1]`` otherwise they will be `numpy.uint8`\s in the + interval ``[0, 255]``. + clip : bool, default: True + If True, clip output to 0 to 1 + + Returns + ------- + Tuple of RGBA values if X[0] is scalar, otherwise an array of + RGBA values with a shape of ``X.shape + (4, )``. + """ + + if len(X) != len(self): + raise ValueError( + f'For the selected colormap the data must have a first dimension ' + f'{len(self)}, not {len(X)}') + rgba, mask_bad = self[0]._get_rgba_and_mask(X[0], bytes=False) + for c, xx in zip(self[1:], X[1:]): + sub_rgba, sub_mask_bad = c._get_rgba_and_mask(xx, bytes=False) + rgba[..., :3] += sub_rgba[..., :3] # add colors + rgba[..., 3] *= sub_rgba[..., 3] # multiply alpha + mask_bad |= sub_mask_bad + + if self.combination_mode == 'sRGB_sub': + rgba[..., :3] -= len(self) - 1 + + rgba[mask_bad] = self.get_bad() + + if clip: + rgba = np.clip(rgba, 0, 1) + + if alpha is not None: + if clip: + alpha = np.clip(alpha, 0, 1) + if np.shape(alpha) not in [(), np.shape(X[0])]: + raise ValueError( + f"alpha is array-like but its shape {np.shape(alpha)} does " + f"not match that of X[0] {np.shape(X[0])}") + rgba[..., -1] *= alpha + + if bytes: + if not clip: + raise ValueError( + "clip cannot be false while bytes is true" + " as uint8 does not support values below 0" + " or above 255.") + rgba = (rgba * 255).astype('uint8') + + if not np.iterable(X[0]): + rgba = tuple(rgba) + + return rgba + + def copy(self): + """Return a copy of the multivarcolormap.""" + return self.__copy__() + + def __copy__(self): + cls = self.__class__ + cmapobject = cls.__new__(cls) + cmapobject.__dict__.update(self.__dict__) + cmapobject._colormaps = [cm.copy() for cm in self._colormaps] + cmapobject._rgba_bad = np.copy(self._rgba_bad) + return cmapobject + + def __eq__(self, other): + if not isinstance(other, MultivarColormap): + return False + if len(self) != len(other): + return False + for c0, c1 in zip(self, other): + if c0 != c1: + return False + if not all(self._rgba_bad == other._rgba_bad): + return False + if self.combination_mode != other.combination_mode: + return False + return True + + def __getitem__(self, item): + return self._colormaps[item] + + def __iter__(self): + for c in self._colormaps: + yield c + + def __len__(self): + return len(self._colormaps) + + def __str__(self): + return self.name + + def get_bad(self): + """Get the color for masked values.""" + return np.array(self._rgba_bad) + + def resampled(self, lutshape): + """ + Return a new colormap with *lutshape* entries. + + Parameters + ---------- + lutshape : tuple of (`int`, `None`) + The tuple must have a length matching the number of variates. + For each element in the tuple, if `int`, the corresponding colorbar + is resampled, if `None`, the corresponding colorbar is not resampled. + + Returns + ------- + MultivarColormap + """ + + if not np.iterable(lutshape) or len(lutshape) != len(self): + raise ValueError(f"lutshape must be of length {len(self)}") + new_cmap = self.copy() + for i, s in enumerate(lutshape): + if s is not None: + new_cmap._colormaps[i] = self[i].resampled(s) + return new_cmap + + def with_extremes(self, *, bad=None, under=None, over=None): + """ + Return a copy of the `MultivarColormap` with modified out-of-range attributes. + + The *bad* keyword modifies the copied `MultivarColormap` while *under* and + *over* modifies the attributes of the copied component colormaps. + Note that *under* and *over* colors are subject to the mixing rules determined + by the *combination_mode*. + + Parameters + ---------- + bad: :mpltype:`color`, default: None + If Matplotlib color, the bad value is set accordingly in the copy + + under tuple of :mpltype:`color`, default: None + If tuple, the `under` value of each component is set with the values + from the tuple. + + over tuple of :mpltype:`color`, default: None + If tuple, the `over` value of each component is set with the values + from the tuple. + + Returns + ------- + MultivarColormap + copy of self with attributes set + + """ + new_cm = self.copy() + if bad is not None: + new_cm._rgba_bad = to_rgba(bad) + if under is not None: + if not np.iterable(under) or len(under) != len(new_cm): + raise ValueError("*under* must contain a color for each scalar colormap" + f" i.e. be of length {len(new_cm)}.") + else: + for c, b in zip(new_cm, under): + c.set_under(b) + if over is not None: + if not np.iterable(over) or len(over) != len(new_cm): + raise ValueError("*over* must contain a color for each scalar colormap" + f" i.e. be of length {len(new_cm)}.") + else: + for c, b in zip(new_cm, over): + c.set_over(b) + return new_cm + + @property + def combination_mode(self): + return self._combination_mode + + def _repr_png_(self): + """Generate a PNG representation of the Colormap.""" + X = np.tile(np.linspace(0, 1, _REPR_PNG_SIZE[0]), + (_REPR_PNG_SIZE[1], 1)) + pixels = np.zeros((_REPR_PNG_SIZE[1]*len(self), _REPR_PNG_SIZE[0], 4), + dtype=np.uint8) + for i, c in enumerate(self): + pixels[i*_REPR_PNG_SIZE[1]:(i+1)*_REPR_PNG_SIZE[1], :] = c(X, bytes=True) + png_bytes = io.BytesIO() + title = self.name + ' multivariate colormap' + author = f'Matplotlib v{mpl.__version__}, https://matplotlib.org' + pnginfo = PngInfo() + pnginfo.add_text('Title', title) + pnginfo.add_text('Description', title) + pnginfo.add_text('Author', author) + pnginfo.add_text('Software', author) + Image.fromarray(pixels).save(png_bytes, format='png', pnginfo=pnginfo) + return png_bytes.getvalue() + + def _repr_html_(self): + """Generate an HTML representation of the MultivarColormap.""" + return ''.join([c._repr_html_() for c in self._colormaps]) + + +class BivarColormap: + """ + Base class for all bivariate to RGBA mappings. + + Designed as a drop-in replacement for Colormap when using a 2D + lookup table. To be used with `~matplotlib.cm.ScalarMappable`. + """ + + def __init__(self, N=256, M=256, shape='square', origin=(0, 0), + name='bivariate colormap'): + """ + Parameters + ---------- + N : int, default: 256 + The number of RGB quantization levels along the first axis. + M : int, default: 256 + The number of RGB quantization levels along the second axis. + shape : {'square', 'circle', 'ignore', 'circleignore'} + + - 'square' each variate is clipped to [0,1] independently + - 'circle' the variates are clipped radially to the center + of the colormap, and a circular mask is applied when the colormap + is displayed + - 'ignore' the variates are not clipped, but instead assigned the + 'outside' color + - 'circleignore' a circular mask is applied, but the data is not + clipped and instead assigned the 'outside' color + + origin : (float, float), default: (0,0) + The relative origin of the colormap. Typically (0, 0), for colormaps + that are linear on both axis, and (.5, .5) for circular colormaps. + Used when getting 1D colormaps from 2D colormaps. + name : str, optional + The name of the colormap. + """ + + self.name = name + self.N = int(N) # ensure that N is always int + self.M = int(M) + _api.check_in_list(['square', 'circle', 'ignore', 'circleignore'], shape=shape) + self._shape = shape + self._rgba_bad = (0.0, 0.0, 0.0, 0.0) # If bad, don't paint anything. + self._rgba_outside = (1.0, 0.0, 1.0, 1.0) + self._isinit = False + self.n_variates = 2 + self._origin = (float(origin[0]), float(origin[1])) + '''#: When this colormap exists on a scalar mappable and colorbar_extend + #: is not False, colorbar creation will pick up ``colorbar_extend`` as + #: the default value for the ``extend`` keyword in the + #: `matplotlib.colorbar.Colorbar` constructor. + self.colorbar_extend = False''' + + def __call__(self, X, alpha=None, bytes=False): + r""" + Parameters + ---------- + X : tuple (X0, X1), X0 and X1: float or int `~numpy.ndarray` or scalar + The data value(s) to convert to RGBA. + + - For floats, *X* should be in the interval ``[0.0, 1.0]`` to + return the RGBA values ``X*100`` percent along the Colormap. + - For integers, *X* should be in the interval ``[0, Colormap.N)`` to + return RGBA values *indexed* from the Colormap with index ``X``. + + alpha : float or array-like or None, default: None + Alpha must be a scalar between 0 and 1, a sequence of such + floats with shape matching X0, or None. + bytes : bool, default: False + If False (default), the returned RGBA values will be floats in the + interval ``[0, 1]`` otherwise they will be `numpy.uint8`\s in the + interval ``[0, 255]``. + + Returns + ------- + Tuple of RGBA values if X is scalar, otherwise an array of + RGBA values with a shape of ``X.shape + (4, )``. + """ + + if len(X) != 2: + raise ValueError( + f'For a `BivarColormap` the data must have a first dimension ' + f'2, not {len(X)}') + + if not self._isinit: + self._init() + + X0 = np.ma.array(X[0], copy=True) + X1 = np.ma.array(X[1], copy=True) + # clip to shape of colormap, circle square, etc. + self._clip((X0, X1)) + + # Native byteorder is faster. + if not X0.dtype.isnative: + X0 = X0.byteswap().view(X0.dtype.newbyteorder()) + if not X1.dtype.isnative: + X1 = X1.byteswap().view(X1.dtype.newbyteorder()) + + if X0.dtype.kind == "f": + X0 *= self.N + # xa == 1 (== N after multiplication) is not out of range. + X0[X0 == self.N] = self.N - 1 + + if X1.dtype.kind == "f": + X1 *= self.M + # xa == 1 (== N after multiplication) is not out of range. + X1[X1 == self.M] = self.M - 1 + + # Pre-compute the masks before casting to int (which can truncate) + mask_outside = (X0 < 0) | (X1 < 0) | (X0 >= self.N) | (X1 >= self.M) + # If input was masked, get the bad mask from it; else mask out nans. + mask_bad_0 = X0.mask if np.ma.is_masked(X0) else np.isnan(X0) + mask_bad_1 = X1.mask if np.ma.is_masked(X1) else np.isnan(X1) + mask_bad = mask_bad_0 | mask_bad_1 + + with np.errstate(invalid="ignore"): + # We need this cast for unsigned ints as well as floats + X0 = X0.astype(int) + X1 = X1.astype(int) + + # Set masked values to zero + # The corresponding rgb values will be replaced later + for X_part in [X0, X1]: + X_part[mask_outside] = 0 + X_part[mask_bad] = 0 + + rgba = self._lut[X0, X1] + if np.isscalar(X[0]): + rgba = np.copy(rgba) + rgba[mask_outside] = self._rgba_outside + rgba[mask_bad] = self._rgba_bad + if bytes: + rgba = (rgba * 255).astype(np.uint8) + if alpha is not None: + alpha = np.clip(alpha, 0, 1) + if bytes: + alpha *= 255 # Will be cast to uint8 upon assignment. + if np.shape(alpha) not in [(), np.shape(X0)]: + raise ValueError( + f"alpha is array-like but its shape {np.shape(alpha)} does " + f"not match that of X[0] {np.shape(X0)}") + rgba[..., -1] = alpha + # If the "bad" color is all zeros, then ignore alpha input. + if (np.array(self._rgba_bad) == 0).all(): + rgba[mask_bad] = (0, 0, 0, 0) + + if not np.iterable(X[0]): + rgba = tuple(rgba) + return rgba + + @property + def lut(self): + """ + For external access to the lut, i.e. for displaying the cmap. + For circular colormaps this returns a lut with a circular mask. + + Internal functions (such as to_rgb()) should use _lut + which stores the lut without a circular mask + A lut without the circular mask is needed in to_rgb() because the + conversion from floats to ints results in some some pixel-requests + just outside of the circular mask + + """ + if not self._isinit: + self._init() + lut = np.copy(self._lut) + if self.shape == 'circle' or self.shape == 'circleignore': + n = np.linspace(-1, 1, self.N) + m = np.linspace(-1, 1, self.M) + radii_sqr = (n**2)[:, np.newaxis] + (m**2)[np.newaxis, :] + mask_outside = radii_sqr > 1 + lut[mask_outside, 3] = 0 + return lut + + def __copy__(self): + cls = self.__class__ + cmapobject = cls.__new__(cls) + cmapobject.__dict__.update(self.__dict__) + + cmapobject._rgba_outside = np.copy(self._rgba_outside) + cmapobject._rgba_bad = np.copy(self._rgba_bad) + cmapobject._shape = self.shape + if self._isinit: + cmapobject._lut = np.copy(self._lut) + return cmapobject + + def __eq__(self, other): + if not isinstance(other, BivarColormap): + return False + # To compare lookup tables the Colormaps have to be initialized + if not self._isinit: + self._init() + if not other._isinit: + other._init() + if not np.array_equal(self._lut, other._lut): + return False + if not np.array_equal(self._rgba_bad, other._rgba_bad): + return False + if not np.array_equal(self._rgba_outside, other._rgba_outside): + return False + if self.shape != other.shape: + return False + return True + + def get_bad(self): + """Get the color for masked values.""" + return self._rgba_bad + + def get_outside(self): + """Get the color for out-of-range values.""" + return self._rgba_outside + + def resampled(self, lutshape, transposed=False): + """ + Return a new colormap with *lutshape* entries. + + Note that this function does not move the origin. + + Parameters + ---------- + lutshape : tuple of ints or None + The tuple must be of length 2, and each entry is either an int or None. + + - If an int, the corresponding axis is resampled. + - If negative the corresponding axis is resampled in reverse + - If -1, the axis is inverted + - If 1 or None, the corresponding axis is not resampled. + + transposed : bool, default: False + if True, the axes are swapped after resampling + + Returns + ------- + BivarColormap + """ + + if not np.iterable(lutshape) or len(lutshape) != 2: + raise ValueError("lutshape must be of length 2") + lutshape = [lutshape[0], lutshape[1]] + if lutshape[0] is None or lutshape[0] == 1: + lutshape[0] = self.N + if lutshape[1] is None or lutshape[1] == 1: + lutshape[1] = self.M + + inverted = [False, False] + if lutshape[0] < 0: + inverted[0] = True + lutshape[0] = -lutshape[0] + if lutshape[0] == 1: + lutshape[0] = self.N + if lutshape[1] < 0: + inverted[1] = True + lutshape[1] = -lutshape[1] + if lutshape[1] == 1: + lutshape[1] = self.M + x_0, x_1 = np.mgrid[0:1:(lutshape[0] * 1j), 0:1:(lutshape[1] * 1j)] + if inverted[0]: + x_0 = x_0[::-1, :] + if inverted[1]: + x_1 = x_1[:, ::-1] + + # we need to use shape = 'square' while resampling the colormap. + # if the colormap has shape = 'circle' we would otherwise get *outside* in the + # resampled colormap + shape_memory = self._shape + self._shape = 'square' + if transposed: + new_lut = self((x_1, x_0)) + new_cmap = BivarColormapFromImage(new_lut, name=self.name, + shape=shape_memory, + origin=self.origin[::-1]) + else: + new_lut = self((x_0, x_1)) + new_cmap = BivarColormapFromImage(new_lut, name=self.name, + shape=shape_memory, + origin=self.origin) + self._shape = shape_memory + + new_cmap._rgba_bad = self._rgba_bad + new_cmap._rgba_outside = self._rgba_outside + return new_cmap + + def reversed(self, axis_0=True, axis_1=True): + """ + Reverses both or one of the axis. + """ + r_0 = -1 if axis_0 else 1 + r_1 = -1 if axis_1 else 1 + return self.resampled((r_0, r_1)) + + def transposed(self): + """ + Transposes the colormap by swapping the order of the axis + """ + return self.resampled((None, None), transposed=True) + + def with_extremes(self, *, bad=None, outside=None, shape=None, origin=None): + """ + Return a copy of the `BivarColormap` with modified attributes. + + Note that the *outside* color is only relevant if `shape` = 'ignore' + or 'circleignore'. + + Parameters + ---------- + bad : None or :mpltype:`color` + If Matplotlib color, the *bad* value is set accordingly in the copy + + outside : None or :mpltype:`color` + If Matplotlib color and shape is 'ignore' or 'circleignore', values + *outside* the colormap are colored accordingly in the copy + + shape : {'square', 'circle', 'ignore', 'circleignore'} + + - If 'square' each variate is clipped to [0,1] independently + - If 'circle' the variates are clipped radially to the center + of the colormap, and a circular mask is applied when the colormap + is displayed + - If 'ignore' the variates are not clipped, but instead assigned the + *outside* color + - If 'circleignore' a circular mask is applied, but the data is not + clipped and instead assigned the *outside* color + + origin : (float, float) + The relative origin of the colormap. Typically (0, 0), for colormaps + that are linear on both axis, and (.5, .5) for circular colormaps. + Used when getting 1D colormaps from 2D colormaps. + + Returns + ------- + BivarColormap + copy of self with attributes set + """ + new_cm = self.copy() + if bad is not None: + new_cm._rgba_bad = to_rgba(bad) + if outside is not None: + new_cm._rgba_outside = to_rgba(outside) + if shape is not None: + _api.check_in_list(['square', 'circle', 'ignore', 'circleignore'], + shape=shape) + new_cm._shape = shape + if origin is not None: + new_cm._origin = (float(origin[0]), float(origin[1])) + + return new_cm + + def _init(self): + """Generate the lookup table, ``self._lut``.""" + raise NotImplementedError("Abstract class only") + + @property + def shape(self): + return self._shape + + @property + def origin(self): + return self._origin + + def _clip(self, X): + """ + For internal use when applying a BivarColormap to data. + i.e. cm.ScalarMappable().to_rgba() + Clips X[0] and X[1] according to 'self.shape'. + X is modified in-place. + + Parameters + ---------- + X: np.array + array of floats or ints to be clipped + shape : {'square', 'circle', 'ignore', 'circleignore'} + + - If 'square' each variate is clipped to [0,1] independently + - If 'circle' the variates are clipped radially to the center + of the colormap. + It is assumed that a circular mask is applied when the colormap + is displayed + - If 'ignore' the variates are not clipped, but instead assigned the + 'outside' color + - If 'circleignore' a circular mask is applied, but the data is not clipped + and instead assigned the 'outside' color + + """ + if self.shape == 'square': + for X_part, mx in zip(X, (self.N, self.M)): + X_part[X_part < 0] = 0 + if X_part.dtype.kind == "f": + X_part[X_part > 1] = 1 + else: + X_part[X_part >= mx] = mx - 1 + + elif self.shape == 'ignore': + for X_part, mx in zip(X, (self.N, self.M)): + X_part[X_part < 0] = -1 + if X_part.dtype.kind == "f": + X_part[X_part > 1] = -1 + else: + X_part[X_part >= mx] = -1 + + elif self.shape == 'circle' or self.shape == 'circleignore': + for X_part in X: + if X_part.dtype.kind != "f": + raise NotImplementedError( + "Circular bivariate colormaps are only" + " implemented for use with with floats") + radii_sqr = (X[0] - 0.5)**2 + (X[1] - 0.5)**2 + mask_outside = radii_sqr > 0.25 + if self.shape == 'circle': + overextend = 2 * np.sqrt(radii_sqr[mask_outside]) + X[0][mask_outside] = (X[0][mask_outside] - 0.5) / overextend + 0.5 + X[1][mask_outside] = (X[1][mask_outside] - 0.5) / overextend + 0.5 + else: + X[0][mask_outside] = -1 + X[1][mask_outside] = -1 + + def __getitem__(self, item): + """Creates and returns a colorbar along the selected axis""" + if not self._isinit: + self._init() + if item == 0: + origin_1_as_int = int(self._origin[1]*self.M) + if origin_1_as_int > self.M-1: + origin_1_as_int = self.M-1 + one_d_lut = self._lut[:, origin_1_as_int] + new_cmap = ListedColormap(one_d_lut, name=f'{self.name}_0', N=self.N) + + elif item == 1: + origin_0_as_int = int(self._origin[0]*self.N) + if origin_0_as_int > self.N-1: + origin_0_as_int = self.N-1 + one_d_lut = self._lut[origin_0_as_int, :] + new_cmap = ListedColormap(one_d_lut, name=f'{self.name}_1', N=self.M) + else: + raise KeyError(f"only 0 or 1 are" + f" valid keys for BivarColormap, not {item!r}") + new_cmap._rgba_bad = self._rgba_bad + if self.shape in ['ignore', 'circleignore']: + new_cmap.set_over(self._rgba_outside) + new_cmap.set_under(self._rgba_outside) + return new_cmap + + def _repr_png_(self): + """Generate a PNG representation of the BivarColormap.""" + if not self._isinit: + self._init() + pixels = self.lut + if pixels.shape[0] < _BIVAR_REPR_PNG_SIZE: + pixels = np.repeat(pixels, + repeats=_BIVAR_REPR_PNG_SIZE//pixels.shape[0], + axis=0)[:256, :] + if pixels.shape[1] < _BIVAR_REPR_PNG_SIZE: + pixels = np.repeat(pixels, + repeats=_BIVAR_REPR_PNG_SIZE//pixels.shape[1], + axis=1)[:, :256] + pixels = (pixels[::-1, :, :] * 255).astype(np.uint8) + png_bytes = io.BytesIO() + title = self.name + ' BivarColormap' + author = f'Matplotlib v{mpl.__version__}, https://matplotlib.org' + pnginfo = PngInfo() + pnginfo.add_text('Title', title) + pnginfo.add_text('Description', title) + pnginfo.add_text('Author', author) + pnginfo.add_text('Software', author) + Image.fromarray(pixels).save(png_bytes, format='png', pnginfo=pnginfo) + return png_bytes.getvalue() + + def _repr_html_(self): + """Generate an HTML representation of the Colormap.""" + png_bytes = self._repr_png_() + png_base64 = base64.b64encode(png_bytes).decode('ascii') + def color_block(color): + hex_color = to_hex(color, keep_alpha=True) + return (f'
') + + return ('
' + f'{self.name} ' + '
' + '
' + '
' + '
' + f'{color_block(self.get_outside())} outside' + '
' + '
' + f'bad {color_block(self.get_bad())}' + '
') + + def copy(self): + """Return a copy of the colormap.""" + return self.__copy__() + + +class SegmentedBivarColormap(BivarColormap): + """ + BivarColormap object generated by supersampling a regular grid. + + Parameters + ---------- + patch : np.array + Patch is required to have a shape (k, l, 3), and will get supersampled + to a lut of shape (N, N, 4). + N : int + The number of RGB quantization levels along each axis. + shape : {'square', 'circle', 'ignore', 'circleignore'} + + - If 'square' each variate is clipped to [0,1] independently + - If 'circle' the variates are clipped radially to the center + of the colormap, and a circular mask is applied when the colormap + is displayed + - If 'ignore' the variates are not clipped, but instead assigned the + 'outside' color + - If 'circleignore' a circular mask is applied, but the data is not clipped + + origin : (float, float) + The relative origin of the colormap. Typically (0, 0), for colormaps + that are linear on both axis, and (.5, .5) for circular colormaps. + Used when getting 1D colormaps from 2D colormaps. + + name : str, optional + The name of the colormap. + """ + + def __init__(self, patch, N=256, shape='square', origin=(0, 0), + name='segmented bivariate colormap'): + _api.check_shape((None, None, 3), patch=patch) + self.patch = patch + super().__init__(N, N, shape, origin, name=name) + + def _init(self): + s = self.patch.shape + _patch = np.empty((s[0], s[1], 4)) + _patch[:, :, :3] = self.patch + _patch[:, :, 3] = 1 + transform = mpl.transforms.Affine2D().translate(-0.5, -0.5)\ + .scale(self.N / (s[1] - 1), self.N / (s[0] - 1)) + self._lut = np.empty((self.N, self.N, 4)) + + _image.resample(_patch, self._lut, transform, _image.BILINEAR, + resample=False, alpha=1) + self._isinit = True + + +class BivarColormapFromImage(BivarColormap): + """ + BivarColormap object generated by supersampling a regular grid. + + Parameters + ---------- + lut : nparray of shape (N, M, 3) or (N, M, 4) + The look-up-table + shape: {'square', 'circle', 'ignore', 'circleignore'} + + - If 'square' each variate is clipped to [0,1] independently + - If 'circle' the variates are clipped radially to the center + of the colormap, and a circular mask is applied when the colormap + is displayed + - If 'ignore' the variates are not clipped, but instead assigned the + 'outside' color + - If 'circleignore' a circular mask is applied, but the data is not clipped + + origin: (float, float) + The relative origin of the colormap. Typically (0, 0), for colormaps + that are linear on both axis, and (.5, .5) for circular colormaps. + Used when getting 1D colormaps from 2D colormaps. + name : str, optional + The name of the colormap. + + """ + + def __init__(self, lut, shape='square', origin=(0, 0), name='from image'): + # We can allow for a PIL.Image as input in the following way, but importing + # matplotlib.image.pil_to_array() results in a circular import + # For now, this function only accepts numpy arrays. + # i.e.: + # if isinstance(Image, lut): + # lut = image.pil_to_array(lut) + lut = np.array(lut, copy=True) + if lut.ndim != 3 or lut.shape[2] not in (3, 4): + raise ValueError("The lut must be an array of shape (n, m, 3) or (n, m, 4)", + " or a PIL.image encoded as RGB or RGBA") + + if lut.dtype == np.uint8: + lut = lut.astype(np.float32)/255 + if lut.shape[2] == 3: + new_lut = np.empty((lut.shape[0], lut.shape[1], 4), dtype=lut.dtype) + new_lut[:, :, :3] = lut + new_lut[:, :, 3] = 1. + lut = new_lut + self._lut = lut + super().__init__(lut.shape[0], lut.shape[1], shape, origin, name=name) + + def _init(self): + self._isinit = True + + class Normalize: """ A class which, when called, maps values within the interval diff --git a/lib/matplotlib/colors.pyi b/lib/matplotlib/colors.pyi index 514801b714b8..6941e3d5d176 100644 --- a/lib/matplotlib/colors.pyi +++ b/lib/matplotlib/colors.pyi @@ -138,6 +138,101 @@ class ListedColormap(Colormap): def resampled(self, lutsize: int) -> ListedColormap: ... def reversed(self, name: str | None = ...) -> ListedColormap: ... +class MultivarColormap: + name: str + n_variates: int + def __init__(self, colormaps: list[Colormap], combination_mode: Literal['sRGB_add', 'sRGB_sub'], name: str = ...) -> None: ... + @overload + def __call__( + self, X: Sequence[Sequence[float]] | np.ndarray, alpha: ArrayLike | None = ..., bytes: bool = ..., clip: bool = ... + ) -> np.ndarray: ... + @overload + def __call__( + self, X: Sequence[float], alpha: float | None = ..., bytes: bool = ..., clip: bool = ... + ) -> tuple[float, float, float, float]: ... + @overload + def __call__( + self, X: ArrayLike, alpha: ArrayLike | None = ..., bytes: bool = ..., clip: bool = ... + ) -> tuple[float, float, float, float] | np.ndarray: ... + def copy(self) -> MultivarColormap: ... + def __copy__(self) -> MultivarColormap: ... + def __eq__(self, other: Any) -> bool: ... + def __getitem__(self, item: int) -> Colormap: ... + def __iter__(self) -> Iterator[Colormap]: ... + def __len__(self) -> int: ... + def get_bad(self) -> np.ndarray: ... + def resampled(self, lutshape: Sequence[int | None]) -> MultivarColormap: ... + def with_extremes( + self, + *, + bad: ColorType | None = ..., + under: Sequence[ColorType] | None = ..., + over: Sequence[ColorType] | None = ... + ) -> MultivarColormap: ... + @property + def combination_mode(self) -> str: ... + def _repr_html_(self) -> str: ... + def _repr_png_(self) -> bytes: ... + +class BivarColormap: + name: str + N: int + M: int + n_variates: int + def __init__( + self, N: int = ..., M: int | None = ..., shape: Literal['square', 'circle', 'ignore', 'circleignore'] = ..., + origin: Sequence[float] = ..., name: str = ... + ) -> None: ... + @overload + def __call__( + self, X: Sequence[Sequence[float]] | np.ndarray, alpha: ArrayLike | None = ..., bytes: bool = ... + ) -> np.ndarray: ... + @overload + def __call__( + self, X: Sequence[float], alpha: float | None = ..., bytes: bool = ... + ) -> tuple[float, float, float, float]: ... + @overload + def __call__( + self, X: ArrayLike, alpha: ArrayLike | None = ..., bytes: bool = ... + ) -> tuple[float, float, float, float] | np.ndarray: ... + @property + def lut(self) -> np.ndarray: ... + @property + def shape(self) -> str: ... + @property + def origin(self) -> tuple[float, float]: ... + def copy(self) -> BivarColormap: ... + def __copy__(self) -> BivarColormap: ... + def __getitem__(self, item: int) -> Colormap: ... + def __eq__(self, other: Any) -> bool: ... + def get_bad(self) -> np.ndarray: ... + def get_outside(self) -> np.ndarray: ... + def resampled(self, lutshape: Sequence[int | None], transposed: bool = ...) -> BivarColormap: ... + def transposed(self) -> BivarColormap: ... + def reversed(self, axis_0: bool = ..., axis_1: bool = ...) -> BivarColormap: ... + def with_extremes( + self, + *, + bad: ColorType | None = ..., + outside: ColorType | None = ..., + shape: str | None = ..., + origin: None | Sequence[float] = ..., + ) -> MultivarColormap: ... + def _repr_html_(self) -> str: ... + def _repr_png_(self) -> bytes: ... + +class SegmentedBivarColormap(BivarColormap): + def __init__( + self, patch: np.ndarray, N: int = ..., shape: Literal['square', 'circle', 'ignore', 'circleignore'] = ..., + origin: Sequence[float] = ..., name: str = ... + ) -> None: ... + +class BivarColormapFromImage(BivarColormap): + def __init__( + self, lut: np.ndarray, shape: Literal['square', 'circle', 'ignore', 'circleignore'] = ..., + origin: Sequence[float] = ..., name: str = ... + ) -> None: ... + class Normalize: callbacks: cbook.CallbackRegistry def __init__( diff --git a/lib/matplotlib/meson.build b/lib/matplotlib/meson.build index c4b66fc1b336..657adfd4e835 100644 --- a/lib/matplotlib/meson.build +++ b/lib/matplotlib/meson.build @@ -4,7 +4,9 @@ python_sources = [ '_animation_data.py', '_blocking_input.py', '_cm.py', + '_cm_bivar.py', '_cm_listed.py', + '_cm_multivar.py', '_color_data.py', '_constrained_layout.py', '_docstring.py', diff --git a/lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/bivariate_cmap_shapes.png b/lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/bivariate_cmap_shapes.png new file mode 100644 index 0000000000000000000000000000000000000000..f22b446fc84b5c0675d96f50faece58312ecc7d1 GIT binary patch literal 5157 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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/multivar_alpha_mixing.png b/lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/multivar_alpha_mixing.png new file mode 100644 index 0000000000000000000000000000000000000000..415dfda291dcd476b5e81e152092ad59bdd49844 GIT binary patch literal 4917 zcmeHLdr(tX9zHx2s2Z@gSVV!5nX->3Z9i5N| z9Ub@C{hmsVrMMsv(LZ;GlgQzSyQNd{&=6*^-&~{sV2)os4a(6EBLMg`^~CXGXXC$@ z4;)J`yJpZfuRo-a&34E->s&9-PCUA?u}mK3lIrDUlkxen+n>ttCN8w;Itx=;I?T*t zq}0DHbiA}ky3@rgaq8Vht16o(nXs~qsUlwXY#-J1#)&Sx4SSf$jyuX8qr^*XnI&;y zO(;c&3_3mofW@+oG>7H|gBchC@Y>V@0K2bQP~m`~F#zdX?ErAk00sbDGByRk!rmR~ z^2KkDt|5>SY0k>3=W@>-baoa>B!LBNcCDKUfThaia^o^-iKL|(|dUA5|e9@+H4Njq0$tsb_`?fL!O!o5l&38+x z#3Ws@9+Nogu)RGSJqz96vNR*&c;~gZwzle<%wIWpK{L{fTn?xluOUAg6bp$v_Xznz z(0KP??`bpXoR(yT02v5>wqF^qYoj+_d+jGeH}A6vfwOrPf(6{sA!t(f5~a*2Ja3Re z&l2hP^M_piq*%bYGz2%r_AThoB&`&jE@XO`{5y9zIVetr+go?d=f%{6Hbaf2|oCnSiu zD9)a6Z~us`;8a>ewD&+mV~x+3*(ZtlG{e-t#Frnz`sQjPMSxY&e9hFvbGa!g(S$Tqc}8Dc>+uRZ`BsUt}E+7I}K=%9Kdd zCDKnfv-Q@#n>8z09paF#-a3)(@(AK%miV6~MxnJK%Mw}vR)djSX~+vNPsz=SlM?a$ zg39L!SNk=?yOz{A>!k(et7bM^oq|StV{Y*UIe4_bE3H&eX#iLT>tm8(XX!xJt3;j7 zDY(hOiYJ;Di^)^Mtn-1!prw8`R4wi8%LWgqLO3iZehD5qr=)lU*<*0erPE?1`d-WBbvaVyQ< zM(EdMi8gE0Rywsg9m0;OJ?w*nO^C2P*k_M_)E1AjJ>kqBaTxabpAK7 zE^s!4cb?wblo6JB?k39r3)SJ!b~M z7oV+9=KPQh-uFxjA794Mu=J%rqQ>81I{%Bw+UVbSj?bbV{xcqgiMjLf!F^)!-u;&f z)uxu~5uvSmZ2#pU*Y71YgydJaiV1w=GdYZ<%Z$egGOO&j6=%+h`qqpd)2G2sEIry$3)=pauS;5s% z$jX%%xwssdBjxVh#NAG%Kv6;8A~70S9$QcB)(jWEq9YO1U9uYDkJUmB?;7tv+wJ@! z-$BS|n2am861d}F}y%%5gx`tG@ZD|TP?fcpaMz=VP79x ztu!3jHnE<-|MH|w@jDqrZ#EFeME91&-sd6&!St6YeNA&7ax;@ajjW#Q(41oLe&6&v ziNtxh3fgNCaS0q{s!=s#zD2SP_EgE6j6BUU!mz-PI<1hJQ7C+A@;fP=u(CX(?7gkJv#a}G$y literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/meson.build b/lib/matplotlib/tests/meson.build index 107066636f31..1dcf3791638a 100644 --- a/lib/matplotlib/tests/meson.build +++ b/lib/matplotlib/tests/meson.build @@ -57,6 +57,7 @@ python_sources = [ 'test_marker.py', 'test_mathtext.py', 'test_matplotlib.py', + 'test_multivariate_colormaps.py', 'test_mlab.py', 'test_offsetbox.py', 'test_patches.py', diff --git a/lib/matplotlib/tests/test_multivariate_colormaps.py b/lib/matplotlib/tests/test_multivariate_colormaps.py new file mode 100644 index 000000000000..81a2e6adeb35 --- /dev/null +++ b/lib/matplotlib/tests/test_multivariate_colormaps.py @@ -0,0 +1,564 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_allclose +import matplotlib.pyplot as plt +from matplotlib.testing.decorators import (image_comparison, + remove_ticks_and_titles) +import matplotlib as mpl +import pytest +from pathlib import Path +from io import BytesIO +from PIL import Image +import base64 + + +@image_comparison(["bivariate_cmap_shapes.png"]) +def test_bivariate_cmap_shapes(): + x_0 = np.repeat(np.linspace(-0.1, 1.1, 10, dtype='float32')[None, :], 10, axis=0) + x_1 = x_0.T + + fig, axes = plt.subplots(1, 4, figsize=(10, 2)) + + # shape = 'square' + cmap = mpl.bivar_colormaps['BiPeak'] + axes[0].imshow(cmap((x_0, x_1)), interpolation='nearest') + + # shape = 'circle' + cmap = mpl.bivar_colormaps['BiCone'] + axes[1].imshow(cmap((x_0, x_1)), interpolation='nearest') + + # shape = 'ignore' + cmap = mpl.bivar_colormaps['BiPeak'] + cmap = cmap.with_extremes(shape='ignore') + axes[2].imshow(cmap((x_0, x_1)), interpolation='nearest') + + # shape = circleignore + cmap = mpl.bivar_colormaps['BiCone'] + cmap = cmap.with_extremes(shape='circleignore') + axes[3].imshow(cmap((x_0, x_1)), interpolation='nearest') + remove_ticks_and_titles(fig) + + +def test_multivar_creation(): + # test creation of a custom multivariate colorbar + blues = mpl.colormaps['Blues'] + cmap = mpl.colors.MultivarColormap((blues, 'Oranges'), 'sRGB_sub') + y, x = np.mgrid[0:3, 0:3]/2 + im = cmap((y, x)) + res = np.array([[[0.96862745, 0.94509804, 0.92156863, 1], + [0.96004614, 0.53504037, 0.23277201, 1], + [0.46666667, 0.1372549, 0.01568627, 1]], + [[0.41708574, 0.64141484, 0.75980008, 1], + [0.40850442, 0.23135717, 0.07100346, 1], + [0, 0, 0, 1]], + [[0.03137255, 0.14901961, 0.34117647, 1], + [0.02279123, 0, 0, 1], + [0, 0, 0, 1]]]) + assert_allclose(im, res, atol=0.01) + + with pytest.raises(ValueError, match="colormaps must be a list of"): + cmap = mpl.colors.MultivarColormap((blues, [blues]), 'sRGB_sub') + with pytest.raises(ValueError, match="A MultivarColormap must"): + cmap = mpl.colors.MultivarColormap('blues', 'sRGB_sub') + with pytest.raises(ValueError, match="A MultivarColormap must"): + cmap = mpl.colors.MultivarColormap((blues), 'sRGB_sub') + + +@image_comparison(["multivar_alpha_mixing.png"]) +def test_multivar_alpha_mixing(): + # test creation of a custom colormap using 'rainbow' + # and a colormap that goes from alpha = 1 to alpha = 0 + rainbow = mpl.colormaps['rainbow'] + alpha = np.zeros((256, 4)) + alpha[:, 3] = np.linspace(1, 0, 256) + alpha_cmap = mpl.colors.LinearSegmentedColormap.from_list('from_list', alpha) + + cmap = mpl.colors.MultivarColormap((rainbow, alpha_cmap), 'sRGB_add') + y, x = np.mgrid[0:10, 0:10]/9 + im = cmap((y, x)) + + fig, ax = plt.subplots() + ax.imshow(im, interpolation='nearest') + remove_ticks_and_titles(fig) + + +def test_multivar_cmap_call(): + cmap = mpl.multivar_colormaps['2VarAddA'] + assert_array_equal(cmap((0.0, 0.0)), (0, 0, 0, 1)) + assert_array_equal(cmap((1.0, 1.0)), (1, 1, 1, 1)) + assert_allclose(cmap((0.0, 0.0), alpha=0.1), (0, 0, 0, 0.1), atol=0.1) + + cmap = mpl.multivar_colormaps['2VarSubA'] + assert_array_equal(cmap((0.0, 0.0)), (1, 1, 1, 1)) + assert_allclose(cmap((1.0, 1.0)), (0, 0, 0, 1), atol=0.1) + + # check outside and bad + cs = cmap([(0., 0., 0., 1.2, np.nan), (0., 1.2, np.nan, 0., 0., )]) + assert_allclose(cs, [[1., 1., 1., 1.], + [0.801, 0.426, 0.119, 1.], + [0., 0., 0., 0.], + [0.199, 0.574, 0.881, 1.], + [0., 0., 0., 0.]]) + + assert_array_equal(cmap((0.0, 0.0), bytes=True), (255, 255, 255, 255)) + + with pytest.raises(ValueError, match="alpha is array-like but its shape"): + cs = cmap([(0, 5, 9), (0, 0, 0)], alpha=(0.5, 0.3)) + + with pytest.raises(ValueError, match="For the selected colormap the data"): + cs = cmap([(0, 5, 9), (0, 0, 0), (0, 0, 0)]) + + with pytest.raises(ValueError, match="clip cannot be false"): + cs = cmap([(0, 5, 9), (0, 0, 0)], bytes=True, clip=False) + # Tests calling a multivariate colormap with integer values + cmap = mpl.multivar_colormaps['2VarSubA'] + + # call only integers + cs = cmap([(0, 50, 100, 0, 0, 300), (0, 0, 0, 50, 100, 300)]) + res = np.array([[1, 1, 1, 1], + [0.85176471, 0.91029412, 0.96023529, 1], + [0.70452941, 0.82764706, 0.93358824, 1], + [0.94358824, 0.88505882, 0.83511765, 1], + [0.89729412, 0.77417647, 0.66823529, 1], + [0, 0, 0, 1]]) + assert_allclose(cs, res, atol=0.01) + + # call only integers, wrong byte order + swapped_dt = np.dtype(int).newbyteorder() + cs = cmap([np.array([0, 50, 100, 0, 0, 300], dtype=swapped_dt), + np.array([0, 0, 0, 50, 100, 300], dtype=swapped_dt)]) + assert_allclose(cs, res, atol=0.01) + + # call mix floats integers + # check calling with bytes = True + cs = cmap([(0, 50, 100, 0, 0, 300), (0, 0, 0, 50, 100, 300)], bytes=True) + res = np.array([[255, 255, 255, 255], + [217, 232, 244, 255], + [179, 211, 238, 255], + [240, 225, 212, 255], + [228, 197, 170, 255], + [0, 0, 0, 255]]) + assert_allclose(cs, res, atol=0.01) + + cs = cmap([(0, 50, 100, 0, 0, 300), (0, 0, 0, 50, 100, 300)], alpha=0.5) + res = np.array([[1, 1, 1, 0.5], + [0.85176471, 0.91029412, 0.96023529, 0.5], + [0.70452941, 0.82764706, 0.93358824, 0.5], + [0.94358824, 0.88505882, 0.83511765, 0.5], + [0.89729412, 0.77417647, 0.66823529, 0.5], + [0, 0, 0, 0.5]]) + assert_allclose(cs, res, atol=0.01) + # call with tuple + assert_allclose(cmap((100, 120), bytes=True, alpha=0.5), + [149, 142, 136, 127], atol=0.01) + + # alpha and bytes + cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)], bytes=True, alpha=0.5) + res = np.array([[0, 0, 255, 127], + [141, 0, 255, 127], + [255, 0, 255, 127], + [0, 115, 255, 127], + [0, 255, 255, 127], + [255, 255, 255, 127]]) + + # bad alpha shape + with pytest.raises(ValueError, match="alpha is array-like but its shape"): + cs = cmap([(0, 5, 9), (0, 0, 0)], bytes=True, alpha=(0.5, 0.3)) + + cmap = cmap.with_extremes(bad=(1, 1, 1, 1)) + cs = cmap([(0., 1.1, np.nan), (0., 1.2, 1.)]) + res = np.array([[1., 1., 1., 1.], + [0., 0., 0., 1.], + [1., 1., 1., 1.]]) + assert_allclose(cs, res, atol=0.01) + + # call outside with tuple + assert_allclose(cmap((300, 300), bytes=True, alpha=0.5), + [0, 0, 0, 127], atol=0.01) + with pytest.raises(ValueError, + match="For the selected colormap the data must have"): + cs = cmap((0, 5, 9)) + + # test over/under + cmap = mpl.multivar_colormaps['2VarAddA'] + with pytest.raises(ValueError, match='i.e. be of length 2'): + cmap.with_extremes(over=0) + with pytest.raises(ValueError, match='i.e. be of length 2'): + cmap.with_extremes(under=0) + + cmap = cmap.with_extremes(under=[(0, 0, 0, 0)]*2) + assert_allclose((0, 0, 0, 0), cmap((-1., 0)), atol=1e-2) + cmap = cmap.with_extremes(over=[(0, 0, 0, 0)]*2) + assert_allclose((0, 0, 0, 0), cmap((2., 0)), atol=1e-2) + + +def test_multivar_bad_mode(): + cmap = mpl.multivar_colormaps['2VarSubA'] + with pytest.raises(ValueError, match="is not a valid value for"): + cmap = mpl.colors.MultivarColormap(cmap[:], 'bad') + + +def test_multivar_resample(): + cmap = mpl.multivar_colormaps['3VarAddA'] + cmap_resampled = cmap.resampled((None, 10, 3)) + + assert_allclose(cmap_resampled[1](0.25), (0.093, 0.116, 0.059, 1.0)) + assert_allclose(cmap_resampled((0, 0.25, 0)), (0.093, 0.116, 0.059, 1.0)) + assert_allclose(cmap_resampled((1, 0.25, 1)), (0.417271, 0.264624, 0.274976, 1.), + atol=0.01) + + with pytest.raises(ValueError, match="lutshape must be of length"): + cmap = cmap.resampled(4) + + +def test_bivar_cmap_call_tuple(): + cmap = mpl.bivar_colormaps['BiOrangeBlue'] + assert_allclose(cmap((1.0, 1.0)), (1, 1, 1, 1), atol=0.01) + assert_allclose(cmap((0.0, 0.0)), (0, 0, 0, 1), atol=0.1) + assert_allclose(cmap((0.0, 0.0), alpha=0.1), (0, 0, 0, 0.1), atol=0.1) + + +def test_bivar_cmap_call(): + """ + Tests calling a bivariate colormap with integer values + """ + im = np.ones((10, 12, 4)) + im[:, :, 0] = np.linspace(0, 1, 10)[:, np.newaxis] + im[:, :, 1] = np.linspace(0, 1, 12)[np.newaxis, :] + cmap = mpl.colors.BivarColormapFromImage(im) + + # call only integers + cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)]) + res = np.array([[0, 0, 1, 1], + [0.556, 0, 1, 1], + [1, 0, 1, 1], + [0, 0.454, 1, 1], + [0, 1, 1, 1], + [1, 1, 1, 1]]) + assert_allclose(cs, res, atol=0.01) + # call only integers, wrong byte order + swapped_dt = np.dtype(int).newbyteorder() + cs = cmap([np.array([0, 5, 9, 0, 0, 10], dtype=swapped_dt), + np.array([0, 0, 0, 5, 11, 12], dtype=swapped_dt)]) + assert_allclose(cs, res, atol=0.01) + + # call mix floats integers + cmap = cmap.with_extremes(outside=(1, 0, 0, 0)) + cs = cmap([(0.5, 0), (0, 3)]) + res = np.array([[0.555, 0, 1, 1], + [0, 0.2727, 1, 1]]) + assert_allclose(cs, res, atol=0.01) + + # check calling with bytes = True + cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)], bytes=True) + res = np.array([[0, 0, 255, 255], + [141, 0, 255, 255], + [255, 0, 255, 255], + [0, 115, 255, 255], + [0, 255, 255, 255], + [255, 255, 255, 255]]) + assert_allclose(cs, res, atol=0.01) + + # test alpha + cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)], alpha=0.5) + res = np.array([[0, 0, 1, 0.5], + [0.556, 0, 1, 0.5], + [1, 0, 1, 0.5], + [0, 0.454, 1, 0.5], + [0, 1, 1, 0.5], + [1, 1, 1, 0.5]]) + assert_allclose(cs, res, atol=0.01) + # call with tuple + assert_allclose(cmap((10, 12), bytes=True, alpha=0.5), + [255, 255, 255, 127], atol=0.01) + + # alpha and bytes + cs = cmap([(0, 5, 9, 0, 0, 10), (0, 0, 0, 5, 11, 12)], bytes=True, alpha=0.5) + res = np.array([[0, 0, 255, 127], + [141, 0, 255, 127], + [255, 0, 255, 127], + [0, 115, 255, 127], + [0, 255, 255, 127], + [255, 255, 255, 127]]) + + # bad alpha shape + with pytest.raises(ValueError, match="alpha is array-like but its shape"): + cs = cmap([(0, 5, 9), (0, 0, 0)], bytes=True, alpha=(0.5, 0.3)) + + # set shape to 'ignore'. + # final point is outside colormap and should then receive + # the 'outside' (in this case [1,0,0,0]) + # also test 'bad' (in this case [1,1,1,0]) + cmap = cmap.with_extremes(outside=(1, 0, 0, 0), bad=(1, 1, 1, 0), shape='ignore') + cs = cmap([(0., 1.1, np.nan), (0., 1.2, 1.)]) + res = np.array([[0, 0, 1, 1], + [1, 0, 0, 0], + [1, 1, 1, 0]]) + assert_allclose(cs, res, atol=0.01) + # call outside with tuple + assert_allclose(cmap((10, 12), bytes=True, alpha=0.5), + [255, 0, 0, 127], atol=0.01) + # with integers + cs = cmap([(0, 10), (0, 12)]) + res = np.array([[0, 0, 1, 1], + [1, 0, 0, 0]]) + assert_allclose(cs, res, atol=0.01) + + with pytest.raises(ValueError, + match="For a `BivarColormap` the data must have"): + cs = cmap((0, 5, 9)) + + cmap = cmap.with_extremes(shape='circle') + with pytest.raises(NotImplementedError, + match="only implemented for use with with floats"): + cs = cmap([(0, 5, 9, 0, 0, 9), (0, 0, 0, 5, 11, 11)]) + + # test origin + cmap = mpl.bivar_colormaps['BiOrangeBlue'].with_extremes(origin=(0.5, 0.5)) + assert_allclose(cmap[0](0.5), + (0.50244140625, 0.5024222412109375, 0.50244140625, 1)) + assert_allclose(cmap[1](0.5), + (0.50244140625, 0.5024222412109375, 0.50244140625, 1)) + cmap = mpl.bivar_colormaps['BiOrangeBlue'].with_extremes(origin=(1, 1)) + assert_allclose(cmap[0](1.), + (0.99853515625, 0.9985467529296875, 0.99853515625, 1.0)) + assert_allclose(cmap[1](1.), + (0.99853515625, 0.9985467529296875, 0.99853515625, 1.0)) + with pytest.raises(KeyError, + match="only 0 or 1 are valid keys"): + cs = cmap[2] + + +def test_bivar_getitem(): + """Test __getitem__ on BivarColormap""" + xA = ([.0, .25, .5, .75, 1., -1, 2], [.5]*7) + xB = ([.5]*7, [.0, .25, .5, .75, 1., -1, 2]) + + cmaps = mpl.bivar_colormaps['BiPeak'] + assert_array_equal(cmaps(xA), cmaps[0](xA[0])) + assert_array_equal(cmaps(xB), cmaps[1](xB[1])) + + cmaps = cmaps.with_extremes(shape='ignore') + assert_array_equal(cmaps(xA), cmaps[0](xA[0])) + assert_array_equal(cmaps(xB), cmaps[1](xB[1])) + + xA = ([.0, .25, .5, .75, 1., -1, 2], [.0]*7) + xB = ([.0]*7, [.0, .25, .5, .75, 1., -1, 2]) + cmaps = mpl.bivar_colormaps['BiOrangeBlue'] + assert_array_equal(cmaps(xA), cmaps[0](xA[0])) + assert_array_equal(cmaps(xB), cmaps[1](xB[1])) + + cmaps = cmaps.with_extremes(shape='ignore') + assert_array_equal(cmaps(xA), cmaps[0](xA[0])) + assert_array_equal(cmaps(xB), cmaps[1](xB[1])) + + +def test_bivar_cmap_bad_shape(): + """ + Tests calling a bivariate colormap with integer values + """ + cmap = mpl.bivar_colormaps['BiCone'] + _ = cmap.lut + with pytest.raises(ValueError, + match="is not a valid value for shape"): + cmap.with_extremes(shape='bad_shape') + + with pytest.raises(ValueError, + match="is not a valid value for shape"): + mpl.colors.BivarColormapFromImage(np.ones((3, 3, 4)), + shape='bad_shape') + + +def test_bivar_cmap_bad_lut(): + """ + Tests calling a bivariate colormap with integer values + """ + with pytest.raises(ValueError, + match="The lut must be an array of shape"): + cmap = mpl.colors.BivarColormapFromImage(np.ones((3, 3, 5))) + + +def test_bivar_cmap_from_image(): + """ + This tests the creation and use of a bivariate colormap + generated from an image + """ + + data_0 = np.arange(6).reshape((2, 3))/5 + data_1 = np.arange(6).reshape((3, 2)).T/5 + + # bivariate colormap from array + cim = np.ones((10, 12, 3)) + cim[:, :, 0] = np.arange(10)[:, np.newaxis]/10 + cim[:, :, 1] = np.arange(12)[np.newaxis, :]/12 + + cmap = mpl.colors.BivarColormapFromImage(cim) + im = cmap((data_0, data_1)) + res = np.array([[[0, 0, 1, 1], + [0.2, 0.33333333, 1, 1], + [0.4, 0.75, 1, 1]], + [[0.6, 0.16666667, 1, 1], + [0.8, 0.58333333, 1, 1], + [0.9, 0.91666667, 1, 1]]]) + assert_allclose(im, res, atol=0.01) + + # input as unit8 + cim = np.ones((10, 12, 3))*255 + cim[:, :, 0] = np.arange(10)[:, np.newaxis]/10*255 + cim[:, :, 1] = np.arange(12)[np.newaxis, :]/12*255 + + cmap = mpl.colors.BivarColormapFromImage(cim.astype(np.uint8)) + im = cmap((data_0, data_1)) + res = np.array([[[0, 0, 1, 1], + [0.2, 0.33333333, 1, 1], + [0.4, 0.75, 1, 1]], + [[0.6, 0.16666667, 1, 1], + [0.8, 0.58333333, 1, 1], + [0.9, 0.91666667, 1, 1]]]) + assert_allclose(im, res, atol=0.01) + + # bivariate colormap from array + png_path = Path(__file__).parent / "baseline_images/pngsuite/basn2c16.png" + cim = Image.open(png_path) + cim = np.asarray(cim.convert('RGBA')) + + cmap = mpl.colors.BivarColormapFromImage(cim) + im = cmap((data_0, data_1), bytes=True) + res = np.array([[[255, 255, 0, 255], + [156, 206, 0, 255], + [49, 156, 49, 255]], + [[206, 99, 0, 255], + [99, 49, 107, 255], + [0, 0, 255, 255]]]) + assert_allclose(im, res, atol=0.01) + + +def test_bivar_resample(): + cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((2, 2)) + assert_allclose(cmap((0.25, 0.25)), (0, 0, 0, 1), atol=1e-2) + + cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((-2, 2)) + assert_allclose(cmap((0.25, 0.25)), (1., 0.5, 0., 1.), atol=1e-2) + + cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((2, -2)) + assert_allclose(cmap((0.25, 0.25)), (0., 0.5, 1., 1.), atol=1e-2) + + cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((-2, -2)) + assert_allclose(cmap((0.25, 0.25)), (1, 1, 1, 1), atol=1e-2) + + cmap = mpl.bivar_colormaps['BiOrangeBlue'].reversed() + assert_allclose(cmap((0.25, 0.25)), (0.748535, 0.748547, 0.748535, 1.), atol=1e-2) + cmap = mpl.bivar_colormaps['BiOrangeBlue'].transposed() + assert_allclose(cmap((0.25, 0.25)), (0.252441, 0.252422, 0.252441, 1.), atol=1e-2) + + with pytest.raises(ValueError, match="lutshape must be of length"): + cmap = cmap.resampled(4) + + +def test_bivariate_repr_png(): + cmap = mpl.bivar_colormaps['BiCone'] + png = cmap._repr_png_() + assert len(png) > 0 + img = Image.open(BytesIO(png)) + assert img.width > 0 + assert img.height > 0 + assert 'Title' in img.text + assert 'Description' in img.text + assert 'Author' in img.text + assert 'Software' in img.text + + +def test_bivariate_repr_html(): + cmap = mpl.bivar_colormaps['BiCone'] + html = cmap._repr_html_() + assert len(html) > 0 + png = cmap._repr_png_() + assert base64.b64encode(png).decode('ascii') in html + assert cmap.name in html + assert html.startswith('') + + +def test_multivariate_repr_png(): + cmap = mpl.multivar_colormaps['3VarAddA'] + png = cmap._repr_png_() + assert len(png) > 0 + img = Image.open(BytesIO(png)) + assert img.width > 0 + assert img.height > 0 + assert 'Title' in img.text + assert 'Description' in img.text + assert 'Author' in img.text + assert 'Software' in img.text + + +def test_multivariate_repr_html(): + cmap = mpl.multivar_colormaps['3VarAddA'] + html = cmap._repr_html_() + assert len(html) > 0 + for c in cmap: + png = c._repr_png_() + assert base64.b64encode(png).decode('ascii') in html + assert cmap.name in html + assert html.startswith('') + + +def test_bivar_eq(): + """ + Tests equality between multivariate colormaps + """ + cmap_0 = mpl.bivar_colormaps['BiPeak'] + + cmap_1 = mpl.bivar_colormaps['BiPeak'] + assert (cmap_0 == cmap_1) is True + + cmap_1 = mpl.multivar_colormaps['2VarAddA'] + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.bivar_colormaps['BiCone'] + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.bivar_colormaps['BiPeak'] + cmap_1 = cmap_1.with_extremes(bad='k') + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.bivar_colormaps['BiPeak'] + cmap_1 = cmap_1.with_extremes(outside='k') + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.bivar_colormaps['BiPeak'] + cmap_1._init() + cmap_1._lut *= 0.5 + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.bivar_colormaps['BiPeak'] + cmap_1 = cmap_1.with_extremes(shape='ignore') + assert (cmap_0 == cmap_1) is False + + +def test_multivar_eq(): + """ + Tests equality between multivariate colormaps + """ + cmap_0 = mpl.multivar_colormaps['2VarAddA'] + + cmap_1 = mpl.multivar_colormaps['2VarAddA'] + assert (cmap_0 == cmap_1) is True + + cmap_1 = mpl.bivar_colormaps['BiPeak'] + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.colors.MultivarColormap([cmap_0[0]]*2, + 'sRGB_add') + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.multivar_colormaps['3VarAddA'] + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.multivar_colormaps['2VarAddA'] + cmap_1 = cmap_1.with_extremes(bad='k') + assert (cmap_0 == cmap_1) is False + + cmap_1 = mpl.multivar_colormaps['2VarAddA'] + cmap_1 = mpl.colors.MultivarColormap(cmap_1[:], 'sRGB_sub') + assert (cmap_0 == cmap_1) is False From 2c3cae2c610e1e56696607030e618e9eca7de0ba Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 22 Aug 2024 22:04:14 -0400 Subject: [PATCH 0197/1230] TST: Add a test for FT2Image The only externally-available API here is `draw_rect_filled` and conversion to NumPy arrays via the buffer protocol. --- lib/matplotlib/tests/test_ft2font.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 2e2ce673f4b8..962732831a67 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -1,6 +1,8 @@ -from pathlib import Path +import itertools import io +from pathlib import Path +import numpy as np import pytest from matplotlib import ft2font @@ -9,6 +11,24 @@ import matplotlib.pyplot as plt +def test_ft2image_draw_rect_filled(): + width = 23 + height = 42 + for x0, y0, x1, y1 in itertools.product([1, 100], [2, 200], [4, 400], [8, 800]): + im = ft2font.FT2Image(width, height) + im.draw_rect_filled(x0, y0, x1, y1) + a = np.asarray(im) + assert a.dtype == np.uint8 + assert a.shape == (height, width) + if x0 == 100 or y0 == 200: + # All the out-of-bounds starts should get automatically clipped. + assert np.sum(a) == 0 + else: + # Otherwise, ends are clipped to the dimension, but are also _inclusive_. + filled = (min(x1 + 1, width) - x0) * (min(y1 + 1, height) - y0) + assert np.sum(a) == 255 * filled + + def test_fallback_errors(): file_name = fm.findfont('DejaVu Sans') From 720aed081b8a805ba5e6be1d841f5e2acce191c5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 23 Aug 2024 01:02:52 -0400 Subject: [PATCH 0198/1230] TST: Add tests for FT2Font attributes --- lib/matplotlib/tests/test_ft2font.py | 96 ++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 962732831a67..8e4957d8c036 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -29,6 +29,102 @@ def test_ft2image_draw_rect_filled(): assert np.sum(a) == 255 * filled +def test_ft2font_dejavu_attrs(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file) + assert font.fname == file + # Names extracted from FontForge: Font Information → PS Names tab. + assert font.postscript_name == 'DejaVuSans' + assert font.family_name == 'DejaVu Sans' + assert font.style_name == 'Book' + assert font.num_faces == 1 # Single TTF. + assert font.num_glyphs == 6241 # From compact encoding view in FontForge. + assert font.num_fixed_sizes == 0 # All glyphs are scalable. + assert font.num_charmaps == 5 + # Other internal flags are set, so only check the ones we're allowed to test. + expected_flags = (ft2font.SCALABLE | ft2font.SFNT | ft2font.HORIZONTAL | + ft2font.KERNING | ft2font.GLYPH_NAMES) + assert (font.face_flags & expected_flags) == expected_flags + assert font.style_flags == 0 # Not italic or bold. + assert font.scalable + # From FontForge: Font Information → General tab → entry name below. + assert font.units_per_EM == 2048 # Em Size. + assert font.underline_position == -175 # Underline position. + assert font.underline_thickness == 90 # Underline height. + # From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below. + assert font.ascender == 1901 # HHead Ascent. + assert font.descender == -483 # HHead Descent. + # Unconfirmed values. + assert font.height == 2384 + assert font.max_advance_width == 3838 + assert font.max_advance_height == 2384 + assert font.bbox == (-2090, -948, 3673, 2524) + + +def test_ft2font_cm_attrs(): + file = fm.findfont('cmtt10') + font = ft2font.FT2Font(file) + assert font.fname == file + # Names extracted from FontForge: Font Information → PS Names tab. + assert font.postscript_name == 'Cmtt10' + assert font.family_name == 'cmtt10' + assert font.style_name == 'Regular' + assert font.num_faces == 1 # Single TTF. + assert font.num_glyphs == 133 # From compact encoding view in FontForge. + assert font.num_fixed_sizes == 0 # All glyphs are scalable. + assert font.num_charmaps == 2 + # Other internal flags are set, so only check the ones we're allowed to test. + expected_flags = (ft2font.SCALABLE | ft2font.SFNT | ft2font.HORIZONTAL | + ft2font.GLYPH_NAMES) + assert (font.face_flags & expected_flags) == expected_flags, font.face_flags + assert font.style_flags == 0 # Not italic or bold. + assert font.scalable + # From FontForge: Font Information → General tab → entry name below. + assert font.units_per_EM == 2048 # Em Size. + assert font.underline_position == -143 # Underline position. + assert font.underline_thickness == 20 # Underline height. + # From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below. + assert font.ascender == 1276 # HHead Ascent. + assert font.descender == -489 # HHead Descent. + # Unconfirmed values. + assert font.height == 1765 + assert font.max_advance_width == 1536 + assert font.max_advance_height == 1765 + assert font.bbox == (-12, -477, 1280, 1430) + + +def test_ft2font_stix_bold_attrs(): + file = fm.findfont('STIXSizeTwoSym:bold') + font = ft2font.FT2Font(file) + assert font.fname == file + # Names extracted from FontForge: Font Information → PS Names tab. + assert font.postscript_name == 'STIXSizeTwoSym-Bold' + assert font.family_name == 'STIXSizeTwoSym' + assert font.style_name == 'Bold' + assert font.num_faces == 1 # Single TTF. + assert font.num_glyphs == 20 # From compact encoding view in FontForge. + assert font.num_fixed_sizes == 0 # All glyphs are scalable. + assert font.num_charmaps == 3 + # Other internal flags are set, so only check the ones we're allowed to test. + expected_flags = (ft2font.SCALABLE | ft2font.SFNT | ft2font.HORIZONTAL | + ft2font.GLYPH_NAMES) + assert (font.face_flags & expected_flags) == expected_flags, font.face_flags + assert font.style_flags == ft2font.BOLD + assert font.scalable + # From FontForge: Font Information → General tab → entry name below. + assert font.units_per_EM == 1000 # Em Size. + assert font.underline_position == -133 # Underline position. + assert font.underline_thickness == 20 # Underline height. + # From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below. + assert font.ascender == 2095 # HHead Ascent. + assert font.descender == -404 # HHead Descent. + # Unconfirmed values. + assert font.height == 2499 + assert font.max_advance_width == 1130 + assert font.max_advance_height == 2499 + assert font.bbox == (4, -355, 1185, 2095) + + def test_fallback_errors(): file_name = fm.findfont('DejaVu Sans') From a1b449865bf7c7252e5d42e19761c64868b357f2 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 23 Aug 2024 02:16:30 -0400 Subject: [PATCH 0199/1230] TST: Add tests for invalid FT2Font arguments --- lib/matplotlib/tests/test_ft2font.py | 44 ++++++++++++++++++---------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 8e4957d8c036..bc6e712e39b7 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -125,25 +125,39 @@ def test_ft2font_stix_bold_attrs(): assert font.bbox == (4, -355, 1185, 2095) -def test_fallback_errors(): - file_name = fm.findfont('DejaVu Sans') +def test_ft2font_invalid_args(tmp_path): + # filename argument. + with pytest.raises(TypeError, match='to a font file or a binary-mode file object'): + ft2font.FT2Font(None) + file = tmp_path / 'invalid-font.ttf' + file.write_text('This is not a valid font file.') + with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'), + file.open('rt') as fd): + ft2font.FT2Font(fd) + with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'), + file.open('wt') as fd): + ft2font.FT2Font(fd) + with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'), + file.open('wb') as fd): + ft2font.FT2Font(fd) - with pytest.raises(TypeError, match="Fallback list must be a list"): - # failing to be a list will fail before the 0 - ft2font.FT2Font(file_name, _fallback_list=(0,)) # type: ignore[arg-type] + file = fm.findfont('DejaVu Sans') - with pytest.raises( - TypeError, match="Fallback fonts must be FT2Font objects." - ): - ft2font.FT2Font(file_name, _fallback_list=[0]) # type: ignore[list-item] + # hinting_factor argument. + with pytest.raises(TypeError, match='cannot be interpreted as an integer'): + ft2font.FT2Font(file, 1.3) + with pytest.raises(ValueError, match='hinting_factor must be greater than 0'): + ft2font.FT2Font(file, 0) + with pytest.raises(TypeError, match='Fallback list must be a list'): + # failing to be a list will fail before the 0 + ft2font.FT2Font(file, _fallback_list=(0,)) # type: ignore[arg-type] + with pytest.raises(TypeError, match='Fallback fonts must be FT2Font objects.'): + ft2font.FT2Font(file, _fallback_list=[0]) # type: ignore[list-item] -def test_ft2font_positive_hinting_factor(): - file_name = fm.findfont('DejaVu Sans') - with pytest.raises( - ValueError, match="hinting_factor must be greater than 0" - ): - ft2font.FT2Font(file_name, 0) + # kerning_factor argument. + with pytest.raises(TypeError, match='cannot be interpreted as an integer'): + ft2font.FT2Font(file, _kerning_factor=1.3) @pytest.mark.parametrize('family_name, file_name', From fa8b39d019dc542500b5d74559932399db2a1ff7 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 23 Aug 2024 02:17:30 -0400 Subject: [PATCH 0200/1230] TYP: Add typing for internal _tri extension --- lib/matplotlib/_tri.pyi | 42 +++++++++++++--------- lib/matplotlib/tests/test_triangulation.py | 38 ++++++++++---------- src/tri/_tri.cpp | 2 +- 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/lib/matplotlib/_tri.pyi b/lib/matplotlib/_tri.pyi index b6e79d7140b3..a0c710fc2309 100644 --- a/lib/matplotlib/_tri.pyi +++ b/lib/matplotlib/_tri.pyi @@ -1,26 +1,36 @@ # This is a private module implemented in C++ -# As such these type stubs are overly generic, but here to allow these types -# as return types for public methods -from typing import Any, final +from typing import final + +import numpy as np +import numpy.typing as npt @final class TrapezoidMapTriFinder: - def __init__(self, *args, **kwargs) -> None: ... - def find_many(self, *args, **kwargs) -> Any: ... - def get_tree_stats(self, *args, **kwargs) -> Any: ... - def initialize(self, *args, **kwargs) -> Any: ... - def print_tree(self, *args, **kwargs) -> Any: ... + def __init__(self, triangulation: Triangulation): ... + def find_many(self, x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]) -> npt.NDArray[np.int_]: ... + def get_tree_stats(self) -> list[int | float]: ... + def initialize(self) -> None: ... + def print_tree(self) -> None: ... @final class TriContourGenerator: - def __init__(self, *args, **kwargs) -> None: ... - def create_contour(self, *args, **kwargs) -> Any: ... - def create_filled_contour(self, *args, **kwargs) -> Any: ... + def __init__(self, triangulation: Triangulation, z: npt.NDArray[np.float64]): ... + def create_contour(self, level: float) -> tuple[list[float], list[int]]: ... + def create_filled_contour(self, lower_level: float, upper_level: float) -> tuple[list[float], list[int]]: ... @final class Triangulation: - def __init__(self, *args, **kwargs) -> None: ... - def calculate_plane_coefficients(self, *args, **kwargs) -> Any: ... - def get_edges(self, *args, **kwargs) -> Any: ... - def get_neighbors(self, *args, **kwargs) -> Any: ... - def set_mask(self, *args, **kwargs) -> Any: ... + def __init__( + self, + x: npt.NDArray[np.float64], + y: npt.NDArray[np.float64], + triangles: npt.NDArray[np.int_], + mask: npt.NDArray[np.bool_] | tuple[()], + edges: npt.NDArray[np.int_] | tuple[()], + neighbors: npt.NDArray[np.int_] | tuple[()], + correct_triangle_orientation: bool, + ): ... + def calculate_plane_coefficients(self, z: npt.ArrayLike) -> npt.NDArray[np.float64]: ... + def get_edges(self) -> npt.NDArray[np.int_]: ... + def get_neighbors(self) -> npt.NDArray[np.int_]: ... + def set_mask(self, mask: npt.NDArray[np.bool_] | tuple[()]) -> None: ... diff --git a/lib/matplotlib/tests/test_triangulation.py b/lib/matplotlib/tests/test_triangulation.py index 6e3ec9628fcc..337443eb1e27 100644 --- a/lib/matplotlib/tests/test_triangulation.py +++ b/lib/matplotlib/tests/test_triangulation.py @@ -1181,7 +1181,7 @@ def test_tricontourf_decreasing_levels(): plt.tricontourf(x, y, z, [1.0, 0.0]) -def test_internal_cpp_api(): +def test_internal_cpp_api() -> None: # Following github issue 8197. from matplotlib import _tri # noqa: F401, ensure lazy-loaded module *is* loaded. @@ -1189,35 +1189,36 @@ def test_internal_cpp_api(): with pytest.raises( TypeError, match=r'__init__\(\): incompatible constructor arguments.'): - mpl._tri.Triangulation() + mpl._tri.Triangulation() # type: ignore[call-arg] with pytest.raises( ValueError, match=r'x and y must be 1D arrays of the same length'): - mpl._tri.Triangulation([], [1], [[]], (), (), (), False) + mpl._tri.Triangulation(np.array([]), np.array([1]), np.array([[]]), (), (), (), + False) - x = [0, 1, 1] - y = [0, 0, 1] + x = np.array([0, 1, 1], dtype=np.float64) + y = np.array([0, 0, 1], dtype=np.float64) with pytest.raises( ValueError, match=r'triangles must be a 2D array of shape \(\?,3\)'): - mpl._tri.Triangulation(x, y, [[0, 1]], (), (), (), False) + mpl._tri.Triangulation(x, y, np.array([[0, 1]]), (), (), (), False) - tris = [[0, 1, 2]] + tris = np.array([[0, 1, 2]], dtype=np.int_) with pytest.raises( ValueError, match=r'mask must be a 1D array with the same length as the ' r'triangles array'): - mpl._tri.Triangulation(x, y, tris, [0, 1], (), (), False) + mpl._tri.Triangulation(x, y, tris, np.array([0, 1]), (), (), False) with pytest.raises( ValueError, match=r'edges must be a 2D array with shape \(\?,2\)'): - mpl._tri.Triangulation(x, y, tris, (), [[1]], (), False) + mpl._tri.Triangulation(x, y, tris, (), np.array([[1]]), (), False) with pytest.raises( ValueError, match=r'neighbors must be a 2D array with the same shape as the ' r'triangles array'): - mpl._tri.Triangulation(x, y, tris, (), (), [[-1]], False) + mpl._tri.Triangulation(x, y, tris, (), (), np.array([[-1]]), False) triang = mpl._tri.Triangulation(x, y, tris, (), (), (), False) @@ -1232,9 +1233,9 @@ def test_internal_cpp_api(): ValueError, match=r'mask must be a 1D array with the same length as the ' r'triangles array'): - triang.set_mask(mask) + triang.set_mask(mask) # type: ignore[arg-type] - triang.set_mask([True]) + triang.set_mask(np.array([True])) assert_array_equal(triang.get_edges(), np.empty((0, 2))) triang.set_mask(()) # Equivalent to Python Triangulation mask=None @@ -1244,15 +1245,14 @@ def test_internal_cpp_api(): with pytest.raises( TypeError, match=r'__init__\(\): incompatible constructor arguments.'): - mpl._tri.TriContourGenerator() + mpl._tri.TriContourGenerator() # type: ignore[call-arg] with pytest.raises( ValueError, - match=r'z must be a 1D array with the same length as the x and y ' - r'arrays'): - mpl._tri.TriContourGenerator(triang, [1]) + match=r'z must be a 1D array with the same length as the x and y arrays'): + mpl._tri.TriContourGenerator(triang, np.array([1])) - z = [0, 1, 2] + z = np.array([0, 1, 2]) tcg = mpl._tri.TriContourGenerator(triang, z) with pytest.raises( @@ -1263,13 +1263,13 @@ def test_internal_cpp_api(): with pytest.raises( TypeError, match=r'__init__\(\): incompatible constructor arguments.'): - mpl._tri.TrapezoidMapTriFinder() + mpl._tri.TrapezoidMapTriFinder() # type: ignore[call-arg] trifinder = mpl._tri.TrapezoidMapTriFinder(triang) with pytest.raises( ValueError, match=r'x and y must be array-like with same shape'): - trifinder.find_many([0], [0, 1]) + trifinder.find_many(np.array([0]), np.array([0, 1])) def test_qhull_large_offset(): diff --git a/src/tri/_tri.cpp b/src/tri/_tri.cpp index 5c01dbfa681c..908136081971 100644 --- a/src/tri/_tri.cpp +++ b/src/tri/_tri.cpp @@ -1314,7 +1314,7 @@ TrapezoidMapTriFinder::TriIndexArray TrapezoidMapTriFinder::find_many(const CoordinateArray& x, const CoordinateArray& y) { - if (x.ndim() != 1 || x.shape(0) != y.shape(0)) + if (x.ndim() != 1 || y.ndim() != 1 || x.shape(0) != y.shape(0)) throw std::invalid_argument( "x and y must be array-like with same shape"); From 2387233bfa860064310a55ffb1d950dc90628eda Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 23 Aug 2024 04:10:11 -0400 Subject: [PATCH 0201/1230] TST: Add tests for FT2Font.clear Also, ensure some internals are initialized/cleared. --- lib/matplotlib/tests/test_ft2font.py | 16 ++++++++++++++++ src/ft2font.cpp | 5 +++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index bc6e712e39b7..986f3f5e5a82 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -160,6 +160,22 @@ def test_ft2font_invalid_args(tmp_path): ft2font.FT2Font(file, _kerning_factor=1.3) +def test_ft2font_clear(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file) + assert font.get_num_glyphs() == 0 + assert font.get_width_height() == (0, 0) + assert font.get_bitmap_offset() == (0, 0) + font.set_text('ABabCDcd') + assert font.get_num_glyphs() == 8 + assert font.get_width_height() != (0, 0) + assert font.get_bitmap_offset() != (0, 0) + font.clear() + assert font.get_num_glyphs() == 0 + assert font.get_width_height() == (0, 0) + assert font.get_bitmap_offset() == (0, 0) + + @pytest.mark.parametrize('family_name, file_name', [("WenQuanYi Zen Hei", "wqy-zenhei.ttc"), ("Noto Sans CJK JP", "NotoSansCJK.ttc"), diff --git a/src/ft2font.cpp b/src/ft2font.cpp index cb9952f3b374..34a602562735 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -304,8 +304,9 @@ FT2Font::~FT2Font() void FT2Font::clear() { - pen.x = 0; - pen.y = 0; + pen.x = pen.y = 0; + bbox.xMin = bbox.yMin = bbox.xMax = bbox.yMax = 0; + advance = 0; for (size_t i = 0; i < glyphs.size(); i++) { FT_Done_Glyph(glyphs[i]); From 6bad7f09906c5d748c54a54e766667cd29c16fab Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 23 Aug 2024 21:44:48 -0400 Subject: [PATCH 0202/1230] TST: Add tests for FT2Font charmaps --- lib/matplotlib/tests/test_ft2font.py | 82 ++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 986f3f5e5a82..59471e023492 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -176,6 +176,88 @@ def test_ft2font_clear(): assert font.get_bitmap_offset() == (0, 0) +def test_ft2font_charmaps(): + def enc(name): + # We don't expose the encoding enum from FreeType, but can generate it here. + # For DejaVu, there are 5 charmaps, but only 2 have enum entries in FreeType. + e = 0 + for x in name: + e <<= 8 + e += ord(x) + return e + + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file) + assert font.num_charmaps == 5 + + # Unicode. + font.select_charmap(enc('unic')) + unic = font.get_charmap() + font.set_charmap(0) # Unicode platform, Unicode BMP only. + after = font.get_charmap() + assert len(after) <= len(unic) + for chr, glyph in after.items(): + assert unic[chr] == glyph == font.get_char_index(chr) + font.set_charmap(1) # Unicode platform, modern subtable. + after = font.get_charmap() + assert unic == after + font.set_charmap(3) # Windows platform, Unicode BMP only. + after = font.get_charmap() + assert len(after) <= len(unic) + for chr, glyph in after.items(): + assert unic[chr] == glyph == font.get_char_index(chr) + font.set_charmap(4) # Windows platform, Unicode full repertoire, modern subtable. + after = font.get_charmap() + assert unic == after + + # This is just a random sample from FontForge. + glyph_names = { + 'non-existent-glyph-name': 0, + 'plusminus': 115, + 'Racute': 278, + 'perthousand': 2834, + 'seveneighths': 3057, + 'triagup': 3721, + 'uni01D3': 405, + 'uni0417': 939, + 'uni2A02': 4464, + 'u1D305': 5410, + 'u1F0A1': 5784, + } + for name, index in glyph_names.items(): + assert font.get_name_index(name) == index + if name == 'non-existent-glyph-name': + name = '.notdef' + # This doesn't always apply, but it does for DejaVu Sans. + assert font.get_glyph_name(index) == name + + # Apple Roman. + font.select_charmap(enc('armn')) + armn = font.get_charmap() + font.set_charmap(2) # Macintosh platform, Roman. + after = font.get_charmap() + assert armn == after + assert len(armn) <= 256 # 8-bit encoding. + # The first 128 characters of Apple Roman match ASCII, which also matches Unicode. + for o in range(1, 128): + if o not in armn or o not in unic: + continue + assert unic[o] == armn[o] + # Check a couple things outside the ASCII set that are different in each charset. + examples = [ + # (Unicode, Macintosh) + (0x2020, 0xA0), # Dagger. + (0x00B0, 0xA1), # Degree symbol. + (0x00A3, 0xA3), # Pound sign. + (0x00A7, 0xA4), # Section sign. + (0x00B6, 0xA6), # Pilcrow. + (0x221E, 0xB0), # Infinity symbol. + ] + for u, m in examples: + # Though the encoding is different, the glyph should be the same. + assert unic[u] == armn[m] + + @pytest.mark.parametrize('family_name, file_name', [("WenQuanYi Zen Hei", "wqy-zenhei.ttc"), ("Noto Sans CJK JP", "NotoSansCJK.ttc"), From 2754aaa42774aa89a6172b22140b86bb118223c1 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 24 Aug 2024 03:22:06 -0400 Subject: [PATCH 0203/1230] TST: Add tests for FT2Font.get_sfnt{,_table} Also, correct the reading of creation and modified dates. --- lib/matplotlib/tests/test_ft2font.py | 424 +++++++++++++++++++++++++++ src/ft2font_wrapper.cpp | 12 +- 2 files changed, 433 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 59471e023492..f3bd44a3539a 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -258,6 +258,430 @@ def enc(name): assert unic[u] == armn[m] +_expected_sfnt_names = { + 'DejaVu Sans': { + 0: 'Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.\n' + 'Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.\n' + 'DejaVu changes are in public domain\n', + 1: 'DejaVu Sans', + 2: 'Book', + 3: 'DejaVu Sans', + 4: 'DejaVu Sans', + 5: 'Version 2.35', + 6: 'DejaVuSans', + 8: 'DejaVu fonts team', + 11: 'http://dejavu.sourceforge.net', + 13: 'Fonts are (c) Bitstream (see below). ' + 'DejaVu changes are in public domain. ' + '''Glyphs imported from Arev fonts are (c) Tavmjung Bah (see below) + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute the +Font Software, including without limitation the rights to use, copy, merge, +publish, distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to the +following conditions: + +The above copyright and trademark notices and this permission notice shall +be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional glyphs or characters may be added to the Fonts, only if the fonts +are renamed to names not containing either the words "Bitstream" or the word +"Vera". + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and is distributed under the "Bitstream +Vera" names. + +The Font Software may be sold as part of a larger software package but no +copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING +ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. ''' ''' + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +associated documentation files (the "Font Software"), to reproduce +and distribute the modifications to the Bitstream Vera Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to +the following conditions: + +The above copyright and trademark notices and this permission notice +shall be included in all copies of one or more of the Font Software +typefaces. + +The Font Software may be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may be +modified and additional glyphs or characters may be added to the +Fonts, only if the fonts are renamed to names not containing either +the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts +or Font Software that has been modified and is distributed under the ''' ''' +"Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but +no copy of one or more of the Font Software typefaces may be sold by +itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL +TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr.''', + 14: 'http://dejavu.sourceforge.net/wiki/index.php/License', + 16: 'DejaVu Sans', + 17: 'Book', + }, + 'cmtt10': { + 0: 'Copyright (C) 1994, Basil K. Malyshev. All Rights Reserved.' + '012BaKoMa Fonts Collection, Level-B.', + 1: 'cmtt10', + 2: 'Regular', + 3: 'FontMonger:cmtt10', + 4: 'cmtt10', + 5: '1.1/12-Nov-94', + 6: 'Cmtt10', + }, + 'STIXSizeTwoSym:bold': { + 0: 'Copyright (c) 2001-2010 by the STI Pub Companies, consisting of the ' + 'American Chemical Society, the American Institute of Physics, the American ' + 'Mathematical Society, the American Physical Society, Elsevier, Inc., and ' + 'The Institute of Electrical and Electronic Engineers, Inc. Portions ' + 'copyright (c) 1998-2003 by MicroPress, Inc. Portions copyright (c) 1990 by ' + 'Elsevier, Inc. All rights reserved.', + 1: 'STIXSizeTwoSym', + 2: 'Bold', + 3: 'FontMaster:STIXSizeTwoSym-Bold:1.0.0', + 4: 'STIXSizeTwoSym-Bold', + 5: 'Version 1.0.0', + 6: 'STIXSizeTwoSym-Bold', + 7: 'STIX Fonts(TM) is a trademark of The Institute of Electrical and ' + 'Electronics Engineers, Inc.', + 9: 'MicroPress Inc., with final additions and corrections provided by Coen ' + 'Hoffman, Elsevier (retired)', + 10: 'Arie de Ruiter, who in 1995 was Head of Information Technology ' + 'Development at Elsevier Science, made a proposal to the STI Pub group, an ' + 'informal group of publishers consisting of representatives from the ' + 'American Chemical Society (ACS), American Institute of Physics (AIP), ' + 'American Mathematical Society (AMS), American Physical Society (APS), ' + 'Elsevier, and Institute of Electrical and Electronics Engineers (IEEE). ' + 'De Ruiter encouraged the members to consider development of a series of ' + 'Web fonts, which he proposed should be called the Scientific and ' + 'Technical Information eXchange, or STIX, Fonts. All STI Pub member ' + 'organizations enthusiastically endorsed this proposal, and the STI Pub ' + 'group agreed to embark on what has become a twelve-year project. The goal ' + 'of the project was to identify all alphabetic, symbolic, and other ' + 'special characters used in any facet of scientific publishing and to ' + 'create a set of Unicode-based fonts that would be distributed free to ' + 'every scientist, student, and other interested party worldwide. The fonts ' + 'would be consistent with the emerging Unicode standard, and would permit ' + 'universal representation of every character. With the release of the STIX ' + "fonts, de Ruiter's vision has been realized.", + 11: 'http://www.stixfonts.org', + 12: 'http://www.micropress-inc.com', + 13: 'As a condition for receiving these fonts at no charge, each person ' + 'downloading the fonts must agree to some simple license terms. The ' + 'license is based on the SIL Open Font License ' + '. The ' + 'SIL License is a free and open source license specifically designed for ' + 'fonts and related software. The basic terms are that the recipient will ' + 'not remove the copyright and trademark statements from the fonts and ' + 'that, if the person decides to create a derivative work based on the STIX ' + 'Fonts but incorporating some changes or enhancements, the derivative work ' + '("Modified Version") will carry a different name. The copyright and ' + 'trademark restrictions are part of the agreement between the STI Pub ' + 'companies and the typeface designer. The "renaming" restriction results ' + 'from the desire of the STI Pub companies to assure that the STIX Fonts ' + 'will continue to function in a predictable fashion for all that use them. ' + 'No copy of one or more of the individual Font typefaces that form the ' + 'STIX Fonts(TM) set may be sold by itself, but other than this one ' + 'restriction, licensees are free to sell the fonts either separately or as ' + 'part of a package that combines other software or fonts with this font ' + 'set.', + 14: 'http://www.stixfonts.org/user_license.html', + }, +} + + +@pytest.mark.parametrize('font_name, expected', _expected_sfnt_names.items(), + ids=_expected_sfnt_names.keys()) +def test_ft2font_get_sfnt(font_name, expected): + file = fm.findfont(font_name) + font = ft2font.FT2Font(file) + sfnt = font.get_sfnt() + for name, value in expected.items(): + # Macintosh, Unicode 1.0, English, name. + assert sfnt.pop((1, 0, 0, name)) == value.encode('ascii') + # Microsoft, Unicode, English United States, name. + assert sfnt.pop((3, 1, 1033, name)) == value.encode('utf-16be') + assert sfnt == {} + + +_expected_sfnt_tables = { + 'DejaVu Sans': { + 'invalid': None, + 'head': { + 'version': (1, 0), + 'fontRevision': (2, 22937), + 'checkSumAdjustment': -175678572, + 'magicNumber': 0x5F0F3CF5, + 'flags': 31, + 'unitsPerEm': 2048, + 'created': (0, 3514699492), 'modified': (0, 3514699492), + 'xMin': -2090, 'yMin': -948, 'xMax': 3673, 'yMax': 2524, + 'macStyle': 0, + 'lowestRecPPEM': 8, + 'fontDirectionHint': 0, + 'indexToLocFormat': 1, + 'glyphDataFormat': 0, + }, + 'maxp': { + 'version': (1, 0), + 'numGlyphs': 6241, + 'maxPoints': 852, 'maxComponentPoints': 104, 'maxTwilightPoints': 16, + 'maxContours': 43, 'maxComponentContours': 12, + 'maxZones': 2, + 'maxStorage': 153, + 'maxFunctionDefs': 64, + 'maxInstructionDefs': 0, + 'maxStackElements': 1045, + 'maxSizeOfInstructions': 534, + 'maxComponentElements': 8, + 'maxComponentDepth': 4, + }, + 'OS/2': { + 'version': 1, + 'xAvgCharWidth': 1038, + 'usWeightClass': 400, 'usWidthClass': 5, + 'fsType': 0, + 'ySubscriptXSize': 1331, 'ySubscriptYSize': 1433, + 'ySubscriptXOffset': 0, 'ySubscriptYOffset': 286, + 'ySuperscriptXSize': 1331, 'ySuperscriptYSize': 1433, + 'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 983, + 'yStrikeoutSize': 102, 'yStrikeoutPosition': 530, + 'sFamilyClass': 0, + 'panose': b'\x02\x0b\x06\x03\x03\x08\x04\x02\x02\x04', + 'ulCharRange': (3875565311, 3523280383, 170156073, 67117068), + 'achVendID': b'PfEd', + 'fsSelection': 64, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 65535, + }, + 'hhea': { + 'version': (1, 0), + 'ascent': 1901, 'descent': -483, 'lineGap': 0, + 'advanceWidthMax': 3838, + 'minLeftBearing': -2090, 'minRightBearing': -1455, + 'xMaxExtent': 3673, + 'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0, + 'metricDataFormat': 0, 'numOfLongHorMetrics': 6226, + }, + 'vhea': None, + 'post': { + 'format': (2, 0), + 'isFixedPitch': 0, 'italicAngle': (0, 0), + 'underlinePosition': -130, 'underlineThickness': 90, + 'minMemType42': 0, 'maxMemType42': 0, + 'minMemType1': 0, 'maxMemType1': 0, + }, + 'pclt': None, + }, + 'cmtt10': { + 'invalid': None, + 'head': { + 'version': (1, 0), + 'fontRevision': (1, 0), + 'checkSumAdjustment': 555110277, + 'magicNumber': 0x5F0F3CF5, + 'flags': 3, + 'unitsPerEm': 2048, + 'created': (0, 0), 'modified': (0, 0), + 'xMin': -12, 'yMin': -477, 'xMax': 1280, 'yMax': 1430, + 'macStyle': 0, + 'lowestRecPPEM': 6, + 'fontDirectionHint': 2, + 'indexToLocFormat': 1, + 'glyphDataFormat': 0, + }, + 'maxp': { + 'version': (1, 0), + 'numGlyphs': 133, + 'maxPoints': 94, 'maxComponentPoints': 0, 'maxTwilightPoints': 12, + 'maxContours': 5, 'maxComponentContours': 0, + 'maxZones': 2, + 'maxStorage': 6, + 'maxFunctionDefs': 64, + 'maxInstructionDefs': 0, + 'maxStackElements': 200, + 'maxSizeOfInstructions': 100, + 'maxComponentElements': 4, + 'maxComponentDepth': 1, + }, + 'OS/2': { + 'version': 0, + 'xAvgCharWidth': 1075, + 'usWeightClass': 400, 'usWidthClass': 5, + 'fsType': 0, + 'ySubscriptXSize': 410, 'ySubscriptYSize': 369, + 'ySubscriptXOffset': 0, 'ySubscriptYOffset': -469, + 'ySuperscriptXSize': 410, 'ySuperscriptYSize': 369, + 'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 1090, + 'yStrikeoutSize': 102, 'yStrikeoutPosition': 530, + 'sFamilyClass': 0, + 'panose': b'\x02\x0b\x05\x00\x00\x00\x00\x00\x00\x00', + 'ulCharRange': (0, 0, 0, 0), + 'achVendID': b'\x00\x00\x00\x00', + 'fsSelection': 64, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 9835, + }, + 'hhea': { + 'version': (1, 0), + 'ascent': 1276, 'descent': -489, 'lineGap': 0, + 'advanceWidthMax': 1536, + 'minLeftBearing': -12, 'minRightBearing': -29, + 'xMaxExtent': 1280, + 'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0, + 'metricDataFormat': 0, 'numOfLongHorMetrics': 133, + }, + 'vhea': None, + 'post': { + 'format': (2, 0), + 'isFixedPitch': 0, 'italicAngle': (0, 0), + 'underlinePosition': -133, 'underlineThickness': 20, + 'minMemType42': 0, 'maxMemType42': 0, + 'minMemType1': 0, 'maxMemType1': 0, + }, + 'pclt': { + 'version': (1, 0), + 'fontNumber': 2147483648, + 'pitch': 1075, + 'xHeight': 905, + 'style': 0, + 'typeFamily': 0, + 'capHeight': 1276, + 'symbolSet': 0, + 'typeFace': b'cmtt10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + 'characterComplement': b'\xff\xff\xff\xff7\xff\xff\xfe', + 'strokeWeight': 0, + 'widthType': -5, + 'serifStyle': 64, + }, + }, + 'STIXSizeTwoSym:bold': { + 'invalid': None, + 'head': { + 'version': (1, 0), + 'fontRevision': (1, 0), + 'checkSumAdjustment': 1803408080, + 'magicNumber': 0x5F0F3CF5, + 'flags': 11, + 'unitsPerEm': 1000, + 'created': (0, 3359035786), 'modified': (0, 3359035786), + 'xMin': 4, 'yMin': -355, 'xMax': 1185, 'yMax': 2095, + 'macStyle': 1, + 'lowestRecPPEM': 8, + 'fontDirectionHint': 2, + 'indexToLocFormat': 0, + 'glyphDataFormat': 0, + }, + 'maxp': { + 'version': (1, 0), + 'numGlyphs': 20, + 'maxPoints': 37, 'maxComponentPoints': 0, 'maxTwilightPoints': 0, + 'maxContours': 1, 'maxComponentContours': 0, + 'maxZones': 2, + 'maxStorage': 1, + 'maxFunctionDefs': 64, + 'maxInstructionDefs': 0, + 'maxStackElements': 64, + 'maxSizeOfInstructions': 0, + 'maxComponentElements': 0, + 'maxComponentDepth': 0, + }, + 'OS/2': { + 'version': 2, + 'xAvgCharWidth': 598, + 'usWeightClass': 700, 'usWidthClass': 5, + 'fsType': 0, + 'ySubscriptXSize': 500, 'ySubscriptYSize': 500, + 'ySubscriptXOffset': 0, 'ySubscriptYOffset': 250, + 'ySuperscriptXSize': 500, 'ySuperscriptYSize': 500, + 'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 500, + 'yStrikeoutSize': 20, 'yStrikeoutPosition': 1037, + 'sFamilyClass': 0, + 'panose': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + 'ulCharRange': (3, 192, 0, 0), + 'achVendID': b'STIX', + 'fsSelection': 32, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 10217, + }, + 'hhea': { + 'version': (1, 0), + 'ascent': 2095, 'descent': -404, 'lineGap': 0, + 'advanceWidthMax': 1130, + 'minLeftBearing': 0, 'minRightBearing': -55, + 'xMaxExtent': 1185, + 'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0, + 'metricDataFormat': 0, 'numOfLongHorMetrics': 19, + }, + 'vhea': None, + 'post': { + 'format': (2, 0), + 'isFixedPitch': 0, 'italicAngle': (0, 0), + 'underlinePosition': -123, 'underlineThickness': 20, + 'minMemType42': 0, 'maxMemType42': 0, + 'minMemType1': 0, 'maxMemType1': 0, + }, + 'pclt': None, + }, +} + + +@pytest.mark.parametrize('font_name', _expected_sfnt_tables.keys()) +@pytest.mark.parametrize('header', _expected_sfnt_tables['DejaVu Sans'].keys()) +def test_ft2font_get_sfnt_table(font_name, header): + file = fm.findfont(font_name) + font = ft2font.FT2Font(file) + assert font.get_sfnt_table(header) == _expected_sfnt_tables[font_name][header] + + @pytest.mark.parametrize('family_name, file_name', [("WenQuanYi Zen Hei", "wqy-zenhei.ttc"), ("Noto Sans CJK JP", "NotoSansCJK.ttc"), diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 6d6e8722b63b..e5a50c0d6f1e 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -1079,7 +1079,7 @@ static PyObject *PyFT2Font_get_sfnt_table(PyFT2Font *self, PyObject *args) case 0: { char head_dict[] = "{s:(h,H), s:(h,H), s:l, s:l, s:H, s:H," - "s:(l,l), s:(l,l), s:h, s:h, s:h, s:h, s:H, s:H, s:h, s:h, s:h}"; + "s:(I,I), s:(I,I), s:h, s:h, s:h, s:h, s:H, s:H, s:h, s:h, s:h}"; TT_Header *t = (TT_Header *)table; return Py_BuildValue(head_dict, "version", FIXED_MAJOR(t->Table_Version), FIXED_MINOR(t->Table_Version), @@ -1088,8 +1088,14 @@ static PyObject *PyFT2Font_get_sfnt_table(PyFT2Font *self, PyObject *args) "magicNumber", t->Magic_Number, "flags", t->Flags, "unitsPerEm", t->Units_Per_EM, - "created", t->Created[0], t->Created[1], - "modified", t->Modified[0], t->Modified[1], + // FreeType 2.6.1 defines these two timestamps as FT_Long, + // but they should be unsigned (fixed in 2.10.0): + // https://gitlab.freedesktop.org/freetype/freetype/-/commit/3e8ec291ffcfa03c8ecba1cdbfaa55f5577f5612 + // It's actually read from the file structure as two 32-bit + // values, so we need to cast down in size to prevent sign + // extension from producing huge 64-bit values. + "created", static_cast(t->Created[0]), static_cast(t->Created[1]), + "modified", static_cast(t->Modified[0]), static_cast(t->Modified[1]), "xMin", t->xMin, "yMin", t->yMin, "xMax", t->xMax, From 3748c992c82688eb015e3ddc782678f2e045606f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 24 Aug 2024 05:32:52 -0400 Subject: [PATCH 0204/1230] TST: Add tests for FT2Font.get_kerning --- lib/matplotlib/tests/test_ft2font.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index f3bd44a3539a..3156324fef0a 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -682,6 +682,30 @@ def test_ft2font_get_sfnt_table(font_name, header): assert font.get_sfnt_table(header) == _expected_sfnt_tables[font_name][header] +@pytest.mark.parametrize('left, right, unscaled, unfitted, default', [ + # These are all the same class. + ('A', 'A', 57, 248, 256), ('A', 'À', 57, 248, 256), ('A', 'Á', 57, 248, 256), + ('A', 'Â', 57, 248, 256), ('A', 'Ã', 57, 248, 256), ('A', 'Ä', 57, 248, 256), + # And a few other random ones. + ('D', 'A', -36, -156, -128), ('T', '.', -243, -1056, -1024), + ('X', 'C', -149, -647, -640), ('-', 'J', 114, 495, 512), +]) +def test_ft2font_get_kerning(left, right, unscaled, unfitted, default): + file = fm.findfont('DejaVu Sans') + # With unscaled, these settings should produce exact values found in FontForge. + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font.set_size(100, 100) + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + ft2font.KERNING_UNSCALED) == unscaled + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + ft2font.KERNING_UNFITTED) == unfitted + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + ft2font.KERNING_DEFAULT) == default + + @pytest.mark.parametrize('family_name, file_name', [("WenQuanYi Zen Hei", "wqy-zenhei.ttc"), ("Noto Sans CJK JP", "NotoSansCJK.ttc"), From 833a2593b694145ee7912737ce33274690851cf2 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 27 Aug 2024 19:05:10 -0400 Subject: [PATCH 0205/1230] Fix return value of FT2Font.set_text and add tests for it `PyArray_SimpleNewFromData` does not copy its input data, and the `std::vector` is a local variable that disappears after the C++ method returns. --- lib/matplotlib/tests/test_ft2font.py | 21 +++++++++++++++++++++ src/ft2font_wrapper.cpp | 7 +++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 3156324fef0a..0c2eb31580b8 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -706,6 +706,27 @@ def test_ft2font_get_kerning(left, right, unscaled, unfitted, default): ft2font.KERNING_DEFAULT) == default +def test_ft2font_set_text(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + xys = font.set_text('') + np.testing.assert_array_equal(xys, np.empty((0, 2))) + assert font.get_width_height() == (0, 0) + assert font.get_num_glyphs() == 0 + assert font.get_descent() == 0 + assert font.get_bitmap_offset() == (0, 0) + # This string uses all the kerning pairs defined for test_ft2font_get_kerning. + xys = font.set_text('AADAT.XC-J') + np.testing.assert_array_equal( + xys, + [(0, 0), (512, 0), (1024, 0), (1600, 0), (2112, 0), (2496, 0), (2688, 0), + (3200, 0), (3712, 0), (4032, 0)]) + assert font.get_width_height() == (4288, 768) + assert font.get_num_glyphs() == 10 + assert font.get_descent() == 192 + assert font.get_bitmap_offset() == (6, 0) + + @pytest.mark.parametrize('family_name, file_name', [("WenQuanYi Zen Hei", "wqy-zenhei.ttc"), ("Noto Sans CJK JP", "NotoSansCJK.ttc"), diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index e5a50c0d6f1e..5e347f4dbb9b 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -14,7 +14,10 @@ static PyObject *convert_xys_to_array(std::vector &xys) { npy_intp dims[] = {(npy_intp)xys.size() / 2, 2 }; if (dims[0] > 0) { - return PyArray_SimpleNewFromData(2, dims, NPY_DOUBLE, &xys[0]); + auto obj = PyArray_SimpleNew(2, dims, NPY_DOUBLE); + auto array = reinterpret_cast(obj); + memcpy(PyArray_DATA(array), xys.data(), PyArray_NBYTES(array)); + return obj; } else { return PyArray_SimpleNew(2, dims, NPY_DOUBLE); } @@ -631,7 +634,7 @@ const char *PyFT2Font_set_text__doc__ = "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" "the default value is LOAD_FORCE_AUTOHINT.\n" "You must call this before `.draw_glyphs_to_bitmap`.\n" - "A sequence of x,y positions is returned.\n"; + "A sequence of x,y positions in 26.6 subpixels is returned; divide by 64 for pixels.\n"; static PyObject *PyFT2Font_set_text(PyFT2Font *self, PyObject *args, PyObject *kwds) { From f618fc21ca805b1b9dccecb364c1acea6c1fe30c Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 27 Aug 2024 19:39:12 -0400 Subject: [PATCH 0206/1230] TST: Add test for FT2Font.set_size --- lib/matplotlib/tests/test_ft2font.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 0c2eb31580b8..a2e820b4434d 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -176,6 +176,20 @@ def test_ft2font_clear(): assert font.get_bitmap_offset() == (0, 0) +def test_ft2font_set_size(): + file = fm.findfont('DejaVu Sans') + # Default is 12pt @ 72 dpi. + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=1) + font.set_text('ABabCDcd') + orig = font.get_width_height() + font.set_size(24, 72) + font.set_text('ABabCDcd') + assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig) + font.set_size(12, 144) + font.set_text('ABabCDcd') + assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig) + + def test_ft2font_charmaps(): def enc(name): # We don't expose the encoding enum from FreeType, but can generate it here. From 7c390dec98d0952057723f64aeb82d1b2a7e37d5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 29 Aug 2024 02:43:25 -0400 Subject: [PATCH 0207/1230] TST: Add tests for FT2Font.load_{char,glyph} --- lib/matplotlib/tests/test_ft2font.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index a2e820b4434d..8e0558feeb21 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -741,6 +741,29 @@ def test_ft2font_set_text(): assert font.get_bitmap_offset() == (6, 0) +def test_ft2font_loading(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + for glyph in [font.load_char(ord('M')), + font.load_glyph(font.get_char_index(ord('M')))]: + assert glyph is not None + assert glyph.width == 576 + assert glyph.height == 576 + assert glyph.horiBearingX == 0 + assert glyph.horiBearingY == 576 + assert glyph.horiAdvance == 640 + assert glyph.linearHoriAdvance == 678528 + assert glyph.vertBearingX == -384 + assert glyph.vertBearingY == 64 + assert glyph.vertAdvance == 832 + assert glyph.bbox == (54, 0, 574, 576) + assert font.get_num_glyphs() == 2 # Both count as loaded. + # But neither has been placed anywhere. + assert font.get_width_height() == (0, 0) + assert font.get_descent() == 0 + assert font.get_bitmap_offset() == (0, 0) + + @pytest.mark.parametrize('family_name, file_name', [("WenQuanYi Zen Hei", "wqy-zenhei.ttc"), ("Noto Sans CJK JP", "NotoSansCJK.ttc"), From 06df2537df7250f87885a8d67140f2eeb0990f8c Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 29 Aug 2024 04:09:48 -0400 Subject: [PATCH 0208/1230] TST: Add tests for FT2Font drawing and path generation --- lib/matplotlib/tests/test_ft2font.py | 60 ++++++++++++++++++++++++++++ src/ft2font_wrapper.cpp | 6 +-- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 8e0558feeb21..f383901b7b31 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -8,6 +8,7 @@ from matplotlib import ft2font from matplotlib.testing.decorators import check_figures_equal import matplotlib.font_manager as fm +import matplotlib.path as mpath import matplotlib.pyplot as plt @@ -764,6 +765,65 @@ def test_ft2font_loading(): assert font.get_bitmap_offset() == (0, 0) +def test_ft2font_drawing(): + expected_str = ( + ' ', + '11 11 ', + '11 11 ', + '1 1 1 1 ', + '1 1 1 1 ', + '1 1 1 1 ', + '1 11 1 ', + '1 11 1 ', + '1 1 ', + '1 1 ', + ' ', + ) + expected = np.array([ + [int(c) for c in line.replace(' ', '0')] for line in expected_str + ]) + expected *= 255 + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font.set_text('M') + font.draw_glyphs_to_bitmap(antialiased=False) + image = font.get_image() + np.testing.assert_array_equal(image, expected) + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + glyph = font.load_char(ord('M')) + image = ft2font.FT2Image(expected.shape[1], expected.shape[0]) + font.draw_glyph_to_bitmap(image, -1, 1, glyph, antialiased=False) + np.testing.assert_array_equal(image, expected) + + +def test_ft2font_get_path(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + vertices, codes = font.get_path() + assert vertices.shape == (0, 2) + assert codes.shape == (0, ) + font.load_char(ord('M')) + vertices, codes = font.get_path() + expected_vertices = np.array([ + (0.843750, 9.000000), (2.609375, 9.000000), # Top left. + (4.906250, 2.875000), # Top of midpoint. + (7.218750, 9.000000), (8.968750, 9.000000), # Top right. + (8.968750, 0.000000), (7.843750, 0.000000), # Bottom right. + (7.843750, 7.906250), # Point under top right. + (5.531250, 1.734375), (4.296875, 1.734375), # Bar under midpoint. + (1.984375, 7.906250), # Point under top left. + (1.984375, 0.000000), (0.843750, 0.000000), # Bottom left. + (0.843750, 9.000000), # Back to top left corner. + (0.000000, 0.000000), + ]) + np.testing.assert_array_equal(vertices, expected_vertices) + expected_codes = np.full(expected_vertices.shape[0], mpath.Path.LINETO, + dtype=mpath.Path.code_type) + expected_codes[0] = mpath.Path.MOVETO + expected_codes[-1] = mpath.Path.CLOSEPOLY + np.testing.assert_array_equal(codes, expected_codes) + + @pytest.mark.parametrize('family_name, file_name', [("WenQuanYi Zen Hei", "wqy-zenhei.ttc"), ("Noto Sans CJK JP", "NotoSansCJK.ttc"), diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 5e347f4dbb9b..6a05680a474c 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -832,9 +832,9 @@ static PyObject *PyFT2Font_draw_glyphs_to_bitmap(PyFT2Font *self, PyObject *args const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = "draw_glyph_to_bitmap(self, image, x, y, glyph, antialiased=True)\n" "--\n\n" - "Draw a single glyph to the bitmap at pixel locations x, y\n" - "Note it is your responsibility to set up the bitmap manually\n" - "with ``set_bitmap_size(w, h)`` before this call is made.\n" + "Draw a single glyph to *image* at pixel locations *x*, *y*\n" + "Note it is your responsibility to create the image manually\n" + "with the correct size before this call is made.\n" "\n" "If you want automatic layout, use `.set_text` in combinations with\n" "`.draw_glyphs_to_bitmap`. This function is instead intended for people\n" From 0e53ac562e80d860d740bc497c2d25bc30a5fff6 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 31 Aug 2024 01:59:39 -0400 Subject: [PATCH 0209/1230] Ensure SketchParams is always fully initialized This fixes the following errors from valgrind: ``` Conditional jump or move depends on uninitialised value(s) at 0x377C3EE4: UnknownInlinedFun (path_converters.h:1025) by 0x377C3EE4: UnknownInlinedFun (_backend_agg.h:477) by 0x377C3EE4: PyRendererAgg_draw_path(PyRendererAgg*, _object*) [clone .lto_priv.0] (_backend_agg_wrapper.cpp:197) by 0x49E8705: method_vectorcall_VARARGS (descrobject.c:331) by 0x4A03B7B: UnknownInlinedFun (pycore_call.h:92) by 0x4A03B7B: PyObject_Vectorcall (call.c:325) by 0x49ECEA4: _PyEval_EvalFrameDefault (bytecodes.c:2714) by 0x4A2B684: UnknownInlinedFun (call.c:419) by 0x4A2B684: UnknownInlinedFun (pycore_call.h:92) by 0x4A2B684: method_vectorcall (classobject.c:91) by 0x49F1E44: UnknownInlinedFun (call.c:387) by 0x49F1E44: _PyEval_EvalFrameDefault (bytecodes.c:3262) by 0x4A2B684: UnknownInlinedFun (call.c:419) by 0x4A2B684: UnknownInlinedFun (pycore_call.h:92) by 0x4A2B684: method_vectorcall (classobject.c:91) by 0x49F1E44: UnknownInlinedFun (call.c:387) by 0x49F1E44: _PyEval_EvalFrameDefault (bytecodes.c:3262) by 0x4A2B737: UnknownInlinedFun (call.c:419) by 0x4A2B737: UnknownInlinedFun (pycore_call.h:92) by 0x4A2B737: method_vectorcall (classobject.c:61) by 0x4A19144: _PyVectorcall_Call (call.c:283) by 0x49F1E44: UnknownInlinedFun (call.c:387) by 0x49F1E44: _PyEval_EvalFrameDefault (bytecodes.c:3262) by 0x49E6CBA: _PyObject_FastCallDictTstate (call.c:144) Conditional jump or move depends on uninitialised value(s) at 0x377C3F4F: UnknownInlinedFun (path_converters.h:1030) by 0x377C3F4F: UnknownInlinedFun (_backend_agg.h:477) by 0x377C3F4F: PyRendererAgg_draw_path(PyRendererAgg*, _object*) [clone .lto_priv.0] (_backend_agg_wrapper.cpp:197) by 0x49E8705: method_vectorcall_VARARGS (descrobject.c:331) by 0x4A03B7B: UnknownInlinedFun (pycore_call.h:92) by 0x4A03B7B: PyObject_Vectorcall (call.c:325) by 0x49ECEA4: _PyEval_EvalFrameDefault (bytecodes.c:2714) by 0x4A2B684: UnknownInlinedFun (call.c:419) by 0x4A2B684: UnknownInlinedFun (pycore_call.h:92) by 0x4A2B684: method_vectorcall (classobject.c:91) by 0x49F1E44: UnknownInlinedFun (call.c:387) by 0x49F1E44: _PyEval_EvalFrameDefault (bytecodes.c:3262) by 0x4A2B684: UnknownInlinedFun (call.c:419) by 0x4A2B684: UnknownInlinedFun (pycore_call.h:92) by 0x4A2B684: method_vectorcall (classobject.c:91) by 0x49F1E44: UnknownInlinedFun (call.c:387) by 0x49F1E44: _PyEval_EvalFrameDefault (bytecodes.c:3262) by 0x4A2B737: UnknownInlinedFun (call.c:419) by 0x4A2B737: UnknownInlinedFun (pycore_call.h:92) by 0x4A2B737: method_vectorcall (classobject.c:61) by 0x4A19144: _PyVectorcall_Call (call.c:283) by 0x49F1E44: UnknownInlinedFun (call.c:387) by 0x49F1E44: _PyEval_EvalFrameDefault (bytecodes.c:3262) by 0x49E6CBA: _PyObject_FastCallDictTstate (call.c:144) ``` --- src/py_converters.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/py_converters.cpp b/src/py_converters.cpp index e700342d5c78..e4a04b7bc057 100644 --- a/src/py_converters.cpp +++ b/src/py_converters.cpp @@ -453,6 +453,8 @@ int convert_sketch_params(PyObject *obj, void *sketchp) if (obj == NULL || obj == Py_None) { sketch->scale = 0.0; + sketch->length = 0.0; + sketch->randomness = 0.0; } else if (!PyArg_ParseTuple(obj, "ddd:sketch_params", &sketch->scale, From e28d5eeedb9e8a519cf33faace09684653254edf Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:25:51 +0100 Subject: [PATCH 0210/1230] MNT: expire ContourSet deprecations --- .../next_api_changes/removals/28767-REC.rst | 31 ++++ lib/matplotlib/contour.py | 162 ------------------ lib/matplotlib/contour.pyi | 23 --- lib/matplotlib/tests/test_contour.py | 111 ++---------- 4 files changed, 46 insertions(+), 281 deletions(-) create mode 100644 doc/api/next_api_changes/removals/28767-REC.rst diff --git a/doc/api/next_api_changes/removals/28767-REC.rst b/doc/api/next_api_changes/removals/28767-REC.rst new file mode 100644 index 000000000000..a06d78245761 --- /dev/null +++ b/doc/api/next_api_changes/removals/28767-REC.rst @@ -0,0 +1,31 @@ +``ContourSet.collections`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed. `~.ContourSet` is now implemented as a single +`~.Collection` of paths, each path corresponding to a contour level, possibly +including multiple unconnected components. + +``ContourSet.antialiased`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed. Use `~.Collection.get_antialiased` or +`~.Collection.set_antialiased` instead. Note that `~.Collection.get_antialiased` +returns an array. + +``tcolors`` and ``tlinewidths`` attributes of ``ContourSet`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... have been removed. Use `~.Collection.get_facecolor`, `~.Collection.get_edgecolor` +or `~.Collection.get_linewidths` instead. + + +``calc_label_rot_and_inline`` method of ``ContourLabeler`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed without replacement. + + +``add_label_clabeltext`` method of ``ContourLabeler`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed. Use `~.ContourLabeler.add_label` instead. diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 0e6068c64b62..503ec2747f10 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -310,14 +310,6 @@ def _split_path_and_get_label_rotation(self, path, idx, screen_pos, lw, spacing= determine rotation and then to break contour if desired. The extra spacing is taken into account when breaking the path, but not when computing the angle. """ - if hasattr(self, "_old_style_split_collections"): - vis = False - for coll in self._old_style_split_collections: - vis |= coll.get_visible() - coll.remove() - self.set_visible(vis) - del self._old_style_split_collections # Invalidate them. - xys = path.vertices codes = path.codes @@ -406,97 +398,6 @@ def interp_vec(x, xp, fp): return [np.interp(x, xp, col) for col in fp.T] return angle, Path(xys, codes) - @_api.deprecated("3.8") - def calc_label_rot_and_inline(self, slc, ind, lw, lc=None, spacing=5): - """ - Calculate the appropriate label rotation given the linecontour - coordinates in screen units, the index of the label location and the - label width. - - If *lc* is not None or empty, also break contours and compute - inlining. - - *spacing* is the empty space to leave around the label, in pixels. - - Both tasks are done together to avoid calculating path lengths - multiple times, which is relatively costly. - - The method used here involves computing the path length along the - contour in pixel coordinates and then looking approximately (label - width / 2) away from central point to determine rotation and then to - break contour if desired. - """ - - if lc is None: - lc = [] - # Half the label width - hlw = lw / 2.0 - - # Check if closed and, if so, rotate contour so label is at edge - closed = _is_closed_polygon(slc) - if closed: - slc = np.concatenate([slc[ind:-1], slc[:ind + 1]]) - if len(lc): # Rotate lc also if not empty - lc = np.concatenate([lc[ind:-1], lc[:ind + 1]]) - ind = 0 - - # Calculate path lengths - pl = np.zeros(slc.shape[0], dtype=float) - dx = np.diff(slc, axis=0) - pl[1:] = np.cumsum(np.hypot(dx[:, 0], dx[:, 1])) - pl = pl - pl[ind] - - # Use linear interpolation to get points around label - xi = np.array([-hlw, hlw]) - if closed: # Look at end also for closed contours - dp = np.array([pl[-1], 0]) - else: - dp = np.zeros_like(xi) - - # Get angle of vector between the two ends of the label - must be - # calculated in pixel space for text rotation to work correctly. - (dx,), (dy,) = (np.diff(np.interp(dp + xi, pl, slc_col)) - for slc_col in slc.T) - rotation = np.rad2deg(np.arctan2(dy, dx)) - - if self.rightside_up: - # Fix angle so text is never upside-down - rotation = (rotation + 90) % 180 - 90 - - # Break contour if desired - nlc = [] - if len(lc): - # Expand range by spacing - xi = dp + xi + np.array([-spacing, spacing]) - - # Get (integer) indices near points of interest; use -1 as marker - # for out of bounds. - I = np.interp(xi, pl, np.arange(len(pl)), left=-1, right=-1) - I = [np.floor(I[0]).astype(int), np.ceil(I[1]).astype(int)] - if I[0] != -1: - xy1 = [np.interp(xi[0], pl, lc_col) for lc_col in lc.T] - if I[1] != -1: - xy2 = [np.interp(xi[1], pl, lc_col) for lc_col in lc.T] - - # Actually break contours - if closed: - # This will remove contour if shorter than label - if all(i != -1 for i in I): - nlc.append(np.vstack([xy2, lc[I[1]:I[0]+1], xy1])) - else: - # These will remove pieces of contour if they have length zero - if I[0] != -1: - nlc.append(np.vstack([lc[:I[0]+1], xy1])) - if I[1] != -1: - nlc.append(np.vstack([xy2, lc[I[1]:]])) - - # The current implementation removes contours completely - # covered by labels. Uncomment line below to keep - # original contour if this is the preferred behavior. - # if not len(nlc): nlc = [lc] - - return rotation, nlc - def add_label(self, x, y, rotation, lev, cvalue): """Add a contour label, respecting whether *use_clabeltext* was set.""" data_x, data_y = self.axes.transData.inverted().transform((x, y)) @@ -519,12 +420,6 @@ def add_label(self, x, y, rotation, lev, cvalue): # Add label to plot here - useful for manual mode label selection self.axes.add_artist(t) - @_api.deprecated("3.8", alternative="add_label") - def add_label_clabeltext(self, x, y, rotation, lev, cvalue): - """Add contour label with `.Text.set_transform_rotates_text`.""" - with cbook._setattr_cm(self, _use_clabeltext=True): - self.add_label(x, y, rotation, lev, cvalue) - def add_label_near(self, x, y, inline=True, inline_spacing=5, transform=None): """ @@ -604,15 +499,6 @@ def remove(self): text.remove() -def _is_closed_polygon(X): - """ - Return whether first and last object in a sequence are the same. These are - presumably coordinates on a polygonal curve, in which case this function - tests if that curve is closed. - """ - return np.allclose(X[0], X[-1], rtol=1e-10, atol=1e-13) - - def _find_closest_point_on_path(xys, p): """ Parameters @@ -906,57 +792,9 @@ def __init__(self, ax, *args, allkinds = property(lambda self: [ [subp.codes for subp in p._iter_connected_components()] for p in self.get_paths()]) - tcolors = _api.deprecated("3.8")(property(lambda self: [ - (tuple(rgba),) for rgba in self.to_rgba(self.cvalues, self.alpha)])) - tlinewidths = _api.deprecated("3.8")(property(lambda self: [ - (w,) for w in self.get_linewidths()])) alpha = property(lambda self: self.get_alpha()) linestyles = property(lambda self: self._orig_linestyles) - @_api.deprecated("3.8", alternative="set_antialiased or get_antialiased", - addendum="Note that get_antialiased returns an array.") - @property - def antialiased(self): - return all(self.get_antialiased()) - - @antialiased.setter - def antialiased(self, aa): - self.set_antialiased(aa) - - @_api.deprecated("3.8") - @property - def collections(self): - # On access, make oneself invisible and instead add the old-style collections - # (one PathCollection per level). We do not try to further split contours into - # connected components as we already lost track of what pairs of contours need - # to be considered as single units to draw filled regions with holes. - if not hasattr(self, "_old_style_split_collections"): - self.set_visible(False) - fcs = self.get_facecolor() - ecs = self.get_edgecolor() - lws = self.get_linewidth() - lss = self.get_linestyle() - self._old_style_split_collections = [] - for idx, path in enumerate(self._paths): - pc = mcoll.PathCollection( - [path] if len(path.vertices) else [], - alpha=self.get_alpha(), - antialiaseds=self._antialiaseds[idx % len(self._antialiaseds)], - transform=self.get_transform(), - zorder=self.get_zorder(), - label="_nolegend_", - facecolor=fcs[idx] if len(fcs) else "none", - edgecolor=ecs[idx] if len(ecs) else "none", - linewidths=[lws[idx % len(lws)]], - linestyles=[lss[idx % len(lss)]], - ) - if self.filled: - pc.set(hatch=self.hatches[idx % len(self.hatches)]) - self._old_style_split_collections.append(pc) - for col in self._old_style_split_collections: - self.axes.add_collection(col) - return self._old_style_split_collections - def get_transform(self): """Return the `.Transform` instance used by this ContourSet.""" if self._transform is None: diff --git a/lib/matplotlib/contour.pyi b/lib/matplotlib/contour.pyi index 9d99fe0f343c..c1df833506eb 100644 --- a/lib/matplotlib/contour.pyi +++ b/lib/matplotlib/contour.pyi @@ -50,20 +50,9 @@ class ContourLabeler: def locate_label( self, linecontour: ArrayLike, labelwidth: float ) -> tuple[float, float, float]: ... - def calc_label_rot_and_inline( - self, - slc: ArrayLike, - ind: int, - lw: float, - lc: ArrayLike | None = ..., - spacing: int = ..., - ) -> tuple[float, list[ArrayLike]]: ... def add_label( self, x: float, y: float, rotation: float, lev: float, cvalue: ColorType ) -> None: ... - def add_label_clabeltext( - self, x: float, y: float, rotation: float, lev: float, cvalue: ColorType - ) -> None: ... def add_label_near( self, x: float, @@ -95,12 +84,6 @@ class ContourSet(ContourLabeler, Collection): clip_path: Patch | Path | TransformedPath | TransformedPatchPath | None labelTexts: list[Text] labelCValues: list[ColorType] - @property - def tcolors(self) -> list[tuple[tuple[float, float, float, float]]]: ... - - # only for not filled - @property - def tlinewidths(self) -> list[tuple[float]]: ... @property def allkinds(self) -> list[list[np.ndarray | None]]: ... @@ -109,12 +92,6 @@ class ContourSet(ContourLabeler, Collection): @property def alpha(self) -> float | None: ... @property - def antialiased(self) -> bool: ... - @antialiased.setter - def antialiased(self, aa: bool | Sequence[bool]) -> None: ... - @property - def collections(self) -> list[PathCollection]: ... - @property def linestyles(self) -> ( None | Literal["solid", "dashed", "dashdot", "dotted"] | diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index 0622c099a20c..6211b2d8418b 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -5,8 +5,7 @@ import contourpy import numpy as np -from numpy.testing import ( - assert_array_almost_equal, assert_array_almost_equal_nulp, assert_array_equal) +from numpy.testing import assert_array_almost_equal, assert_array_almost_equal_nulp import matplotlib as mpl from matplotlib import pyplot as plt, rc_context, ticker from matplotlib.colors import LogNorm, same_color @@ -15,19 +14,6 @@ import pytest -# Helper to test the transition from ContourSets holding multiple Collections to being a -# single Collection; remove once the deprecated old layout expires. -def _maybe_split_collections(do_split): - if not do_split: - return - for fig in map(plt.figure, plt.get_fignums()): - for ax in fig.axes: - for coll in ax.collections: - if isinstance(coll, mpl.contour.ContourSet): - with pytest.warns(mpl._api.MatplotlibDeprecationWarning): - coll.collections - - def test_contour_shape_1d_valid(): x = np.arange(10) @@ -108,17 +94,14 @@ def test_contour_set_paths(fig_test, fig_ref): cs_test.set_paths(cs_ref.get_paths()) -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_manual_labels'], remove_text=True, style='mpl20', tol=0.26) -def test_contour_manual_labels(split_collections): +def test_contour_manual_labels(): x, y = np.meshgrid(np.arange(0, 10), np.arange(0, 10)) z = np.max(np.dstack([abs(x), abs(y)]), 2) plt.figure(figsize=(6, 2), dpi=200) cs = plt.contour(x, y, z) - _maybe_split_collections(split_collections) - pts = np.array([(1.0, 3.0), (1.0, 4.4), (1.0, 6.0)]) plt.clabel(cs, manual=pts) pts = np.array([(2.0, 3.0), (2.0, 4.4), (2.0, 6.0)]) @@ -144,29 +127,21 @@ def test_contour_manual_moveto(): assert clabels[0].get_text() == "0" -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_disconnected_segments'], remove_text=True, style='mpl20', extensions=['png']) -def test_contour_label_with_disconnected_segments(split_collections): +def test_contour_label_with_disconnected_segments(): x, y = np.mgrid[-1:1:21j, -1:1:21j] z = 1 / np.sqrt(0.01 + (x + 0.3) ** 2 + y ** 2) z += 1 / np.sqrt(0.01 + (x - 0.3) ** 2 + y ** 2) plt.figure() cs = plt.contour(x, y, z, levels=[7]) - - # Adding labels should invalidate the old style - _maybe_split_collections(split_collections) - cs.clabel(manual=[(0.2, 0.1)]) - _maybe_split_collections(split_collections) - -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_manual_colors_and_levels.png'], remove_text=True, tol=0.018 if platform.machine() == 'arm64' else 0) -def test_given_colors_levels_and_extends(split_collections): +def test_given_colors_levels_and_extends(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -195,12 +170,9 @@ def test_given_colors_levels_and_extends(split_collections): plt.colorbar(c, ax=ax) - _maybe_split_collections(split_collections) - -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_log_locator.svg'], style='mpl20', remove_text=False) -def test_log_locator_levels(split_collections): +def test_log_locator_levels(): fig, ax = plt.subplots() @@ -219,12 +191,9 @@ def test_log_locator_levels(split_collections): cb = fig.colorbar(c, ax=ax) assert_array_almost_equal(cb.ax.get_yticks(), c.levels) - _maybe_split_collections(split_collections) - -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_datetime_axis.png'], style='mpl20') -def test_contour_datetime_axis(split_collections): +def test_contour_datetime_axis(): fig = plt.figure() fig.subplots_adjust(hspace=0.4, top=0.98, bottom=.15) base = datetime.datetime(2013, 1, 1) @@ -247,13 +216,10 @@ def test_contour_datetime_axis(split_collections): label.set_ha('right') label.set_rotation(30) - _maybe_split_collections(split_collections) - -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_test_label_transforms.png'], remove_text=True, style='mpl20', tol=1.1) -def test_labels(split_collections): +def test_labels(): # Adapted from pylab_examples example code: contour_demo.py # see issues #2475, #2843, and #2818 for explanation delta = 0.025 @@ -272,9 +238,6 @@ def test_labels(split_collections): disp_units = [(216, 177), (359, 290), (521, 406)] data_units = [(-2, .5), (0, -1.5), (2.8, 1)] - # Adding labels should invalidate the old style - _maybe_split_collections(split_collections) - CS.clabel() for x, y in data_units: @@ -283,8 +246,6 @@ def test_labels(split_collections): for x, y in disp_units: CS.add_label_near(x, y, inline=True, transform=False) - _maybe_split_collections(split_collections) - def test_label_contour_start(): # Set up data and figure/axes that result in automatic labelling adding the @@ -311,10 +272,9 @@ def test_label_contour_start(): assert 0 in idxs -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_corner_mask_False.png', 'contour_corner_mask_True.png'], remove_text=True, tol=1.88) -def test_corner_mask(split_collections): +def test_corner_mask(): n = 60 mask_level = 0.95 noise_amp = 1.0 @@ -328,8 +288,6 @@ def test_corner_mask(split_collections): plt.figure() plt.contourf(z, corner_mask=corner_mask) - _maybe_split_collections(split_collections) - def test_contourf_decreasing_levels(): # github issue 5477. @@ -400,11 +358,10 @@ def test_clabel_with_large_spacing(): # tol because ticks happen to fall on pixel boundaries so small # floating point changes in tick location flip which pixel gets # the tick. -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(['contour_log_extension.png'], remove_text=True, style='mpl20', tol=1.444) -def test_contourf_log_extension(split_collections): +def test_contourf_log_extension(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -436,17 +393,14 @@ def test_contourf_log_extension(split_collections): assert_array_almost_equal_nulp(cb.ax.get_ylim(), np.array((1e-4, 1e6))) cb = plt.colorbar(c3, ax=ax3) - _maybe_split_collections(split_collections) - -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison( ['contour_addlines.png'], remove_text=True, style='mpl20', tol=0.15 if platform.machine() in ('aarch64', 'arm64', 'ppc64le', 's390x') else 0.03) # tolerance is because image changed minutely when tick finding on # colorbars was cleaned up... -def test_contour_addlines(split_collections): +def test_contour_addlines(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -460,13 +414,10 @@ def test_contour_addlines(split_collections): cb.add_lines(cont) assert_array_almost_equal(cb.ax.get_ylim(), [114.3091, 9972.30735], 3) - _maybe_split_collections(split_collections) - -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(baseline_images=['contour_uneven'], extensions=['png'], remove_text=True, style='mpl20') -def test_contour_uneven(split_collections): +def test_contour_uneven(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -479,8 +430,6 @@ def test_contour_uneven(split_collections): cs = ax.contourf(z, levels=[2, 4, 6, 10, 20]) fig.colorbar(cs, ax=ax, spacing='uniform') - _maybe_split_collections(split_collections) - @pytest.mark.parametrize( "rc_lines_linewidth, rc_contour_linewidth, call_linewidths, expected", [ @@ -497,8 +446,6 @@ def test_contour_linewidth( X = np.arange(4*3).reshape(4, 3) cs = ax.contour(X, linewidths=call_linewidths) assert cs.get_linewidths()[0] == expected - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="tlinewidths"): - assert cs.tlinewidths[0][0] == expected @pytest.mark.backend("pdf") @@ -507,10 +454,9 @@ def test_label_nonagg(): plt.clabel(plt.contour([[1, 2], [3, 4]])) -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(baseline_images=['contour_closed_line_loop'], extensions=['png'], remove_text=True) -def test_contour_closed_line_loop(split_collections): +def test_contour_closed_line_loop(): # github issue 19568. z = [[0, 0, 0], [0, 2, 0], [0, 0, 0], [2, 1, 2]] @@ -519,8 +465,6 @@ def test_contour_closed_line_loop(split_collections): ax.set_xlim(-0.1, 2.1) ax.set_ylim(-0.1, 3.1) - _maybe_split_collections(split_collections) - def test_quadcontourset_reuse(): # If QuadContourSet returned from one contour(f) call is passed as first @@ -535,10 +479,9 @@ def test_quadcontourset_reuse(): assert qcs3._contour_generator == qcs1._contour_generator -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(baseline_images=['contour_manual'], extensions=['png'], remove_text=True, tol=0.89) -def test_contour_manual(split_collections): +def test_contour_manual(): # Manually specifying contour lines/polygons to plot. from matplotlib.contour import ContourSet @@ -561,13 +504,10 @@ def test_contour_manual(split_collections): ContourSet(ax, [2, 3], [segs], [kinds], filled=True, cmap=cmap) ContourSet(ax, [2], [segs], [kinds], colors='k', linewidths=3) - _maybe_split_collections(split_collections) - -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(baseline_images=['contour_line_start_on_corner_edge'], extensions=['png'], remove_text=True) -def test_contour_line_start_on_corner_edge(split_collections): +def test_contour_line_start_on_corner_edge(): fig, ax = plt.subplots(figsize=(6, 5)) x, y = np.meshgrid([0, 1, 2, 3, 4], [0, 1, 2]) @@ -581,8 +521,6 @@ def test_contour_line_start_on_corner_edge(split_collections): lines = ax.contour(x, y, z, corner_mask=True, colors='k') cbar.add_lines(lines) - _maybe_split_collections(split_collections) - def test_find_nearest_contour(): xy = np.indices((15, 15)) @@ -703,10 +641,9 @@ def test_algorithm_supports_corner_mask(algorithm): plt.contourf(z, algorithm=algorithm, corner_mask=True) -@pytest.mark.parametrize("split_collections", [False, True]) @image_comparison(baseline_images=['contour_all_algorithms'], extensions=['png'], remove_text=True, tol=0.06) -def test_all_algorithms(split_collections): +def test_all_algorithms(): algorithms = ['mpl2005', 'mpl2014', 'serial', 'threaded'] rng = np.random.default_rng(2981) @@ -722,8 +659,6 @@ def test_all_algorithms(split_collections): ax.contour(x, y, z, algorithm=algorithm, colors='k') ax.set_title(algorithm) - _maybe_split_collections(split_collections) - def test_subfigure_clabel(): # Smoke test for gh#23173 @@ -882,19 +817,3 @@ def test_allsegs_allkinds(): assert len(result) == 2 assert len(result[0]) == 5 assert len(result[1]) == 4 - - -def test_deprecated_apis(): - cs = plt.contour(np.arange(16).reshape((4, 4))) - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="collections"): - colls = cs.collections - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="tcolors"): - assert_array_equal(cs.tcolors, [c.get_edgecolor() for c in colls]) - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="tlinewidths"): - assert cs.tlinewidths == [c.get_linewidth() for c in colls] - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="antialiased"): - assert cs.antialiased - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="antialiased"): - cs.antialiased = False - with pytest.warns(mpl.MatplotlibDeprecationWarning, match="antialiased"): - assert not cs.antialiased From 29d886eab5d5ea54b023bb2601a0f83055ae4104 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Sun, 24 Sep 2023 02:57:04 -0600 Subject: [PATCH 0211/1230] Implement dynamic clipping to axes box for 3D plots Make axlim_clip flag keyword only Updates test image test image restore --- .../next_whats_new/3d_clip_to_axis_limits.rst | 30 +++ galleries/examples/mplot3d/axlim_clip.py | 31 +++ lib/matplotlib/collections.py | 2 +- lib/matplotlib/text.py | 11 +- lib/mpl_toolkits/mplot3d/art3d.py | 212 +++++++++++++----- lib/mpl_toolkits/mplot3d/axes3d.py | 155 +++++++++---- lib/mpl_toolkits/mplot3d/proj3d.py | 25 ++- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 43 +++- 8 files changed, 404 insertions(+), 105 deletions(-) create mode 100644 doc/users/next_whats_new/3d_clip_to_axis_limits.rst create mode 100644 galleries/examples/mplot3d/axlim_clip.py diff --git a/doc/users/next_whats_new/3d_clip_to_axis_limits.rst b/doc/users/next_whats_new/3d_clip_to_axis_limits.rst new file mode 100644 index 000000000000..b60927bcd0b5 --- /dev/null +++ b/doc/users/next_whats_new/3d_clip_to_axis_limits.rst @@ -0,0 +1,30 @@ +Data in 3D plots can now be dynamically clipped to the axes view limits +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All 3D plotting functions now support the *axlim_clip* keyword argument, which +will clip the data to the axes view limits, hiding all data outside those +bounds. This clipping will be dynamically applied in real time while panning +and zooming. + +Please note that if one vertex of a line segment or 3D patch is clipped, the +entire segment or patch will be hidden. Not being able to show partial lines +or patches such that they are "smoothly" cut off at the boundaries of the view +box is a limitation of the current renderer. + +.. plot:: + :include-source: true + :alt: Example of default behavior (left) and axlim_clip=True (right) + + import matplotlib.pyplot as plt + import numpy as np + + fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) + np.random.seed(1) + xyz = np.random.rand(25, 3) + + # Note that when a line has one vertex outside the view limits, the entire + # line is hidden. The same is true for 3D patches (not shown). + ax.plot(xyz[:, 0], xyz[:, 1], xyz[:, 2], '-o') + ax.plot(xyz[:, 0], xyz[:, 1], xyz[:, 2], '--*', axlim_clip=True) + ax.set(xlim=(0.25, 0.75), ylim=(0, 1), zlim=(0, 1)) + ax.legend(['axlim_clip=False (default)', 'axlim_clip=True']) diff --git a/galleries/examples/mplot3d/axlim_clip.py b/galleries/examples/mplot3d/axlim_clip.py new file mode 100644 index 000000000000..b25c55a30ad1 --- /dev/null +++ b/galleries/examples/mplot3d/axlim_clip.py @@ -0,0 +1,31 @@ +""" +===================================== +Clip the data to the axes view limits +===================================== + +Demonstrate clipping of line and marker data to the axes view limits. The +``axlim_clip`` keyword argument can be used in any of the 3D plotting +functions. +""" + +import matplotlib.pyplot as plt +import numpy as np + +fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) + +# Generate the random data +np.random.seed(1) +xyz = np.random.rand(25, 3) + +# Default behavior is axlim_clip=False +ax.plot(xyz[:, 0], xyz[:, 1], xyz[:, 2], '-o') + +# When axlim_clip=True, note that when a line segment has one vertex outside +# the view limits, the entire line is hidden. The same is true for 3D patches +# if one of their vertices is outside the limits (not shown). +ax.plot(xyz[:, 0], xyz[:, 1], xyz[:, 2], '--*', axlim_clip=True) + +ax.set(xlim=(0.25, 0.75), ylim=(0, 1), zlim=(-1, 1)) +ax.legend(['axlim_clip=False (default)', 'axlim_clip=True']) + +plt.show() diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 00146cec3cb0..db6fc5b37293 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -339,7 +339,7 @@ def _prepare_points(self): # This might have changed an ndarray into a masked array. offset_trf = offset_trf.get_affine() - if isinstance(offsets, np.ma.MaskedArray): + if np.ma.isMaskedArray(offsets): offsets = offsets.filled(np.nan) # Changing from a masked array to nan-filled ndarray # is probably most efficient at this point. diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index af990ec1bf9f..d03b9336bca3 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -753,9 +753,16 @@ def draw(self, renderer): # don't use self.get_position here, which refers to text # position in Text: - posx = float(self.convert_xunits(self._x)) - posy = float(self.convert_yunits(self._y)) + x, y = self._x, self._y + if np.ma.is_masked(x): + x = np.nan + if np.ma.is_masked(y): + y = np.nan + posx = float(self.convert_xunits(x)) + posy = float(self.convert_yunits(y)) posx, posy = trans.transform((posx, posy)) + if np.isnan(posx) or np.isnan(posy): + return # don't throw a warning here if not np.isfinite(posx) or not np.isfinite(posy): _log.warning("posx and posy should be finite values") return diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 38ebe88dc80e..0e1d6205d23c 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -73,6 +73,34 @@ def get_dir_vector(zdir): raise ValueError("'x', 'y', 'z', None or vector of length 3 expected") +def _viewlim_mask(xs, ys, zs, axes): + """ + Return original points with points outside the axes view limits masked. + + Parameters + ---------- + xs, ys, zs : array-like + The points to mask. + axes : Axes3D + The axes to use for the view limits. + + Returns + ------- + xs_masked, ys_masked, zs_masked : np.ma.array + The masked points. + """ + mask = np.logical_or.reduce((xs < axes.xy_viewLim.xmin, + xs > axes.xy_viewLim.xmax, + ys < axes.xy_viewLim.ymin, + ys > axes.xy_viewLim.ymax, + zs < axes.zz_viewLim.xmin, + zs > axes.zz_viewLim.xmax)) + xs_masked = np.ma.array(xs, mask=mask) + ys_masked = np.ma.array(ys, mask=mask) + zs_masked = np.ma.array(zs, mask=mask) + return xs_masked, ys_masked, zs_masked + + class Text3D(mtext.Text): """ Text object with 3D position and direction. @@ -86,6 +114,8 @@ class Text3D(mtext.Text): zdir : {'x', 'y', 'z', None, 3-tuple} The direction of the text. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide text outside the axes view limits. Other Parameters ---------------- @@ -93,9 +123,10 @@ class Text3D(mtext.Text): All other parameters are passed on to `~matplotlib.text.Text`. """ - def __init__(self, x=0, y=0, z=0, text='', zdir='z', **kwargs): + def __init__(self, x=0, y=0, z=0, text='', zdir='z', axlim_clip=False, + **kwargs): mtext.Text.__init__(self, x, y, text, **kwargs) - self.set_3d_properties(z, zdir) + self.set_3d_properties(z, zdir, axlim_clip) def get_position_3d(self): """Return the (x, y, z) position of the text.""" @@ -129,7 +160,7 @@ def set_z(self, z): self._z = z self.stale = True - def set_3d_properties(self, z=0, zdir='z'): + def set_3d_properties(self, z=0, zdir='z', axlim_clip=False): """ Set the *z* position and direction of the text. @@ -140,14 +171,23 @@ def set_3d_properties(self, z=0, zdir='z'): zdir : {'x', 'y', 'z', 3-tuple} The direction of the text. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide text outside the axes view limits. """ self._z = z self._dir_vec = get_dir_vector(zdir) + self._axlim_clip = axlim_clip self.stale = True @artist.allow_rasterization def draw(self, renderer): - position3d = np.array((self._x, self._y, self._z)) + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(self._x, self._y, self._z, self.axes) + position3d = np.ma.row_stack((xs, ys, zs)).ravel().filled(np.nan) + else: + xs, ys, zs = self._x, self._y, self._z + position3d = np.asanyarray([xs, ys, zs]) + proj = proj3d._proj_trans_points( [position3d, position3d + self._dir_vec], self.axes.M) dx = proj[0][1] - proj[0][0] @@ -164,7 +204,7 @@ def get_tightbbox(self, renderer=None): return None -def text_2d_to_3d(obj, z=0, zdir='z'): +def text_2d_to_3d(obj, z=0, zdir='z', axlim_clip=False): """ Convert a `.Text` to a `.Text3D` object. @@ -175,9 +215,11 @@ def text_2d_to_3d(obj, z=0, zdir='z'): zdir : {'x', 'y', 'z', 3-tuple} The direction of the text. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide text outside the axes view limits. """ obj.__class__ = Text3D - obj.set_3d_properties(z, zdir) + obj.set_3d_properties(z, zdir, axlim_clip) class Line3D(lines.Line2D): @@ -191,7 +233,7 @@ class Line3D(lines.Line2D): `~.Line2D.set_data`, `~.Line2D.set_xdata`, and `~.Line2D.set_ydata`. """ - def __init__(self, xs, ys, zs, *args, **kwargs): + def __init__(self, xs, ys, zs, *args, axlim_clip=False, **kwargs): """ Parameters @@ -207,8 +249,9 @@ def __init__(self, xs, ys, zs, *args, **kwargs): """ super().__init__([], [], *args, **kwargs) self.set_data_3d(xs, ys, zs) + self._axlim_clip = axlim_clip - def set_3d_properties(self, zs=0, zdir='z'): + def set_3d_properties(self, zs=0, zdir='z', axlim_clip=False): """ Set the *z* position and direction of the line. @@ -220,12 +263,15 @@ def set_3d_properties(self, zs=0, zdir='z'): zdir : {'x', 'y', 'z'} Plane to plot line orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide lines with an endpoint outside the axes view limits. """ xs = self.get_xdata() ys = self.get_ydata() zs = cbook._to_unmasked_float_array(zs).ravel() zs = np.broadcast_to(zs, len(xs)) self._verts3d = juggle_axes(xs, ys, zs, zdir) + self._axlim_clip = axlim_clip self.stale = True def set_data_3d(self, *args): @@ -266,7 +312,10 @@ def get_data_3d(self): @artist.allow_rasterization def draw(self, renderer): - xs3d, ys3d, zs3d = self._verts3d + if self._axlim_clip: + xs3d, ys3d, zs3d = _viewlim_mask(*self._verts3d, self.axes) + else: + xs3d, ys3d, zs3d = self._verts3d xs, ys, zs, tis = proj3d._proj_transform_clip(xs3d, ys3d, zs3d, self.axes.M, self.axes._focal_length) @@ -275,7 +324,7 @@ def draw(self, renderer): self.stale = False -def line_2d_to_3d(line, zs=0, zdir='z'): +def line_2d_to_3d(line, zs=0, zdir='z', axlim_clip=False): """ Convert a `.Line2D` to a `.Line3D` object. @@ -286,10 +335,12 @@ def line_2d_to_3d(line, zs=0, zdir='z'): zdir : {'x', 'y', 'z'} Plane to plot line orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide lines with an endpoint outside the axes view limits. """ line.__class__ = Line3D - line.set_3d_properties(zs, zdir) + line.set_3d_properties(zs, zdir, axlim_clip) def _path_to_3d_segment(path, zs=0, zdir='z'): @@ -351,15 +402,18 @@ class Collection3D(Collection): def do_3d_projection(self): """Project the points according to renderer matrix.""" - xyzs_list = [proj3d.proj_transform(*vs.T, self.axes.M) - for vs, _ in self._3dverts_codes] - self._paths = [mpath.Path(np.column_stack([xs, ys]), cs) + vs_list = [vs for vs, _ in self._3dverts_codes] + if self._axlim_clip: + vs_list = [np.ma.row_stack(_viewlim_mask(*vs.T, self.axes)).T + for vs in vs_list] + xyzs_list = [proj3d.proj_transform(*vs.T, self.axes.M) for vs in vs_list] + self._paths = [mpath.Path(np.ma.column_stack([xs, ys]), cs) for (xs, ys, _), (_, cs) in zip(xyzs_list, self._3dverts_codes)] zs = np.concatenate([zs for _, _, zs in xyzs_list]) return zs.min() if len(zs) else 1e9 -def collection_2d_to_3d(col, zs=0, zdir='z'): +def collection_2d_to_3d(col, zs=0, zdir='z', axlim_clip=False): """Convert a `.Collection` to a `.Collection3D` object.""" zs = np.broadcast_to(zs, len(col.get_paths())) col._3dverts_codes = [ @@ -369,12 +423,16 @@ def collection_2d_to_3d(col, zs=0, zdir='z'): p.codes) for p, z in zip(col.get_paths(), zs)] col.__class__ = cbook._make_class_factory(Collection3D, "{}3D")(type(col)) + col._axlim_clip = axlim_clip class Line3DCollection(LineCollection): """ A collection of 3D lines. """ + def __init__(self, lines, axlim_clip=False, **kwargs): + super().__init__(lines, **kwargs) + self._axlim_clip = axlim_clip def set_sort_zpos(self, val): """Set the position to use for z-sorting.""" @@ -392,9 +450,13 @@ def do_3d_projection(self): """ Project the points according to renderer matrix. """ + segments = self._segments3d + if self._axlim_clip: + segments = [np.ma.column_stack([*_viewlim_mask(*zip(*points), self.axes)]) + for points in segments] xyslist = [proj3d._proj_trans_points(points, self.axes.M) - for points in self._segments3d] - segments_2d = [np.column_stack([xs, ys]) for xs, ys, zs in xyslist] + for points in segments] + segments_2d = [np.ma.column_stack([xs, ys]) for xs, ys, zs in xyslist] LineCollection.set_segments(self, segments_2d) # FIXME @@ -404,11 +466,12 @@ def do_3d_projection(self): return minz -def line_collection_2d_to_3d(col, zs=0, zdir='z'): +def line_collection_2d_to_3d(col, zs=0, zdir='z', axlim_clip=False): """Convert a `.LineCollection` to a `.Line3DCollection` object.""" segments3d = _paths_to_3d_segments(col.get_paths(), zs, zdir) col.__class__ = Line3DCollection col.set_segments(segments3d) + col._axlim_clip = axlim_clip class Patch3D(Patch): @@ -416,7 +479,7 @@ class Patch3D(Patch): 3D patch object. """ - def __init__(self, *args, zs=(), zdir='z', **kwargs): + def __init__(self, *args, zs=(), zdir='z', axlim_clip=False, **kwargs): """ Parameters ---------- @@ -427,11 +490,13 @@ def __init__(self, *args, zs=(), zdir='z', **kwargs): zdir : {'x', 'y', 'z'} Plane to plot patch orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. """ super().__init__(*args, **kwargs) - self.set_3d_properties(zs, zdir) + self.set_3d_properties(zs, zdir, axlim_clip) - def set_3d_properties(self, verts, zs=0, zdir='z'): + def set_3d_properties(self, verts, zs=0, zdir='z', axlim_clip=False): """ Set the *z* position and direction of the patch. @@ -444,10 +509,13 @@ def set_3d_properties(self, verts, zs=0, zdir='z'): zdir : {'x', 'y', 'z'} Plane to plot patch orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. """ zs = np.broadcast_to(zs, len(verts)) self._segment3d = [juggle_axes(x, y, z, zdir) for ((x, y), z) in zip(verts, zs)] + self._axlim_clip = axlim_clip def get_path(self): # docstring inherited @@ -459,11 +527,14 @@ def get_path(self): def do_3d_projection(self): s = self._segment3d - xs, ys, zs = zip(*s) + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*zip(*s), self.axes) + else: + xs, ys, zs = zip(*s) vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, self.axes.M, self.axes._focal_length) - self._path2d = mpath.Path(np.column_stack([vxs, vys])) + self._path2d = mpath.Path(np.ma.column_stack([vxs, vys])) return min(vzs) @@ -472,7 +543,7 @@ class PathPatch3D(Patch3D): 3D PathPatch object. """ - def __init__(self, path, *, zs=(), zdir='z', **kwargs): + def __init__(self, path, *, zs=(), zdir='z', axlim_clip=False, **kwargs): """ Parameters ---------- @@ -483,12 +554,14 @@ def __init__(self, path, *, zs=(), zdir='z', **kwargs): zdir : {'x', 'y', 'z', 3-tuple} Plane to plot path patch orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide path patches with a point outside the axes view limits. """ # Not super().__init__! Patch.__init__(self, **kwargs) - self.set_3d_properties(path, zs, zdir) + self.set_3d_properties(path, zs, zdir, axlim_clip) - def set_3d_properties(self, path, zs=0, zdir='z'): + def set_3d_properties(self, path, zs=0, zdir='z', axlim_clip=False): """ Set the *z* position and direction of the path patch. @@ -501,17 +574,23 @@ def set_3d_properties(self, path, zs=0, zdir='z'): zdir : {'x', 'y', 'z', 3-tuple} Plane to plot path patch orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide path patches with a point outside the axes view limits. """ - Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir) + Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir, + axlim_clip=axlim_clip) self._code3d = path.codes def do_3d_projection(self): s = self._segment3d - xs, ys, zs = zip(*s) + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*zip(*s), self.axes) + else: + xs, ys, zs = zip(*s) vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, self.axes.M, self.axes._focal_length) - self._path2d = mpath.Path(np.column_stack([vxs, vys]), self._code3d) + self._path2d = mpath.Path(np.ma.column_stack([vxs, vys]), self._code3d) return min(vzs) @@ -523,11 +602,11 @@ def _get_patch_verts(patch): return polygons[0] if len(polygons) else np.array([]) -def patch_2d_to_3d(patch, z=0, zdir='z'): +def patch_2d_to_3d(patch, z=0, zdir='z', axlim_clip=False): """Convert a `.Patch` to a `.Patch3D` object.""" verts = _get_patch_verts(patch) patch.__class__ = Patch3D - patch.set_3d_properties(verts, z, zdir) + patch.set_3d_properties(verts, z, zdir, axlim_clip) def pathpatch_2d_to_3d(pathpatch, z=0, zdir='z'): @@ -545,7 +624,8 @@ class Patch3DCollection(PatchCollection): A collection of 3D patches. """ - def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): + def __init__(self, *args, + zs=0, zdir='z', depthshade=True, axlim_clip=False, **kwargs): """ Create a collection of flat 3D patches with its normal vector pointed in *zdir* direction, and located at *zs* on the *zdir* @@ -562,7 +642,7 @@ def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): """ self._depthshade = depthshade super().__init__(*args, **kwargs) - self.set_3d_properties(zs, zdir) + self.set_3d_properties(zs, zdir, axlim_clip) def get_depthshade(self): return self._depthshade @@ -585,7 +665,7 @@ def set_sort_zpos(self, val): self._sort_zpos = val self.stale = True - def set_3d_properties(self, zs, zdir): + def set_3d_properties(self, zs, zdir, axlim_clip=False): """ Set the *z* positions and direction of the patches. @@ -598,6 +678,8 @@ def set_3d_properties(self, zs, zdir): Plane to plot patches orthogonal to. All patches must have the same direction. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. """ # Force the collection to initialize the face and edgecolors # just in case it is a scalarmappable with a colormap. @@ -611,15 +693,19 @@ def set_3d_properties(self, zs, zdir): self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir) self._z_markers_idx = slice(-1) self._vzs = None + self._axlim_clip = axlim_clip self.stale = True def do_3d_projection(self): - xs, ys, zs = self._offsets3d + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*self._offsets3d, self.axes) + else: + xs, ys, zs = self._offsets3d vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, self.axes.M, self.axes._focal_length) self._vzs = vzs - super().set_offsets(np.column_stack([vxs, vys])) + super().set_offsets(np.ma.column_stack([vxs, vys])) if vzs.size > 0: return min(vzs) @@ -653,7 +739,8 @@ class Path3DCollection(PathCollection): A collection of 3D paths. """ - def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): + def __init__(self, *args, + zs=0, zdir='z', depthshade=True, axlim_clip=False, **kwargs): """ Create a collection of flat 3D paths with its normal vector pointed in *zdir* direction, and located at *zs* on the *zdir* @@ -671,7 +758,7 @@ def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): self._depthshade = depthshade self._in_draw = False super().__init__(*args, **kwargs) - self.set_3d_properties(zs, zdir) + self.set_3d_properties(zs, zdir, axlim_clip) self._offset_zordered = None def draw(self, renderer): @@ -684,7 +771,7 @@ def set_sort_zpos(self, val): self._sort_zpos = val self.stale = True - def set_3d_properties(self, zs, zdir): + def set_3d_properties(self, zs, zdir, axlim_clip=False): """ Set the *z* positions and direction of the paths. @@ -697,6 +784,8 @@ def set_3d_properties(self, zs, zdir): Plane to plot paths orthogonal to. All paths must have the same direction. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide paths with a vertex outside the axes view limits. """ # Force the collection to initialize the face and edgecolors # just in case it is a scalarmappable with a colormap. @@ -707,6 +796,7 @@ def set_3d_properties(self, zs, zdir): else: xs = [] ys = [] + self._zdir = zdir self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir) # In the base draw methods we access the attributes directly which # means we cannot resolve the shuffling in the getter methods like @@ -727,6 +817,8 @@ def set_3d_properties(self, zs, zdir): # points and point properties according to the index array self._z_markers_idx = slice(-1) self._vzs = None + + self._axlim_clip = axlim_clip self.stale = True def set_sizes(self, sizes, dpi=72.0): @@ -756,14 +848,17 @@ def set_depthshade(self, depthshade): self.stale = True def do_3d_projection(self): - xs, ys, zs = self._offsets3d + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*self._offsets3d, self.axes) + else: + xs, ys, zs = self._offsets3d vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, self.axes.M, self.axes._focal_length) # Sort the points based on z coordinates # Performance optimization: Create a sorted index array and reorder # points and point properties according to the index array - z_markers_idx = self._z_markers_idx = np.argsort(vzs)[::-1] + z_markers_idx = self._z_markers_idx = np.ma.argsort(vzs)[::-1] self._vzs = vzs # we have to special case the sizes because of code in collections.py @@ -777,7 +872,7 @@ def do_3d_projection(self): if len(self._linewidths3d) > 1: self._linewidths = self._linewidths3d[z_markers_idx] - PathCollection.set_offsets(self, np.column_stack((vxs, vys))) + PathCollection.set_offsets(self, np.ma.column_stack((vxs, vys))) # Re-order items vzs = vzs[z_markers_idx] @@ -785,7 +880,7 @@ def do_3d_projection(self): vys = vys[z_markers_idx] # Store ordered offset for drawing purpose - self._offset_zordered = np.column_stack((vxs, vys)) + self._offset_zordered = np.ma.column_stack((vxs, vys)) return np.min(vzs) if vzs.size else np.nan @@ -825,7 +920,7 @@ def get_edgecolor(self): return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor()) -def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): +def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True, axlim_clip=False): """ Convert a `.PatchCollection` into a `.Patch3DCollection` object (or a `.PathCollection` into a `.Path3DCollection` object). @@ -843,7 +938,8 @@ def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): See `.get_dir_vector` for a description of the values. depthshade : bool, default: True Whether to shade the patches to give a sense of depth. - + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. """ if isinstance(col, PathCollection): col.__class__ = Path3DCollection @@ -852,7 +948,7 @@ def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): col.__class__ = Patch3DCollection col._depthshade = depthshade col._in_draw = False - col.set_3d_properties(zs, zdir) + col.set_3d_properties(zs, zdir, axlim_clip) class Poly3DCollection(PolyCollection): @@ -877,7 +973,7 @@ class Poly3DCollection(PolyCollection): """ def __init__(self, verts, *args, zsort='average', shade=False, - lightsource=None, **kwargs): + lightsource=None, axlim_clip=False, **kwargs): """ Parameters ---------- @@ -899,6 +995,9 @@ def __init__(self, verts, *args, zsort='average', shade=False, .. versionadded:: 3.7 + axlim_clip : bool, default: False + Whether to hide polygons with a vertex outside the view limits. + *args, **kwargs All other parameters are forwarded to `.PolyCollection`. @@ -933,6 +1032,7 @@ def __init__(self, verts, *args, zsort='average', shade=False, raise ValueError('verts must be a list of (N, 3) array-like') self.set_zsort(zsort) self._codes3d = None + self._axlim_clip = axlim_clip _zsort_functions = { 'average': np.average, @@ -997,7 +1097,7 @@ def set_verts_and_codes(self, verts, codes): # and set our own codes instead. self._codes3d = codes - def set_3d_properties(self): + def set_3d_properties(self, axlim_clip=False): # Force the collection to initialize the face and edgecolors # just in case it is a scalarmappable with a colormap. self.update_scalarmappable() @@ -1030,7 +1130,16 @@ def do_3d_projection(self): self._facecolor3d = self._facecolors if self._edge_is_mapped: self._edgecolor3d = self._edgecolors - txs, tys, tzs = proj3d._proj_transform_vec(self._vec, self.axes.M) + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*self._vec[0:3], self.axes) + if self._vec.shape[0] == 4: # Will be 3 (xyz) or 4 (xyzw) + w_masked = np.ma.masked_where(zs.mask, self._vec[3]) + vec = np.ma.array([xs, ys, zs, w_masked]) + else: + vec = np.ma.array([xs, ys, zs]) + else: + vec = self._vec + txs, tys, tzs = proj3d._proj_transform_vec(vec, self.axes.M) xyzlist = [(txs[sl], tys[sl], tzs[sl]) for sl in self._segslices] # This extra fuss is to re-order face / edge colors @@ -1047,7 +1156,7 @@ def do_3d_projection(self): if xyzlist: # sort by depth (furthest drawn first) z_segments_2d = sorted( - ((self._zsortfunc(zs), np.column_stack([xs, ys]), fc, ec, idx) + ((self._zsortfunc(zs.data), np.ma.column_stack([xs, ys]), fc, ec, idx) for idx, ((xs, ys, zs), fc, ec) in enumerate(zip(xyzlist, cface, cedge))), key=lambda x: x[0], reverse=True) @@ -1124,7 +1233,7 @@ def get_edgecolor(self): return np.asarray(self._edgecolors2d) -def poly_collection_2d_to_3d(col, zs=0, zdir='z'): +def poly_collection_2d_to_3d(col, zs=0, zdir='z', axlim_clip=False): """ Convert a `.PolyCollection` into a `.Poly3DCollection` object. @@ -1144,6 +1253,7 @@ def poly_collection_2d_to_3d(col, zs=0, zdir='z'): col.__class__ = Poly3DCollection col.set_verts_and_codes(segments_3d, codes) col.set_3d_properties() + col._axlim_clip = axlim_clip def juggle_axes(xs, ys, zs, zdir): diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index ea93d3eadf82..94dcdd74974a 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -1887,7 +1887,7 @@ def get_zbound(self): else: return upper, lower - def text(self, x, y, z, s, zdir=None, **kwargs): + def text(self, x, y, z, s, zdir=None, *, axlim_clip=False, **kwargs): """ Add the text *s* to the 3D Axes at location *x*, *y*, *z* in data coordinates. @@ -1900,6 +1900,8 @@ def text(self, x, y, z, s, zdir=None, **kwargs): zdir : {'x', 'y', 'z', 3-tuple}, optional The direction to be used as the z-direction. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide text that is outside the axes view limits. **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.text`. @@ -1909,13 +1911,13 @@ def text(self, x, y, z, s, zdir=None, **kwargs): The created `.Text3D` instance. """ text = super().text(x, y, s, **kwargs) - art3d.text_2d_to_3d(text, z, zdir) + art3d.text_2d_to_3d(text, z, zdir, axlim_clip) return text text3D = text text2D = Axes.text - def plot(self, xs, ys, *args, zdir='z', **kwargs): + def plot(self, xs, ys, *args, zdir='z', axlim_clip=False, **kwargs): """ Plot 2D or 3D data. @@ -1930,6 +1932,8 @@ def plot(self, xs, ys, *args, zdir='z', **kwargs): each point. zdir : {'x', 'y', 'z'}, default: 'z' When plotting 2D data, the direction to use as z. + axlim_clip : bool, default: False + Whether to hide data that is outside the axes view limits. **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.plot`. """ @@ -1949,7 +1953,7 @@ def plot(self, xs, ys, *args, zdir='z', **kwargs): lines = super().plot(xs, ys, *args, **kwargs) for line in lines: - art3d.line_2d_to_3d(line, zs=zs, zdir=zdir) + art3d.line_2d_to_3d(line, zs=zs, zdir=zdir, axlim_clip=axlim_clip) xs, ys, zs = art3d.juggle_axes(xs, ys, zs, zdir) self.auto_scale_xyz(xs, ys, zs, had_data) @@ -2082,7 +2086,7 @@ def fill_between(self, x1, y1, z1, x2, y2, z2, *, return polyc def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, - vmax=None, lightsource=None, **kwargs): + vmax=None, lightsource=None, axlim_clip=False, **kwargs): """ Create a surface plot. @@ -2147,6 +2151,9 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, lightsource : `~matplotlib.colors.LightSource`, optional The lightsource to use when *shade* is True. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. + **kwargs Other keyword arguments are forwarded to `.Poly3DCollection`. """ @@ -2247,9 +2254,9 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, if fcolors is not None: polyc = art3d.Poly3DCollection( polys, edgecolors=colset, facecolors=colset, shade=shade, - lightsource=lightsource, **kwargs) + lightsource=lightsource, axlim_clip=axlim_clip, **kwargs) elif cmap: - polyc = art3d.Poly3DCollection(polys, **kwargs) + polyc = art3d.Poly3DCollection(polys, axlim_clip=axlim_clip, **kwargs) # can't always vectorize, because polys might be jagged if isinstance(polys, np.ndarray): avg_z = polys[..., 2].mean(axis=-1) @@ -2267,15 +2274,15 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, color = np.array(mcolors.to_rgba(color)) polyc = art3d.Poly3DCollection( - polys, facecolors=color, shade=shade, - lightsource=lightsource, **kwargs) + polys, facecolors=color, shade=shade, lightsource=lightsource, + axlim_clip=axlim_clip, **kwargs) self.add_collection(polyc) self.auto_scale_xyz(X, Y, Z, had_data) return polyc - def plot_wireframe(self, X, Y, Z, **kwargs): + def plot_wireframe(self, X, Y, Z, *, axlim_clip=False, **kwargs): """ Plot a 3D wireframe. @@ -2291,6 +2298,10 @@ def plot_wireframe(self, X, Y, Z, **kwargs): X, Y, Z : 2D arrays Data values. + axlim_clip : bool, default: False + Whether to hide lines and patches with vertices outside the axes + view limits. + rcount, ccount : int Maximum number of samples used in each direction. If the input data is larger, it will be downsampled (by slicing) to these @@ -2387,14 +2398,14 @@ def plot_wireframe(self, X, Y, Z, **kwargs): + [list(zip(xl, yl, zl)) for xl, yl, zl in zip(txlines, tylines, tzlines)]) - linec = art3d.Line3DCollection(lines, **kwargs) + linec = art3d.Line3DCollection(lines, axlim_clip=axlim_clip, **kwargs) self.add_collection(linec) self.auto_scale_xyz(X, Y, Z, had_data) return linec def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, - lightsource=None, **kwargs): + lightsource=None, axlim_clip=False, **kwargs): """ Plot a triangulated surface. @@ -2436,6 +2447,8 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, *cmap* is specified. lightsource : `~matplotlib.colors.LightSource`, optional The lightsource to use when *shade* is True. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. **kwargs All other keyword arguments are passed on to :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` @@ -2472,7 +2485,8 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, verts = np.stack((xt, yt, zt), axis=-1) if cmap: - polyc = art3d.Poly3DCollection(verts, *args, **kwargs) + polyc = art3d.Poly3DCollection(verts, *args, + axlim_clip=axlim_clip, **kwargs) # average over the three points of each triangle avg_z = verts[:, :, 2].mean(axis=1) polyc.set_array(avg_z) @@ -2483,7 +2497,7 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, else: polyc = art3d.Poly3DCollection( verts, *args, shade=shade, lightsource=lightsource, - facecolors=color, **kwargs) + facecolors=color, axlim_clip=axlim_clip, **kwargs) self.add_collection(polyc) self.auto_scale_xyz(tri.x, tri.y, z, had_data) @@ -2519,18 +2533,21 @@ def _3d_extend_contour(self, cset, stride=5): cset.remove() def add_contour_set( - self, cset, extend3d=False, stride=5, zdir='z', offset=None): + self, cset, extend3d=False, stride=5, zdir='z', offset=None, + axlim_clip=False): zdir = '-' + zdir if extend3d: self._3d_extend_contour(cset, stride) else: art3d.collection_2d_to_3d( - cset, zs=offset if offset is not None else cset.levels, zdir=zdir) + cset, zs=offset if offset is not None else cset.levels, zdir=zdir, + axlim_clip=axlim_clip) - def add_contourf_set(self, cset, zdir='z', offset=None): - self._add_contourf_set(cset, zdir=zdir, offset=offset) + def add_contourf_set(self, cset, zdir='z', offset=None, *, axlim_clip=False): + self._add_contourf_set(cset, zdir=zdir, offset=offset, + axlim_clip=axlim_clip) - def _add_contourf_set(self, cset, zdir='z', offset=None): + def _add_contourf_set(self, cset, zdir='z', offset=None, axlim_clip=False): """ Returns ------- @@ -2549,12 +2566,14 @@ def _add_contourf_set(self, cset, zdir='z', offset=None): midpoints = np.append(midpoints, max_level) art3d.collection_2d_to_3d( - cset, zs=offset if offset is not None else midpoints, zdir=zdir) + cset, zs=offset if offset is not None else midpoints, zdir=zdir, + axlim_clip=axlim_clip) return midpoints @_preprocess_data() def contour(self, X, Y, Z, *args, - extend3d=False, stride=5, zdir='z', offset=None, **kwargs): + extend3d=False, stride=5, zdir='z', offset=None, axlim_clip=False, + **kwargs): """ Create a 3D contour plot. @@ -2573,6 +2592,8 @@ def contour(self, X, Y, Z, *args, position in a plane normal to *zdir*. data : indexable object, optional DATA_PARAMETER_PLACEHOLDER + axlim_clip : bool, default: False + Whether to hide lines with a vertex outside the axes view limits. *args, **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.contour`. @@ -2585,7 +2606,7 @@ def contour(self, X, Y, Z, *args, jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) cset = super().contour(jX, jY, jZ, *args, **kwargs) - self.add_contour_set(cset, extend3d, stride, zdir, offset) + self.add_contour_set(cset, extend3d, stride, zdir, offset, axlim_clip) self.auto_scale_xyz(X, Y, Z, had_data) return cset @@ -2594,7 +2615,8 @@ def contour(self, X, Y, Z, *args, @_preprocess_data() def tricontour(self, *args, - extend3d=False, stride=5, zdir='z', offset=None, **kwargs): + extend3d=False, stride=5, zdir='z', offset=None, axlim_clip=False, + **kwargs): """ Create a 3D contour plot. @@ -2617,6 +2639,8 @@ def tricontour(self, *args, position in a plane normal to *zdir*. data : indexable object, optional DATA_PARAMETER_PLACEHOLDER + axlim_clip : bool, default: False + Whether to hide lines with a vertex outside the axes view limits. *args, **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.tricontour`. @@ -2640,7 +2664,7 @@ def tricontour(self, *args, tri = Triangulation(jX, jY, tri.triangles, tri.mask) cset = super().tricontour(tri, jZ, *args, **kwargs) - self.add_contour_set(cset, extend3d, stride, zdir, offset) + self.add_contour_set(cset, extend3d, stride, zdir, offset, axlim_clip) self.auto_scale_xyz(X, Y, Z, had_data) return cset @@ -2656,7 +2680,8 @@ def _auto_scale_contourf(self, X, Y, Z, zdir, levels, had_data): self.auto_scale_xyz(*limits, had_data) @_preprocess_data() - def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): + def contourf(self, X, Y, Z, *args, + zdir='z', offset=None, axlim_clip=False, **kwargs): """ Create a 3D filled contour plot. @@ -2671,6 +2696,8 @@ def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): position in a plane normal to *zdir*. data : indexable object, optional DATA_PARAMETER_PLACEHOLDER + axlim_clip : bool, default: False + Whether to hide lines with a vertex outside the axes view limits. *args, **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.contourf`. @@ -2682,7 +2709,7 @@ def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) cset = super().contourf(jX, jY, jZ, *args, **kwargs) - levels = self._add_contourf_set(cset, zdir, offset) + levels = self._add_contourf_set(cset, zdir, offset, axlim_clip) self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data) return cset @@ -2690,7 +2717,7 @@ def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): contourf3D = contourf @_preprocess_data() - def tricontourf(self, *args, zdir='z', offset=None, **kwargs): + def tricontourf(self, *args, zdir='z', offset=None, axlim_clip=False, **kwargs): """ Create a 3D filled contour plot. @@ -2709,6 +2736,8 @@ def tricontourf(self, *args, zdir='z', offset=None, **kwargs): position in a plane normal to zdir. data : indexable object, optional DATA_PARAMETER_PLACEHOLDER + axlim_clip : bool, default: False + Whether to hide lines with a vertex outside the axes view limits. *args, **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.tricontourf`. @@ -2733,12 +2762,13 @@ def tricontourf(self, *args, zdir='z', offset=None, **kwargs): tri = Triangulation(jX, jY, tri.triangles, tri.mask) cset = super().tricontourf(tri, jZ, *args, **kwargs) - levels = self._add_contourf_set(cset, zdir, offset) + levels = self._add_contourf_set(cset, zdir, offset, axlim_clip) self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data) return cset - def add_collection3d(self, col, zs=0, zdir='z', autolim=True): + def add_collection3d(self, col, zs=0, zdir='z', autolim=True, *, + axlim_clip=False): """ Add a 3D collection object to the plot. @@ -2762,6 +2792,8 @@ def add_collection3d(self, col, zs=0, zdir='z', autolim=True): The direction to use for the z-positions. autolim : bool, default: True Whether to update the data limits. + axlim_clip : bool, default: False + Whether to hide the scatter points outside the axes view limits. """ had_data = self.has_data() @@ -2773,13 +2805,16 @@ def add_collection3d(self, col, zs=0, zdir='z', autolim=True): # object would also pass.) Maybe have a collection3d # abstract class to test for and exclude? if type(col) is mcoll.PolyCollection: - art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir) + art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir, + axlim_clip=axlim_clip) col.set_sort_zpos(zsortval) elif type(col) is mcoll.LineCollection: - art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir) + art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir, + axlim_clip=axlim_clip) col.set_sort_zpos(zsortval) elif type(col) is mcoll.PatchCollection: - art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir) + art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir, + axlim_clip=axlim_clip) col.set_sort_zpos(zsortval) if autolim: @@ -2800,8 +2835,9 @@ def add_collection3d(self, col, zs=0, zdir='z', autolim=True): @_preprocess_data(replace_names=["xs", "ys", "zs", "s", "edgecolors", "c", "facecolor", "facecolors", "color"]) - def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, - *args, **kwargs): + def scatter(self, xs, ys, + zs=0, zdir='z', s=20, c=None, depthshade=True, *args, + axlim_clip=False, **kwargs): """ Create a scatter plot. @@ -2837,6 +2873,8 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, Whether to shade the scatter markers to give the appearance of depth. Each call to ``scatter()`` will perform its depthshading independently. + axlim_clip : bool, default: False + Whether to hide the scatter points outside the axes view limits. data : indexable object, optional DATA_PARAMETER_PLACEHOLDER **kwargs @@ -2865,7 +2903,8 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, patches = super().scatter(xs, ys, s=s, c=c, *args, **kwargs) art3d.patch_collection_2d_to_3d(patches, zs=zs, zdir=zdir, - depthshade=depthshade) + depthshade=depthshade, + axlim_clip=axlim_clip) if self._zmargin < 0.05 and xs.size > 0: self.set_zmargin(0.05) @@ -2877,7 +2916,8 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, scatter3D = scatter @_preprocess_data() - def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): + def bar(self, left, height, zs=0, zdir='z', *args, + axlim_clip=False, **kwargs): """ Add 2D bar(s). @@ -2894,6 +2934,8 @@ def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): When plotting 2D data, the direction to use as z ('x', 'y' or 'z'). data : indexable object, optional DATA_PARAMETER_PLACEHOLDER + axlim_clip : bool, default: False + Whether to hide bars with points outside the axes view limits. **kwargs Other keyword arguments are forwarded to `matplotlib.axes.Axes.bar`. @@ -2914,7 +2956,7 @@ def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): vs = art3d._get_patch_verts(p) verts += vs.tolist() verts_zs += [z] * len(vs) - art3d.patch_2d_to_3d(p, z, zdir) + art3d.patch_2d_to_3d(p, z, zdir, axlim_clip) if 'alpha' in kwargs: p.set_alpha(kwargs['alpha']) @@ -2933,7 +2975,8 @@ def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): @_preprocess_data() def bar3d(self, x, y, z, dx, dy, dz, color=None, - zsort='average', shade=True, lightsource=None, *args, **kwargs): + zsort='average', shade=True, lightsource=None, *args, + axlim_clip=False, **kwargs): """ Generate a 3D barplot. @@ -2983,6 +3026,9 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None, data : indexable object, optional DATA_PARAMETER_PLACEHOLDER + axlim_clip : bool, default: False + Whether to hide the bars with points outside the axes view limits. + **kwargs Any additional keyword arguments are passed onto `~.art3d.Poly3DCollection`. @@ -3085,6 +3131,7 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None, facecolors=facecolors, shade=shade, lightsource=lightsource, + axlim_clip=axlim_clip, *args, **kwargs) self.add_collection(col) @@ -3102,7 +3149,7 @@ def set_title(self, label, fontdict=None, loc='center', **kwargs): @_preprocess_data() def quiver(self, X, Y, Z, U, V, W, *, length=1, arrow_length_ratio=.3, pivot='tail', normalize=False, - **kwargs): + axlim_clip=False, **kwargs): """ Plot a 3D field of arrows. @@ -3137,6 +3184,9 @@ def quiver(self, X, Y, Z, U, V, W, *, data : indexable object, optional DATA_PARAMETER_PLACEHOLDER + axlim_clip : bool, default: False + Whether to hide arrows with points outside the axes view limits. + **kwargs Any additional keyword arguments are delegated to :class:`.Line3DCollection` @@ -3215,7 +3265,7 @@ def calc_arrows(UVW): else: lines = [] - linec = art3d.Line3DCollection(lines, **kwargs) + linec = art3d.Line3DCollection(lines, axlim_clip=axlim_clip, **kwargs) self.add_collection(linec) self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data) @@ -3225,7 +3275,7 @@ def calc_arrows(UVW): quiver3D = quiver def voxels(self, *args, facecolors=None, edgecolors=None, shade=True, - lightsource=None, **kwargs): + lightsource=None, axlim_clip=False, **kwargs): """ ax.voxels([x, y, z,] /, filled, facecolors=None, edgecolors=None, \ **kwargs) @@ -3272,6 +3322,9 @@ def voxels(self, *args, facecolors=None, edgecolors=None, shade=True, lightsource : `~matplotlib.colors.LightSource`, optional The lightsource to use when *shade* is True. + axlim_clip : bool, default: False + Whether to hide voxels with points outside the axes view limits. + **kwargs Additional keyword arguments to pass onto `~mpl_toolkits.mplot3d.art3d.Poly3DCollection`. @@ -3427,7 +3480,8 @@ def permutation_matrices(n): poly = art3d.Poly3DCollection( faces, facecolors=facecolor, edgecolors=edgecolor, - shade=shade, lightsource=lightsource, **kwargs) + shade=shade, lightsource=lightsource, axlim_clip=axlim_clip, + **kwargs) self.add_collection3d(poly) polygons[coord] = poly @@ -3438,6 +3492,7 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', barsabove=False, errorevery=1, ecolor=None, elinewidth=None, capsize=None, capthick=None, xlolims=False, xuplims=False, ylolims=False, yuplims=False, zlolims=False, zuplims=False, + axlim_clip=False, **kwargs): """ Plot lines and/or markers with errorbars around them. @@ -3515,6 +3570,9 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', Used to avoid overlapping error bars when two series share x-axis values. + axlim_clip : bool, default: False + Whether to hide error bars that are outside the axes limits. + Returns ------- errlines : list @@ -3570,7 +3628,7 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', # data processing. (data_line, base_style), = self._get_lines._plot_args( self, (x, y) if fmt == '' else (x, y, fmt), kwargs, return_kwargs=True) - art3d.line_2d_to_3d(data_line, zs=z) + art3d.line_2d_to_3d(data_line, zs=z, axlim_clip=axlim_clip) # Do this after creating `data_line` to avoid modifying `base_style`. if barsabove: @@ -3714,9 +3772,11 @@ def _extract_errs(err, data, lomask, himask): # these markers will rotate as the viewing angle changes cap_lo = art3d.Line3D(*lo_caps_xyz, ls='', marker=capmarker[i_zdir], + axlim_clip=axlim_clip, **eb_cap_style) cap_hi = art3d.Line3D(*hi_caps_xyz, ls='', marker=capmarker[i_zdir], + axlim_clip=axlim_clip, **eb_cap_style) self.add_line(cap_lo) self.add_line(cap_hi) @@ -3731,6 +3791,7 @@ def _extract_errs(err, data, lomask, himask): self.quiver(xl0, yl0, zl0, *-dir_vector, **eb_quiver_style) errline = art3d.Line3DCollection(np.array(coorderr).T, + axlim_clip=axlim_clip, **eb_lines_style) self.add_collection(errline) errlines.append(errline) @@ -3776,7 +3837,7 @@ def get_tightbbox(self, renderer=None, call_axes_locator=True, @_preprocess_data() def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', - bottom=0, label=None, orientation='z'): + bottom=0, label=None, orientation='z', axlim_clip=False): """ Create a 3D stem plot. @@ -3829,6 +3890,9 @@ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', data : indexable object, optional DATA_PARAMETER_PLACEHOLDER + axlim_clip : bool, default: False + Whether to hide stems that are outside the axes limits. + Returns ------- `.StemContainer` @@ -3877,7 +3941,8 @@ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', baseline, = self.plot(basex, basey, basefmt, zs=bottom, zdir=orientation, label='_nolegend_') stemlines = art3d.Line3DCollection( - lines, linestyles=linestyle, colors=linecolor, label='_nolegend_') + lines, linestyles=linestyle, colors=linecolor, label='_nolegend_', + axlim_clip=axlim_clip) self.add_collection(stemlines) markerline, = self.plot(x, y, z, markerfmt, label='_nolegend_') diff --git a/lib/mpl_toolkits/mplot3d/proj3d.py b/lib/mpl_toolkits/mplot3d/proj3d.py index 1fcbafbbcdbc..c79c8eeba899 100644 --- a/lib/mpl_toolkits/mplot3d/proj3d.py +++ b/lib/mpl_toolkits/mplot3d/proj3d.py @@ -171,20 +171,33 @@ def _ortho_transformation(zfront, zback): def _proj_transform_vec(vec, M): - vecw = np.dot(M, vec) + vecw = np.dot(M, vec.data) w = vecw[3] txs, tys, tzs = vecw[0]/w, vecw[1]/w, vecw[2]/w + if np.ma.isMA(vec[0]): # we check each to protect for scalars + txs = np.ma.array(txs, mask=vec[0].mask) + if np.ma.isMA(vec[1]): + tys = np.ma.array(tys, mask=vec[1].mask) + if np.ma.isMA(vec[2]): + tzs = np.ma.array(tzs, mask=vec[2].mask) return txs, tys, tzs def _proj_transform_vec_clip(vec, M, focal_length): - vecw = np.dot(M, vec) + vecw = np.dot(M, vec.data) w = vecw[3] txs, tys, tzs = vecw[0] / w, vecw[1] / w, vecw[2] / w if np.isinf(focal_length): # don't clip orthographic projection tis = np.ones(txs.shape, dtype=bool) else: tis = (-1 <= txs) & (txs <= 1) & (-1 <= tys) & (tys <= 1) & (tzs <= 0) + if np.ma.isMA(vec[0]): + tis = tis & ~vec[0].mask + if np.ma.isMA(vec[1]): + tis = tis & ~vec[1].mask + if np.ma.isMA(vec[2]): + tis = tis & ~vec[2].mask + txs = np.ma.masked_array(txs, ~tis) tys = np.ma.masked_array(tys, ~tis) tzs = np.ma.masked_array(tzs, ~tis) @@ -206,7 +219,10 @@ def inv_transform(xs, ys, zs, invM): def _vec_pad_ones(xs, ys, zs): - return np.array([xs, ys, zs, np.ones_like(xs)]) + if np.ma.isMA(xs) or np.ma.isMA(ys) or np.ma.isMA(zs): + return np.ma.array([xs, ys, zs, np.ones_like(xs)]) + else: + return np.array([xs, ys, zs, np.ones_like(xs)]) def proj_transform(xs, ys, zs, M): @@ -252,7 +268,8 @@ def proj_trans_points(points, M): def _proj_trans_points(points, M): - xs, ys, zs = zip(*points) + points = np.asanyarray(points) + xs, ys, zs = points[:, 0], points[:, 1], points[:, 2] return proj_transform(xs, ys, zs, M) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index c64e888fdc2e..2212b8fe4f95 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -1380,6 +1380,45 @@ def test_axes3d_isometric(): ax.grid(True) +@check_figures_equal(extensions=["png"]) +def test_axlim_clip(fig_test, fig_ref): + # With axlim clipping + ax = fig_test.add_subplot(projection="3d") + x = np.linspace(0, 1, 11) + y = np.linspace(0, 1, 11) + X, Y = np.meshgrid(x, y) + Z = X + Y + ax.plot_surface(X, Y, Z, facecolor='C1', edgecolors=None, + rcount=50, ccount=50, axlim_clip=True) + # This ax.plot is to cover the extra surface edge which is not clipped out + ax.plot([0.5, 0.5], [0, 1], [0.5, 1.5], + color='k', linewidth=3, zorder=5, axlim_clip=True) + ax.scatter(X.ravel(), Y.ravel(), Z.ravel() + 1, axlim_clip=True) + ax.quiver(X.ravel(), Y.ravel(), Z.ravel() + 2, + 0*X.ravel(), 0*Y.ravel(), 0*Z.ravel() + 1, + arrow_length_ratio=0, axlim_clip=True) + ax.plot(X[0], Y[0], Z[0] + 3, color='C2', axlim_clip=True) + ax.text(1.1, 0.5, 4, 'test', axlim_clip=True) # won't be visible + ax.set(xlim=(0, 0.5), ylim=(0, 1), zlim=(0, 5)) + + # With manual clipping + ax = fig_ref.add_subplot(projection="3d") + idx = (X <= 0.5) + X = X[idx].reshape(11, 6) + Y = Y[idx].reshape(11, 6) + Z = Z[idx].reshape(11, 6) + ax.plot_surface(X, Y, Z, facecolor='C1', edgecolors=None, + rcount=50, ccount=50, axlim_clip=False) + ax.plot([0.5, 0.5], [0, 1], [0.5, 1.5], + color='k', linewidth=3, zorder=5, axlim_clip=False) + ax.scatter(X.ravel(), Y.ravel(), Z.ravel() + 1, axlim_clip=False) + ax.quiver(X.ravel(), Y.ravel(), Z.ravel() + 2, + 0*X.ravel(), 0*Y.ravel(), 0*Z.ravel() + 1, + arrow_length_ratio=0, axlim_clip=False) + ax.plot(X[0], Y[0], Z[0] + 3, color='C2', axlim_clip=False) + ax.set(xlim=(0, 0.5), ylim=(0, 1), zlim=(0, 5)) + + @pytest.mark.parametrize('value', [np.inf, np.nan]) @pytest.mark.parametrize(('setter', 'side'), [ ('set_xlim3d', 'left'), @@ -2138,10 +2177,10 @@ def test_computed_zorder(): ax.add_collection3d(tri) # plot a vector - ax.plot((2, 2), (2, 2), (0, 4), c='red', zorder=2) + ax.plot((2, 2), (2, 2), (0, 4), c='red', zorder=2, axlim_clip=False) # plot some points - ax.scatter((3, 3), (1, 3), (1, 3), c='red', zorder=10) + ax.scatter((3, 3), (1, 3), (1, 3), c='red', zorder=10, axlim_clip=False) ax.set_xlim((0, 5.0)) ax.set_ylim((0, 5.0)) From d31c0d155ec6ea7848dd9722fdecccf8d9704bdf Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Wed, 24 Jul 2024 15:23:54 -0600 Subject: [PATCH 0212/1230] Extend 3D axlim_clip to fill_between --- lib/mpl_toolkits/mplot3d/axes3d.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 94dcdd74974a..b95b0c24aea6 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -1963,7 +1963,7 @@ def plot(self, xs, ys, *args, zdir='z', axlim_clip=False, **kwargs): def fill_between(self, x1, y1, z1, x2, y2, z2, *, where=None, mode='auto', facecolors=None, shade=None, - **kwargs): + axlim_clip=False, **kwargs): """ Fill the area between two 3D curves. @@ -2009,6 +2009,9 @@ def fill_between(self, x1, y1, z1, x2, y2, z2, *, Whether to shade the facecolors. If *None*, then defaults to *True* for 'quad' mode and *False* for 'polygon' mode. + axlim_clip : bool, default: False + Whether to hide data that is outside the axes view limits. + **kwargs All other keyword arguments are passed on to `.Poly3DCollection`. @@ -2079,7 +2082,7 @@ def fill_between(self, x1, y1, z1, x2, y2, z2, *, polys.append(poly) polyc = art3d.Poly3DCollection(polys, facecolors=facecolors, shade=shade, - **kwargs) + axlim_clip=axlim_clip, **kwargs) self.add_collection(polyc) self.auto_scale_xyz([x1, x2], [y1, y2], [z1, z2], had_data) From 01fada922f8baab725ab733d2f4ae2512af14473 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Sat, 10 Aug 2024 14:57:32 -0600 Subject: [PATCH 0213/1230] Code review comments on 3D axlim clipping --- lib/matplotlib/collections.py | 2 +- lib/mpl_toolkits/mplot3d/art3d.py | 7 +++- lib/mpl_toolkits/mplot3d/axes3d.py | 38 +++++++++---------- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 4 +- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index db6fc5b37293..00146cec3cb0 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -339,7 +339,7 @@ def _prepare_points(self): # This might have changed an ndarray into a masked array. offset_trf = offset_trf.get_affine() - if np.ma.isMaskedArray(offsets): + if isinstance(offsets, np.ma.MaskedArray): offsets = offsets.filled(np.nan) # Changing from a masked array to nan-filled ndarray # is probably most efficient at this point. diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 0e1d6205d23c..74106cfdf91b 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -452,8 +452,11 @@ def do_3d_projection(self): """ segments = self._segments3d if self._axlim_clip: - segments = [np.ma.column_stack([*_viewlim_mask(*zip(*points), self.axes)]) - for points in segments] + all_points = np.ma.vstack(segments) + masked_points = np.ma.column_stack([*_viewlim_mask(*all_points.T, + self.axes)]) + segment_lengths = [segment.shape[0] for segment in segments] + segments = np.split(masked_points, np.cumsum(segment_lengths[:-1])) xyslist = [proj3d._proj_trans_points(points, self.axes.M) for points in segments] segments_2d = [np.ma.column_stack([xs, ys]) for xs, ys, zs in xyslist] diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index b95b0c24aea6..33b4f0ab9d39 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -2593,10 +2593,10 @@ def contour(self, X, Y, Z, *args, offset : float, optional If specified, plot a projection of the contour lines at this position in a plane normal to *zdir*. - data : indexable object, optional - DATA_PARAMETER_PLACEHOLDER axlim_clip : bool, default: False Whether to hide lines with a vertex outside the axes view limits. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER *args, **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.contour`. @@ -2640,10 +2640,10 @@ def tricontour(self, *args, offset : float, optional If specified, plot a projection of the contour lines at this position in a plane normal to *zdir*. - data : indexable object, optional - DATA_PARAMETER_PLACEHOLDER axlim_clip : bool, default: False Whether to hide lines with a vertex outside the axes view limits. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER *args, **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.tricontour`. @@ -2697,10 +2697,10 @@ def contourf(self, X, Y, Z, *args, offset : float, optional If specified, plot a projection of the contour lines at this position in a plane normal to *zdir*. - data : indexable object, optional - DATA_PARAMETER_PLACEHOLDER axlim_clip : bool, default: False Whether to hide lines with a vertex outside the axes view limits. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER *args, **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.contourf`. @@ -2737,10 +2737,10 @@ def tricontourf(self, *args, zdir='z', offset=None, axlim_clip=False, **kwargs): offset : float, optional If specified, plot a projection of the contour lines at this position in a plane normal to zdir. - data : indexable object, optional - DATA_PARAMETER_PLACEHOLDER axlim_clip : bool, default: False Whether to hide lines with a vertex outside the axes view limits. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER *args, **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.tricontourf`. @@ -2935,10 +2935,10 @@ def bar(self, left, height, zs=0, zdir='z', *args, used for all bars. zdir : {'x', 'y', 'z'}, default: 'z' When plotting 2D data, the direction to use as z ('x', 'y' or 'z'). - data : indexable object, optional - DATA_PARAMETER_PLACEHOLDER axlim_clip : bool, default: False Whether to hide bars with points outside the axes view limits. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER **kwargs Other keyword arguments are forwarded to `matplotlib.axes.Axes.bar`. @@ -3026,12 +3026,12 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None, lightsource : `~matplotlib.colors.LightSource`, optional The lightsource to use when *shade* is True. - data : indexable object, optional - DATA_PARAMETER_PLACEHOLDER - axlim_clip : bool, default: False Whether to hide the bars with points outside the axes view limits. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Any additional keyword arguments are passed onto `~.art3d.Poly3DCollection`. @@ -3184,12 +3184,12 @@ def quiver(self, X, Y, Z, U, V, W, *, Whether all arrows are normalized to have the same length, or keep the lengths defined by *u*, *v*, and *w*. - data : indexable object, optional - DATA_PARAMETER_PLACEHOLDER - axlim_clip : bool, default: False Whether to hide arrows with points outside the axes view limits. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Any additional keyword arguments are delegated to :class:`.Line3DCollection` @@ -3890,12 +3890,12 @@ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', orientation : {'x', 'y', 'z'}, default: 'z' The direction along which stems are drawn. - data : indexable object, optional - DATA_PARAMETER_PLACEHOLDER - axlim_clip : bool, default: False Whether to hide stems that are outside the axes limits. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + Returns ------- `.StemContainer` diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 2212b8fe4f95..295548591b18 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -2177,10 +2177,10 @@ def test_computed_zorder(): ax.add_collection3d(tri) # plot a vector - ax.plot((2, 2), (2, 2), (0, 4), c='red', zorder=2, axlim_clip=False) + ax.plot((2, 2), (2, 2), (0, 4), c='red', zorder=2) # plot some points - ax.scatter((3, 3), (1, 3), (1, 3), c='red', zorder=10, axlim_clip=False) + ax.scatter((3, 3), (1, 3), (1, 3), c='red', zorder=10) ax.set_xlim((0, 5.0)) ax.set_ylim((0, 5.0)) From cc5e8d5ea8960f39e10f2543d5e9b20c8e8907a9 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh <14363975+scottshambaugh@users.noreply.github.com> Date: Sat, 24 Aug 2024 04:00:19 +0000 Subject: [PATCH 0214/1230] Fix docs Implement dynamic clipping to axes box for 3D plots Make axlim_clip flag keyword only Updates test image test image restore Implement dynamic clipping to axes box for 3D plots Make axlim_clip flag keyword only Updates test image test image restore Implement dynamic clipping to axes box for 3D plots Make axlim_clip flag keyword only Updates test image test image restore Code review comments on 3D axlim clipping Code review comments on 3D axlim clipping --- doc/missing-references.json | 2 +- .../next_whats_new/3d_clip_to_axis_limits.rst | 8 ++--- galleries/users_explain/toolkits/mplot3d.rst | 2 ++ lib/mpl_toolkits/mplot3d/axes3d.py | 36 +++++++++++++++++++ 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/doc/missing-references.json b/doc/missing-references.json index a93a03b6ef73..2e4b482d845e 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -325,7 +325,7 @@ "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:212", "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Quiver:251", "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Path3DCollection.set:46", - "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Poly3DCollection.set:44" + "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Poly3DCollection.set:45" ], "matplotlib.collections._MeshData.set_array": [ "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolormesh:164", diff --git a/doc/users/next_whats_new/3d_clip_to_axis_limits.rst b/doc/users/next_whats_new/3d_clip_to_axis_limits.rst index b60927bcd0b5..d97ba1b675ba 100644 --- a/doc/users/next_whats_new/3d_clip_to_axis_limits.rst +++ b/doc/users/next_whats_new/3d_clip_to_axis_limits.rst @@ -6,10 +6,10 @@ will clip the data to the axes view limits, hiding all data outside those bounds. This clipping will be dynamically applied in real time while panning and zooming. -Please note that if one vertex of a line segment or 3D patch is clipped, the -entire segment or patch will be hidden. Not being able to show partial lines -or patches such that they are "smoothly" cut off at the boundaries of the view -box is a limitation of the current renderer. +Please note that if one vertex of a line segment or 3D patch is clipped, then +the entire segment or patch will be hidden. Not being able to show partial +lines or patches such that they are "smoothly" cut off at the boundaries of the +view box is a limitation of the current renderer. .. plot:: :include-source: true diff --git a/galleries/users_explain/toolkits/mplot3d.rst b/galleries/users_explain/toolkits/mplot3d.rst index 100449f23a0e..b4ddc48790cb 100644 --- a/galleries/users_explain/toolkits/mplot3d.rst +++ b/galleries/users_explain/toolkits/mplot3d.rst @@ -121,6 +121,8 @@ See `.Axes3D.fill_between` for API documentation. :target: /gallery/mplot3d/fillbetween3d.html :align: center +.. versionadded:: 3.10 + .. _polygon3d: Polygon plots diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 33b4f0ab9d39..90944d19a692 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -1902,6 +1902,8 @@ def text(self, x, y, z, s, zdir=None, *, axlim_clip=False, **kwargs): See `.get_dir_vector` for a description of the values. axlim_clip : bool, default: False Whether to hide text that is outside the axes view limits. + + .. versionadded:: 3.10 **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.text`. @@ -1934,6 +1936,8 @@ def plot(self, xs, ys, *args, zdir='z', axlim_clip=False, **kwargs): When plotting 2D data, the direction to use as z. axlim_clip : bool, default: False Whether to hide data that is outside the axes view limits. + + .. versionadded:: 3.10 **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.plot`. """ @@ -2012,6 +2016,8 @@ def fill_between(self, x1, y1, z1, x2, y2, z2, *, axlim_clip : bool, default: False Whether to hide data that is outside the axes view limits. + .. versionadded:: 3.10 + **kwargs All other keyword arguments are passed on to `.Poly3DCollection`. @@ -2157,6 +2163,8 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, axlim_clip : bool, default: False Whether to hide patches with a vertex outside the axes view limits. + .. versionadded:: 3.10 + **kwargs Other keyword arguments are forwarded to `.Poly3DCollection`. """ @@ -2305,6 +2313,8 @@ def plot_wireframe(self, X, Y, Z, *, axlim_clip=False, **kwargs): Whether to hide lines and patches with vertices outside the axes view limits. + .. versionadded:: 3.10 + rcount, ccount : int Maximum number of samples used in each direction. If the input data is larger, it will be downsampled (by slicing) to these @@ -2452,6 +2462,8 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, The lightsource to use when *shade* is True. axlim_clip : bool, default: False Whether to hide patches with a vertex outside the axes view limits. + + .. versionadded:: 3.10 **kwargs All other keyword arguments are passed on to :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` @@ -2595,6 +2607,8 @@ def contour(self, X, Y, Z, *args, position in a plane normal to *zdir*. axlim_clip : bool, default: False Whether to hide lines with a vertex outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER @@ -2642,6 +2656,8 @@ def tricontour(self, *args, position in a plane normal to *zdir*. axlim_clip : bool, default: False Whether to hide lines with a vertex outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER *args, **kwargs @@ -2699,6 +2715,8 @@ def contourf(self, X, Y, Z, *args, position in a plane normal to *zdir*. axlim_clip : bool, default: False Whether to hide lines with a vertex outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER *args, **kwargs @@ -2739,6 +2757,8 @@ def tricontourf(self, *args, zdir='z', offset=None, axlim_clip=False, **kwargs): position in a plane normal to zdir. axlim_clip : bool, default: False Whether to hide lines with a vertex outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER *args, **kwargs @@ -2797,6 +2817,8 @@ def add_collection3d(self, col, zs=0, zdir='z', autolim=True, *, Whether to update the data limits. axlim_clip : bool, default: False Whether to hide the scatter points outside the axes view limits. + + .. versionadded:: 3.10 """ had_data = self.has_data() @@ -2878,6 +2900,8 @@ def scatter(self, xs, ys, independently. axlim_clip : bool, default: False Whether to hide the scatter points outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER **kwargs @@ -2937,6 +2961,8 @@ def bar(self, left, height, zs=0, zdir='z', *args, When plotting 2D data, the direction to use as z ('x', 'y' or 'z'). axlim_clip : bool, default: False Whether to hide bars with points outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER **kwargs @@ -3029,6 +3055,8 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None, axlim_clip : bool, default: False Whether to hide the bars with points outside the axes view limits. + .. versionadded:: 3.10 + data : indexable object, optional DATA_PARAMETER_PLACEHOLDER @@ -3187,6 +3215,8 @@ def quiver(self, X, Y, Z, U, V, W, *, axlim_clip : bool, default: False Whether to hide arrows with points outside the axes view limits. + .. versionadded:: 3.10 + data : indexable object, optional DATA_PARAMETER_PLACEHOLDER @@ -3328,6 +3358,8 @@ def voxels(self, *args, facecolors=None, edgecolors=None, shade=True, axlim_clip : bool, default: False Whether to hide voxels with points outside the axes view limits. + .. versionadded:: 3.10 + **kwargs Additional keyword arguments to pass onto `~mpl_toolkits.mplot3d.art3d.Poly3DCollection`. @@ -3576,6 +3608,8 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', axlim_clip : bool, default: False Whether to hide error bars that are outside the axes limits. + .. versionadded:: 3.10 + Returns ------- errlines : list @@ -3893,6 +3927,8 @@ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', axlim_clip : bool, default: False Whether to hide stems that are outside the axes limits. + .. versionadded:: 3.10 + data : indexable object, optional DATA_PARAMETER_PLACEHOLDER From ef32d223eac1b603ad5e9e328efcc774b81f8745 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Tue, 3 Sep 2024 15:00:24 -0700 Subject: [PATCH 0215/1230] DOC/TST: lock numpy < 2.1 (#28779) * DOC/TST: lock numpy < 2.1 --- .circleci/config.yml | 2 +- requirements/dev/build-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2fa296746efb..e7348b868d4b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -98,7 +98,7 @@ commands: parameters: numpy_version: type: string - default: "!=2.1.0" + default: "~=2.0.0" steps: - run: name: Install Python dependencies diff --git a/requirements/dev/build-requirements.txt b/requirements/dev/build-requirements.txt index 6f0c6029f4a2..0861a11c9ee5 100644 --- a/requirements/dev/build-requirements.txt +++ b/requirements/dev/build-requirements.txt @@ -1,4 +1,4 @@ pybind11!=2.13.3 meson-python -numpy +numpy<2.1.0 setuptools-scm From 0729ddb97882549cd16b70ecf5cb664d294970df Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 5 Sep 2024 17:20:15 +0200 Subject: [PATCH 0216/1230] Fix places where "auto" was not listed as valid interpolation_stage. --- lib/matplotlib/backends/qt_editor/figureoptions.py | 2 +- lib/matplotlib/image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index c36bbeb62641..529f45829999 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -165,7 +165,7 @@ def prepare_data(d, init): 'Interpolation', [mappable.get_interpolation(), *interpolations])) - interpolation_stages = ['data', 'rgba'] + interpolation_stages = ['data', 'rgba', 'auto'] mappabledata.append(( 'Interpolation stage', [mappable.get_interpolation_stage(), *interpolation_stages])) diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 95994201b94e..0a3782f99309 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -722,7 +722,7 @@ def get_interpolation_stage(self): """ Return when interpolation happens during the transform to RGBA. - One of 'data', 'rgba'. + One of 'data', 'rgba', 'auto'. """ return self._interpolation_stage From 16fad2e00644fbe2a9e751777a7ffbea641440c8 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 4 Sep 2024 22:20:25 -0400 Subject: [PATCH 0217/1230] TST: Fix test_pickle_load_from_subprocess in a dirty tree For a ditry tree, `setuptools-scm` adds the date to the end of the version. This test runs a subprocess and `subprocess_run_helper` will set `SOURCE_DATE_EPOCH=0`, which forces the date to be 1970-01-01, triggering a mismatched version warning on unpickle. Since we aren't testing the version-compatibility warning, set `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MATPLOTLIB` in the subprocess call so that versions match. Also, set the `dist_name` parameter when querying setuptools-scm or it won't check that environment variable. --- lib/matplotlib/__init__.py | 1 + lib/matplotlib/tests/test_pickle.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 13bfa81d9ffa..b20af9108bd0 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -225,6 +225,7 @@ def _get_version(): else: return setuptools_scm.get_version( root=root, + dist_name="matplotlib", version_scheme="release-branch-semver", local_scheme="node-and-date", fallback_version=_version.version, diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py index 1474a67d28aa..8bb7ef9deb54 100644 --- a/lib/matplotlib/tests/test_pickle.py +++ b/lib/matplotlib/tests/test_pickle.py @@ -150,7 +150,15 @@ def test_pickle_load_from_subprocess(fig_test, fig_ref, tmp_path): proc = subprocess_run_helper( _pickle_load_subprocess, timeout=60, - extra_env={'PICKLE_FILE_PATH': str(fp), 'MPLBACKEND': 'Agg'} + extra_env={ + "PICKLE_FILE_PATH": str(fp), + "MPLBACKEND": "Agg", + # subprocess_run_helper will set SOURCE_DATE_EPOCH=0, so for a dirty tree, + # the version will have the date 19700101. As we aren't trying to test the + # version compatibility warning, force setuptools-scm to use the same + # version as us. + "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MATPLOTLIB": mpl.__version__, + }, ) loaded_fig = pickle.loads(ast.literal_eval(proc.stdout)) From e776ce788f45ab85c54b2583403c406f2823f4a0 Mon Sep 17 00:00:00 2001 From: Gavin S Date: Sun, 11 Aug 2024 22:24:35 -0700 Subject: [PATCH 0218/1230] Add Returns documentation to `to_jshtml` function --- lib/matplotlib/animation.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 00b16d240740..67a0198f8f51 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -1311,6 +1311,12 @@ def to_jshtml(self, fps=None, embed_frames=True, default_mode=None): What to do when the animation ends. Must be one of ``{'loop', 'once', 'reflect'}``. Defaults to ``'loop'`` if the *repeat* parameter is True, otherwise ``'once'``. + + Returns + ------- + str + An HTML representation of the animation embedded as a js object as + produced with the ``HTMLWriter``. """ if fps is None and hasattr(self, '_interval'): # Convert interval in ms to frames per second From 07731096d34e5a939d674b6118bcaab7c7568d21 Mon Sep 17 00:00:00 2001 From: Gavin S Date: Sun, 11 Aug 2024 22:30:49 -0700 Subject: [PATCH 0219/1230] Fix return to match docstring standards --- lib/matplotlib/animation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 67a0198f8f51..515a9eaa15ad 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -1316,7 +1316,7 @@ def to_jshtml(self, fps=None, embed_frames=True, default_mode=None): ------- str An HTML representation of the animation embedded as a js object as - produced with the ``HTMLWriter``. + produced with the *HTMLWriter*. """ if fps is None and hasattr(self, '_interval'): # Convert interval in ms to frames per second From 0946afe6ae2d4f9fe687b95355da15d7a71f43ad Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:03:21 +0100 Subject: [PATCH 0220/1230] Link to HTMLWriter docs Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/animation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 515a9eaa15ad..0f6811a3bab9 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -1316,7 +1316,7 @@ def to_jshtml(self, fps=None, embed_frames=True, default_mode=None): ------- str An HTML representation of the animation embedded as a js object as - produced with the *HTMLWriter*. + produced with the `.HTMLWriter`. """ if fps is None and hasattr(self, '_interval'): # Convert interval in ms to frames per second From 4439116246b1f74e6dda7a11d96602c3912c01b9 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 7 Sep 2024 14:33:56 +0100 Subject: [PATCH 0221/1230] Backport PR #28706: Add Returns info to to_jshtml docstring --- lib/matplotlib/animation.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 5a4764f1a79f..b402c5fdb4da 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -1311,6 +1311,12 @@ def to_jshtml(self, fps=None, embed_frames=True, default_mode=None): What to do when the animation ends. Must be one of ``{'loop', 'once', 'reflect'}``. Defaults to ``'loop'`` if the *repeat* parameter is True, otherwise ``'once'``. + + Returns + ------- + str + An HTML representation of the animation embedded as a js object as + produced with the `.HTMLWriter`. """ if fps is None and hasattr(self, '_interval'): # Convert interval in ms to frames per second From a809c6488b1b2b6551e68509b48859b3e974c370 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sat, 7 Sep 2024 11:03:12 -0400 Subject: [PATCH 0222/1230] TST: Skip webp tests if it isn't available --- lib/matplotlib/tests/test_agg.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_agg.py b/lib/matplotlib/tests/test_agg.py index 6ca74ed400b1..3012c6665413 100644 --- a/lib/matplotlib/tests/test_agg.py +++ b/lib/matplotlib/tests/test_agg.py @@ -2,7 +2,7 @@ import numpy as np from numpy.testing import assert_array_almost_equal -from PIL import Image, TiffTags +from PIL import features, Image, TiffTags import pytest @@ -249,6 +249,7 @@ def test_pil_kwargs_tiff(): assert tags["ImageDescription"] == "test image" +@pytest.mark.skipif(not features.check("webp"), reason="WebP support not available") def test_pil_kwargs_webp(): plt.plot([0, 1, 2], [0, 1, 0]) buf_small = io.BytesIO() @@ -262,6 +263,7 @@ def test_pil_kwargs_webp(): assert buf_large.getbuffer().nbytes > buf_small.getbuffer().nbytes +@pytest.mark.skipif(not features.check("webp"), reason="WebP support not available") def test_webp_alpha(): plt.plot([0, 1, 2], [0, 1, 0]) buf = io.BytesIO() From f6b8a1b8c11d3c105a05a7ce796b0c775186126d Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Sat, 7 Sep 2024 10:16:42 -0500 Subject: [PATCH 0223/1230] numticks kwonly, doc removal of set_label --- doc/api/next_api_changes/removals/28183-OG.rst | 8 ++++++-- lib/matplotlib/ticker.py | 4 ++-- lib/matplotlib/ticker.pyi | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/doc/api/next_api_changes/removals/28183-OG.rst b/doc/api/next_api_changes/removals/28183-OG.rst index 9511a33b5519..55745e47809a 100644 --- a/doc/api/next_api_changes/removals/28183-OG.rst +++ b/doc/api/next_api_changes/removals/28183-OG.rst @@ -1,5 +1,5 @@ -``Tick.set_label1`` and ``Tick.set_label2`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``Tick.set_label``, ``Tick.set_label1`` and ``Tick.set_label2`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ... are removed. Calling these methods from third-party code usually had no effect, as the labels are overwritten at draw time by the tick formatter. @@ -52,6 +52,10 @@ consistently with ``_ImageBase.set_filterrad``. The only parameter of ``Annotation.contains`` and ``Legend.contains`` is renamed to *mouseevent* consistently with `.Artist.contains`. +Method parameters renamed +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *p* parameter of ``BboxBase.padded`` is renamed to *w_pad*, consistently with the other parameter, *h_pad* *numdecs* parameter and attribute of ``LogLocator`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 940cacc63fb9..51c44b3a958a 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2275,7 +2275,7 @@ class LogLocator(Locator): Places ticks at the values ``subs[j] * base**i``. """ - def __init__(self, base=10.0, subs=(1.0,), numticks=None): + def __init__(self, base=10.0, subs=(1.0,), *, numticks=None): """ Parameters ---------- @@ -2306,7 +2306,7 @@ def __init__(self, base=10.0, subs=(1.0,), numticks=None): self._set_subs(subs) self.numticks = numticks - def set_params(self, base=None, subs=None, numticks=None): + def set_params(self, base=None, subs=None, *, numticks=None): """Set parameters within this locator.""" if base is not None: self._base = float(base) diff --git a/lib/matplotlib/ticker.pyi b/lib/matplotlib/ticker.pyi index 4ecc6054feb9..fd8e41848671 100644 --- a/lib/matplotlib/ticker.pyi +++ b/lib/matplotlib/ticker.pyi @@ -236,12 +236,14 @@ class LogLocator(Locator): self, base: float = ..., subs: None | Literal["auto", "all"] | Sequence[float] = ..., + *, numticks: int | None = ..., ) -> None: ... def set_params( self, base: float | None = ..., subs: Literal["auto", "all"] | Sequence[float] | None = ..., + *, numticks: int | None = ..., ) -> None: ... From 1ef218fbdf532e7a9607058f54f56298b7549600 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 8 Sep 2024 08:21:30 +0200 Subject: [PATCH 0224/1230] DOC: Fix duplicate Figure.set_dpi entry --- doc/api/figure_api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/figure_api.rst b/doc/api/figure_api.rst index 2371e5a9a863..5dd3adbfec9f 100644 --- a/doc/api/figure_api.rst +++ b/doc/api/figure_api.rst @@ -91,7 +91,7 @@ Figure geometry Figure.get_figwidth Figure.dpi Figure.set_dpi - Figure.set_dpi + Figure.get_dpi Subplot layout -------------- From 7c370eac988c51dea9d6fca285636ebfcdeba55c Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sun, 8 Sep 2024 08:44:57 +0100 Subject: [PATCH 0225/1230] Backport PR #28790: DOC: Fix duplicate Figure.set_dpi entry --- doc/api/figure_api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/figure_api.rst b/doc/api/figure_api.rst index 2371e5a9a863..5dd3adbfec9f 100644 --- a/doc/api/figure_api.rst +++ b/doc/api/figure_api.rst @@ -91,7 +91,7 @@ Figure geometry Figure.get_figwidth Figure.dpi Figure.set_dpi - Figure.set_dpi + Figure.get_dpi Subplot layout -------------- From cf7e7ae718ced0a8b2bbf7aa2d30b141fa5eeb8c Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sun, 8 Sep 2024 08:44:57 +0100 Subject: [PATCH 0226/1230] Backport PR #28790: DOC: Fix duplicate Figure.set_dpi entry --- doc/api/figure_api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/figure_api.rst b/doc/api/figure_api.rst index 2371e5a9a863..5dd3adbfec9f 100644 --- a/doc/api/figure_api.rst +++ b/doc/api/figure_api.rst @@ -91,7 +91,7 @@ Figure geometry Figure.get_figwidth Figure.dpi Figure.set_dpi - Figure.set_dpi + Figure.get_dpi Subplot layout -------------- From 9159096146c13f5bd9b612a4e6b141b2cc17b096 Mon Sep 17 00:00:00 2001 From: Ammar Qazi Date: Mon, 9 Sep 2024 18:31:06 +0200 Subject: [PATCH 0227/1230] Add example of petroff10 --- galleries/examples/style_sheets/petroff10.py | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 galleries/examples/style_sheets/petroff10.py diff --git a/galleries/examples/style_sheets/petroff10.py b/galleries/examples/style_sheets/petroff10.py new file mode 100644 index 000000000000..f6293fd40a6b --- /dev/null +++ b/galleries/examples/style_sheets/petroff10.py @@ -0,0 +1,43 @@ +""" +===================== +Petroff10 style sheet +===================== + +This example demonstrates the "petroff10" style, which implements the 10-color +sequence developed by Matthew A. Petroff [1]_ for accessible data visualization. +The style balances aesthetics with accessibility considerations, making it +suitable for various types of plots while ensuring readability and distinction +between data series. + +.. [1] https://arxiv.org/abs/2107.02270 + +""" + +import matplotlib.pyplot as plt +import numpy as np + + +def colored_lines_example(ax): + t = np.linspace(-10, 10, 100) + nb_colors = len(plt.rcParams['axes.prop_cycle']) + shifts = np.linspace(-5, 5, nb_colors) + amplitudes = np.linspace(1, 1.5, nb_colors) + for t0, a in zip(shifts, amplitudes): + y = a / (1 + np.exp(-(t - t0))) + line, = ax.plot(t, y, '-') + point_indices = np.linspace(0, len(t) - 1, 20, dtype=int) + ax.plot(t[point_indices], y[point_indices], 'o', color=line.get_color()) + ax.set_xlim(-10, 10) + + +def image_and_patch_example(ax): + ax.imshow(np.random.random(size=(20, 20)), interpolation='none') + c = plt.Circle((5, 5), radius=5, label='patch') + ax.add_patch(c) + +plt.style.use('petroff10') +fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(12, 5)) +fig.suptitle("'petroff10' style sheet") +colored_lines_example(ax1) +image_and_patch_example(ax2) +plt.show() From 3a9e161714ce94bd8c51567c5839b21a3fd802bc Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:10:26 +0200 Subject: [PATCH 0228/1230] DOC: Correctly list modules that have been internalized Making them internal-only happened in 3.8 (https://matplotlib.org/3.9.2/api/prev_api_changes/api_changes_3.8.0.html#deprecated-modules-removed) --- doc/api/_afm_api.rst | 8 ++++++++ doc/api/_docstring_api.rst | 8 ++++++++ doc/api/_tight_bbox_api.rst | 8 ++++++++ doc/api/_tight_layout_api.rst | 8 ++++++++ doc/api/_type1font.rst | 8 ++++++++ doc/api/afm_api.rst | 13 ------------- doc/api/docstring_api.rst | 13 ------------- doc/api/index.rst | 10 +++++----- doc/api/tight_bbox_api.rst | 13 ------------- doc/api/tight_layout_api.rst | 13 ------------- doc/api/type1font.rst | 13 ------------- 11 files changed, 45 insertions(+), 70 deletions(-) create mode 100644 doc/api/_afm_api.rst create mode 100644 doc/api/_docstring_api.rst create mode 100644 doc/api/_tight_bbox_api.rst create mode 100644 doc/api/_tight_layout_api.rst create mode 100644 doc/api/_type1font.rst delete mode 100644 doc/api/afm_api.rst delete mode 100644 doc/api/docstring_api.rst delete mode 100644 doc/api/tight_bbox_api.rst delete mode 100644 doc/api/tight_layout_api.rst delete mode 100644 doc/api/type1font.rst diff --git a/doc/api/_afm_api.rst b/doc/api/_afm_api.rst new file mode 100644 index 000000000000..4e2ac4997272 --- /dev/null +++ b/doc/api/_afm_api.rst @@ -0,0 +1,8 @@ +******************* +``matplotlib._afm`` +******************* + +.. automodule:: matplotlib._afm + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/_docstring_api.rst b/doc/api/_docstring_api.rst new file mode 100644 index 000000000000..040a3653a87b --- /dev/null +++ b/doc/api/_docstring_api.rst @@ -0,0 +1,8 @@ +************************* +``matplotlib._docstring`` +************************* + +.. automodule:: matplotlib._docstring + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/_tight_bbox_api.rst b/doc/api/_tight_bbox_api.rst new file mode 100644 index 000000000000..826e051fcf6d --- /dev/null +++ b/doc/api/_tight_bbox_api.rst @@ -0,0 +1,8 @@ +************************** +``matplotlib._tight_bbox`` +************************** + +.. automodule:: matplotlib._tight_bbox + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/_tight_layout_api.rst b/doc/api/_tight_layout_api.rst new file mode 100644 index 000000000000..ac4f66f280e1 --- /dev/null +++ b/doc/api/_tight_layout_api.rst @@ -0,0 +1,8 @@ +**************************** +``matplotlib._tight_layout`` +**************************** + +.. automodule:: matplotlib._tight_layout + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/_type1font.rst b/doc/api/_type1font.rst new file mode 100644 index 000000000000..1a9ff2292887 --- /dev/null +++ b/doc/api/_type1font.rst @@ -0,0 +1,8 @@ +************************* +``matplotlib._type1font`` +************************* + +.. automodule:: matplotlib._type1font + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/afm_api.rst b/doc/api/afm_api.rst deleted file mode 100644 index bcae04150909..000000000000 --- a/doc/api/afm_api.rst +++ /dev/null @@ -1,13 +0,0 @@ -****************** -``matplotlib.afm`` -****************** - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._afm - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/docstring_api.rst b/doc/api/docstring_api.rst deleted file mode 100644 index 38a73a2e83d1..000000000000 --- a/doc/api/docstring_api.rst +++ /dev/null @@ -1,13 +0,0 @@ -************************ -``matplotlib.docstring`` -************************ - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._docstring - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/index.rst b/doc/api/index.rst index 70c3b5343e7a..53f397a6817a 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -79,7 +79,6 @@ Alphabetical list of modules: :maxdepth: 1 matplotlib_configuration_api.rst - afm_api.rst animation_api.rst artist_api.rst axes_api.rst @@ -98,7 +97,6 @@ Alphabetical list of modules: container_api.rst contour_api.rst dates_api.rst - docstring_api.rst dviread.rst figure_api.rst font_manager_api.rst @@ -134,16 +132,18 @@ Alphabetical list of modules: text_api.rst texmanager_api.rst ticker_api.rst - tight_bbox_api.rst - tight_layout_api.rst transformations.rst tri_api.rst - type1font.rst typing_api.rst units_api.rst widgets_api.rst + _afm_api.rst _api_api.rst + _docstring_api.rst _enums_api.rst + _type1font.rst + _tight_bbox_api.rst + _tight_layout_api.rst toolkits/mplot3d.rst toolkits/axes_grid1.rst toolkits/axisartist.rst diff --git a/doc/api/tight_bbox_api.rst b/doc/api/tight_bbox_api.rst deleted file mode 100644 index 9e8dd2fa66f9..000000000000 --- a/doc/api/tight_bbox_api.rst +++ /dev/null @@ -1,13 +0,0 @@ -************************* -``matplotlib.tight_bbox`` -************************* - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._tight_bbox - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/tight_layout_api.rst b/doc/api/tight_layout_api.rst deleted file mode 100644 index 35f92e3ddced..000000000000 --- a/doc/api/tight_layout_api.rst +++ /dev/null @@ -1,13 +0,0 @@ -*************************** -``matplotlib.tight_layout`` -*************************** - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._tight_layout - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/type1font.rst b/doc/api/type1font.rst deleted file mode 100644 index 00ef38f4d447..000000000000 --- a/doc/api/type1font.rst +++ /dev/null @@ -1,13 +0,0 @@ -************************ -``matplotlib.type1font`` -************************ - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._type1font - :members: - :undoc-members: - :show-inheritance: From ef229a496b61a1c5d3a6ccfc7c63bbe5381fb3e4 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:54:37 +0200 Subject: [PATCH 0229/1230] DOC: Clarify AxLine.set_xy2 / AxLine.set_slope --- lib/matplotlib/lines.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 5c17f3f01bec..acaf6328ac49 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -1568,6 +1568,12 @@ def set_xy2(self, x, y): """ Set the *xy2* value of the line. + .. note:: + + You can only set *xy2* if the line was created using the *xy2* + parameter. If the line was created using *slope*, please use + `~.AxLine.set_slope`. + Parameters ---------- x, y : float @@ -1583,6 +1589,12 @@ def set_slope(self, slope): """ Set the *slope* value of the line. + .. note:: + + You can only set *slope* if the line was created using the *slope* + parameter. If the line was created using *xy2*, please use + `~.AxLine.set_xy2`. + Parameters ---------- slope : float From 88309f528843195ec1e25d75ea5cf148a40bcd8d Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 11 Sep 2024 16:54:17 -0400 Subject: [PATCH 0230/1230] Backport PR #28798: DOC: Correctly list modules that have been internalized --- doc/api/_afm_api.rst | 8 ++++++++ doc/api/_docstring_api.rst | 8 ++++++++ doc/api/_tight_bbox_api.rst | 8 ++++++++ doc/api/_tight_layout_api.rst | 8 ++++++++ doc/api/_type1font.rst | 8 ++++++++ doc/api/afm_api.rst | 13 ------------- doc/api/docstring_api.rst | 13 ------------- doc/api/index.rst | 10 +++++----- doc/api/tight_bbox_api.rst | 13 ------------- doc/api/tight_layout_api.rst | 13 ------------- doc/api/type1font.rst | 13 ------------- 11 files changed, 45 insertions(+), 70 deletions(-) create mode 100644 doc/api/_afm_api.rst create mode 100644 doc/api/_docstring_api.rst create mode 100644 doc/api/_tight_bbox_api.rst create mode 100644 doc/api/_tight_layout_api.rst create mode 100644 doc/api/_type1font.rst delete mode 100644 doc/api/afm_api.rst delete mode 100644 doc/api/docstring_api.rst delete mode 100644 doc/api/tight_bbox_api.rst delete mode 100644 doc/api/tight_layout_api.rst delete mode 100644 doc/api/type1font.rst diff --git a/doc/api/_afm_api.rst b/doc/api/_afm_api.rst new file mode 100644 index 000000000000..4e2ac4997272 --- /dev/null +++ b/doc/api/_afm_api.rst @@ -0,0 +1,8 @@ +******************* +``matplotlib._afm`` +******************* + +.. automodule:: matplotlib._afm + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/_docstring_api.rst b/doc/api/_docstring_api.rst new file mode 100644 index 000000000000..040a3653a87b --- /dev/null +++ b/doc/api/_docstring_api.rst @@ -0,0 +1,8 @@ +************************* +``matplotlib._docstring`` +************************* + +.. automodule:: matplotlib._docstring + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/_tight_bbox_api.rst b/doc/api/_tight_bbox_api.rst new file mode 100644 index 000000000000..826e051fcf6d --- /dev/null +++ b/doc/api/_tight_bbox_api.rst @@ -0,0 +1,8 @@ +************************** +``matplotlib._tight_bbox`` +************************** + +.. automodule:: matplotlib._tight_bbox + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/_tight_layout_api.rst b/doc/api/_tight_layout_api.rst new file mode 100644 index 000000000000..ac4f66f280e1 --- /dev/null +++ b/doc/api/_tight_layout_api.rst @@ -0,0 +1,8 @@ +**************************** +``matplotlib._tight_layout`` +**************************** + +.. automodule:: matplotlib._tight_layout + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/_type1font.rst b/doc/api/_type1font.rst new file mode 100644 index 000000000000..1a9ff2292887 --- /dev/null +++ b/doc/api/_type1font.rst @@ -0,0 +1,8 @@ +************************* +``matplotlib._type1font`` +************************* + +.. automodule:: matplotlib._type1font + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/afm_api.rst b/doc/api/afm_api.rst deleted file mode 100644 index bcae04150909..000000000000 --- a/doc/api/afm_api.rst +++ /dev/null @@ -1,13 +0,0 @@ -****************** -``matplotlib.afm`` -****************** - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._afm - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/docstring_api.rst b/doc/api/docstring_api.rst deleted file mode 100644 index 38a73a2e83d1..000000000000 --- a/doc/api/docstring_api.rst +++ /dev/null @@ -1,13 +0,0 @@ -************************ -``matplotlib.docstring`` -************************ - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._docstring - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/index.rst b/doc/api/index.rst index 70c3b5343e7a..53f397a6817a 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -79,7 +79,6 @@ Alphabetical list of modules: :maxdepth: 1 matplotlib_configuration_api.rst - afm_api.rst animation_api.rst artist_api.rst axes_api.rst @@ -98,7 +97,6 @@ Alphabetical list of modules: container_api.rst contour_api.rst dates_api.rst - docstring_api.rst dviread.rst figure_api.rst font_manager_api.rst @@ -134,16 +132,18 @@ Alphabetical list of modules: text_api.rst texmanager_api.rst ticker_api.rst - tight_bbox_api.rst - tight_layout_api.rst transformations.rst tri_api.rst - type1font.rst typing_api.rst units_api.rst widgets_api.rst + _afm_api.rst _api_api.rst + _docstring_api.rst _enums_api.rst + _type1font.rst + _tight_bbox_api.rst + _tight_layout_api.rst toolkits/mplot3d.rst toolkits/axes_grid1.rst toolkits/axisartist.rst diff --git a/doc/api/tight_bbox_api.rst b/doc/api/tight_bbox_api.rst deleted file mode 100644 index 9e8dd2fa66f9..000000000000 --- a/doc/api/tight_bbox_api.rst +++ /dev/null @@ -1,13 +0,0 @@ -************************* -``matplotlib.tight_bbox`` -************************* - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._tight_bbox - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/tight_layout_api.rst b/doc/api/tight_layout_api.rst deleted file mode 100644 index 35f92e3ddced..000000000000 --- a/doc/api/tight_layout_api.rst +++ /dev/null @@ -1,13 +0,0 @@ -*************************** -``matplotlib.tight_layout`` -*************************** - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._tight_layout - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/type1font.rst b/doc/api/type1font.rst deleted file mode 100644 index 00ef38f4d447..000000000000 --- a/doc/api/type1font.rst +++ /dev/null @@ -1,13 +0,0 @@ -************************ -``matplotlib.type1font`` -************************ - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._type1font - :members: - :undoc-members: - :show-inheritance: From 259b3ee84784341b50696206d559f67c74ba9c89 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 11 Sep 2024 16:54:17 -0400 Subject: [PATCH 0231/1230] Backport PR #28798: DOC: Correctly list modules that have been internalized --- doc/api/_afm_api.rst | 8 ++++++++ doc/api/_docstring_api.rst | 8 ++++++++ doc/api/_tight_bbox_api.rst | 8 ++++++++ doc/api/_tight_layout_api.rst | 8 ++++++++ doc/api/_type1font.rst | 8 ++++++++ doc/api/afm_api.rst | 13 ------------- doc/api/docstring_api.rst | 13 ------------- doc/api/index.rst | 10 +++++----- doc/api/tight_bbox_api.rst | 13 ------------- doc/api/tight_layout_api.rst | 13 ------------- doc/api/type1font.rst | 13 ------------- 11 files changed, 45 insertions(+), 70 deletions(-) create mode 100644 doc/api/_afm_api.rst create mode 100644 doc/api/_docstring_api.rst create mode 100644 doc/api/_tight_bbox_api.rst create mode 100644 doc/api/_tight_layout_api.rst create mode 100644 doc/api/_type1font.rst delete mode 100644 doc/api/afm_api.rst delete mode 100644 doc/api/docstring_api.rst delete mode 100644 doc/api/tight_bbox_api.rst delete mode 100644 doc/api/tight_layout_api.rst delete mode 100644 doc/api/type1font.rst diff --git a/doc/api/_afm_api.rst b/doc/api/_afm_api.rst new file mode 100644 index 000000000000..4e2ac4997272 --- /dev/null +++ b/doc/api/_afm_api.rst @@ -0,0 +1,8 @@ +******************* +``matplotlib._afm`` +******************* + +.. automodule:: matplotlib._afm + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/_docstring_api.rst b/doc/api/_docstring_api.rst new file mode 100644 index 000000000000..040a3653a87b --- /dev/null +++ b/doc/api/_docstring_api.rst @@ -0,0 +1,8 @@ +************************* +``matplotlib._docstring`` +************************* + +.. automodule:: matplotlib._docstring + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/_tight_bbox_api.rst b/doc/api/_tight_bbox_api.rst new file mode 100644 index 000000000000..826e051fcf6d --- /dev/null +++ b/doc/api/_tight_bbox_api.rst @@ -0,0 +1,8 @@ +************************** +``matplotlib._tight_bbox`` +************************** + +.. automodule:: matplotlib._tight_bbox + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/_tight_layout_api.rst b/doc/api/_tight_layout_api.rst new file mode 100644 index 000000000000..ac4f66f280e1 --- /dev/null +++ b/doc/api/_tight_layout_api.rst @@ -0,0 +1,8 @@ +**************************** +``matplotlib._tight_layout`` +**************************** + +.. automodule:: matplotlib._tight_layout + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/_type1font.rst b/doc/api/_type1font.rst new file mode 100644 index 000000000000..1a9ff2292887 --- /dev/null +++ b/doc/api/_type1font.rst @@ -0,0 +1,8 @@ +************************* +``matplotlib._type1font`` +************************* + +.. automodule:: matplotlib._type1font + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/afm_api.rst b/doc/api/afm_api.rst deleted file mode 100644 index bcae04150909..000000000000 --- a/doc/api/afm_api.rst +++ /dev/null @@ -1,13 +0,0 @@ -****************** -``matplotlib.afm`` -****************** - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._afm - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/docstring_api.rst b/doc/api/docstring_api.rst deleted file mode 100644 index 38a73a2e83d1..000000000000 --- a/doc/api/docstring_api.rst +++ /dev/null @@ -1,13 +0,0 @@ -************************ -``matplotlib.docstring`` -************************ - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._docstring - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/index.rst b/doc/api/index.rst index 70c3b5343e7a..53f397a6817a 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -79,7 +79,6 @@ Alphabetical list of modules: :maxdepth: 1 matplotlib_configuration_api.rst - afm_api.rst animation_api.rst artist_api.rst axes_api.rst @@ -98,7 +97,6 @@ Alphabetical list of modules: container_api.rst contour_api.rst dates_api.rst - docstring_api.rst dviread.rst figure_api.rst font_manager_api.rst @@ -134,16 +132,18 @@ Alphabetical list of modules: text_api.rst texmanager_api.rst ticker_api.rst - tight_bbox_api.rst - tight_layout_api.rst transformations.rst tri_api.rst - type1font.rst typing_api.rst units_api.rst widgets_api.rst + _afm_api.rst _api_api.rst + _docstring_api.rst _enums_api.rst + _type1font.rst + _tight_bbox_api.rst + _tight_layout_api.rst toolkits/mplot3d.rst toolkits/axes_grid1.rst toolkits/axisartist.rst diff --git a/doc/api/tight_bbox_api.rst b/doc/api/tight_bbox_api.rst deleted file mode 100644 index 9e8dd2fa66f9..000000000000 --- a/doc/api/tight_bbox_api.rst +++ /dev/null @@ -1,13 +0,0 @@ -************************* -``matplotlib.tight_bbox`` -************************* - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._tight_bbox - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/tight_layout_api.rst b/doc/api/tight_layout_api.rst deleted file mode 100644 index 35f92e3ddced..000000000000 --- a/doc/api/tight_layout_api.rst +++ /dev/null @@ -1,13 +0,0 @@ -*************************** -``matplotlib.tight_layout`` -*************************** - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._tight_layout - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/type1font.rst b/doc/api/type1font.rst deleted file mode 100644 index 00ef38f4d447..000000000000 --- a/doc/api/type1font.rst +++ /dev/null @@ -1,13 +0,0 @@ -************************ -``matplotlib.type1font`` -************************ - -.. attention:: - This module is considered internal. - - Its use is deprecated and it will be removed in a future version. - -.. automodule:: matplotlib._type1font - :members: - :undoc-members: - :show-inheritance: From b50bd8bcb8302e87a831aa61f472dd5edf17a88e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 15 Aug 2024 02:18:17 -0400 Subject: [PATCH 0232/1230] Convert ft2font extension to pybind11 --- doc/missing-references.json | 3 - lib/matplotlib/ft2font.pyi | 99 +- lib/matplotlib/tests/test_ft2font.py | 6 +- requirements/testing/mypy.txt | 2 +- src/ft2font.h | 3 - src/ft2font_wrapper.cpp | 1676 ++++++++++---------------- src/meson.build | 2 +- 7 files changed, 715 insertions(+), 1076 deletions(-) diff --git a/doc/missing-references.json b/doc/missing-references.json index 87c9ce9b716f..a0eb69308eb4 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -349,9 +349,6 @@ "Figure.stale_callback": [ "doc/users/explain/figure/interactive_guide.rst:333" ], - "Glyph": [ - "doc/gallery/misc/ftface_props.rst:25" - ], "Image": [ "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.gci:4" ], diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 0a27411ff39c..b2eb8cea1cc8 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -1,4 +1,6 @@ +import sys from typing import BinaryIO, Literal, TypedDict, final, overload +from typing_extensions import Buffer # < Py 3.12 import numpy as np from numpy.typing import NDArray @@ -159,28 +161,7 @@ class _SfntPcltDict(TypedDict): serifStyle: int @final -class FT2Font: - ascender: int - bbox: tuple[int, int, int, int] - descender: int - face_flags: int - family_name: str - fname: str - height: int - max_advance_height: int - max_advance_width: int - num_charmaps: int - num_faces: int - num_fixed_sizes: int - num_glyphs: int - postscript_name: str - scalable: bool - style_flags: int - style_name: str - underline_position: int - underline_thickness: int - units_per_EM: int - +class FT2Font(Buffer): def __init__( self, filename: str | BinaryIO, @@ -189,6 +170,8 @@ class FT2Font: _fallback_list: list[FT2Font] | None = ..., _kerning_factor: int = ... ) -> None: ... + if sys.version_info[:2] >= (3, 12): + def __buffer__(self, flags: int) -> memoryview: ... def _get_fontmap(self, string: str) -> dict[str, FT2Font]: ... def clear(self) -> None: ... def draw_glyph_to_bitmap( @@ -232,23 +215,73 @@ class FT2Font: def set_text( self, string: str, angle: float = ..., flags: int = ... ) -> NDArray[np.float64]: ... + @property + def ascender(self) -> int: ... + @property + def bbox(self) -> tuple[int, int, int, int]: ... + @property + def descender(self) -> int: ... + @property + def face_flags(self) -> int: ... + @property + def family_name(self) -> str: ... + @property + def fname(self) -> str: ... + @property + def height(self) -> int: ... + @property + def max_advance_height(self) -> int: ... + @property + def max_advance_width(self) -> int: ... + @property + def num_charmaps(self) -> int: ... + @property + def num_faces(self) -> int: ... + @property + def num_fixed_sizes(self) -> int: ... + @property + def num_glyphs(self) -> int: ... + @property + def postscript_name(self) -> str: ... + @property + def scalable(self) -> bool: ... + @property + def style_flags(self) -> int: ... + @property + def style_name(self) -> str: ... + @property + def underline_position(self) -> int: ... + @property + def underline_thickness(self) -> int: ... + @property + def units_per_EM(self) -> int: ... @final -class FT2Image: # TODO: When updating mypy>=1.4, subclass from Buffer. +class FT2Image(Buffer): def __init__(self, width: float, height: float) -> None: ... def draw_rect_filled(self, x0: float, y0: float, x1: float, y1: float) -> None: ... + if sys.version_info[:2] >= (3, 12): + def __buffer__(self, flags: int) -> memoryview: ... @final class Glyph: - width: int - height: int - horiBearingX: int - horiBearingY: int - horiAdvance: int - linearHoriAdvance: int - vertBearingX: int - vertBearingY: int - vertAdvance: int - + @property + def width(self) -> int: ... + @property + def height(self) -> int: ... + @property + def horiBearingX(self) -> int: ... + @property + def horiBearingY(self) -> int: ... + @property + def horiAdvance(self) -> int: ... + @property + def linearHoriAdvance(self) -> int: ... + @property + def vertBearingX(self) -> int: ... + @property + def vertBearingY(self) -> int: ... + @property + def vertAdvance(self) -> int: ... @property def bbox(self) -> tuple[int, int, int, int]: ... diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index f383901b7b31..1bfa990bd8f5 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -130,6 +130,8 @@ def test_ft2font_invalid_args(tmp_path): # filename argument. with pytest.raises(TypeError, match='to a font file or a binary-mode file object'): ft2font.FT2Font(None) + with pytest.raises(TypeError, match='to a font file or a binary-mode file object'): + ft2font.FT2Font(object()) # Not bytes or string, and has no read() method. file = tmp_path / 'invalid-font.ttf' file.write_text('This is not a valid font file.') with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'), @@ -145,7 +147,7 @@ def test_ft2font_invalid_args(tmp_path): file = fm.findfont('DejaVu Sans') # hinting_factor argument. - with pytest.raises(TypeError, match='cannot be interpreted as an integer'): + with pytest.raises(TypeError, match='incompatible constructor arguments'): ft2font.FT2Font(file, 1.3) with pytest.raises(ValueError, match='hinting_factor must be greater than 0'): ft2font.FT2Font(file, 0) @@ -157,7 +159,7 @@ def test_ft2font_invalid_args(tmp_path): ft2font.FT2Font(file, _fallback_list=[0]) # type: ignore[list-item] # kerning_factor argument. - with pytest.raises(TypeError, match='cannot be interpreted as an integer'): + with pytest.raises(TypeError, match='incompatible constructor arguments'): ft2font.FT2Font(file, _kerning_factor=1.3) diff --git a/requirements/testing/mypy.txt b/requirements/testing/mypy.txt index 4fec6a8c000f..0b65050b52de 100644 --- a/requirements/testing/mypy.txt +++ b/requirements/testing/mypy.txt @@ -1,7 +1,7 @@ # Extra pip requirements for the GitHub Actions mypy build mypy>=1.9 -typing-extensions>=4.1 +typing-extensions>=4.6 # Extra stubs distributed separately from the main pypi package pandas-stubs diff --git a/src/ft2font.h b/src/ft2font.h index 2f24bfb01f79..7891c6050341 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -6,9 +6,6 @@ #ifndef MPL_FT2FONT_H #define MPL_FT2FONT_H -#define PY_SSIZE_T_CLEAN -#include - #include #include #include diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 6a05680a474c..27ba249ec916 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -1,133 +1,41 @@ -#include "mplutils.h" -#include "ft2font.h" -#include "numpy_cpp.h" -#include "py_converters.h" -#include "py_exceptions.h" +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION +#include +#include +#include -// From Python -#include +#include "ft2font.h" +#include "numpy/arrayobject.h" -#include +#include #include +#include +#include + +namespace py = pybind11; +using namespace pybind11::literals; -static PyObject *convert_xys_to_array(std::vector &xys) +static py::array_t +convert_xys_to_array(std::vector &xys) { - npy_intp dims[] = {(npy_intp)xys.size() / 2, 2 }; - if (dims[0] > 0) { - auto obj = PyArray_SimpleNew(2, dims, NPY_DOUBLE); - auto array = reinterpret_cast(obj); - memcpy(PyArray_DATA(array), xys.data(), PyArray_NBYTES(array)); - return obj; - } else { - return PyArray_SimpleNew(2, dims, NPY_DOUBLE); + py::ssize_t dims[] = { static_cast(xys.size()) / 2, 2 }; + py::array_t result(dims); + if (xys.size() > 0) { + memcpy(result.mutable_data(), xys.data(), result.nbytes()); } + return result; } /********************************************************************** * FT2Image * */ -typedef struct -{ - PyObject_HEAD - FT2Image *x; - Py_ssize_t shape[2]; - Py_ssize_t strides[2]; - Py_ssize_t suboffsets[2]; -} PyFT2Image; - -static PyTypeObject PyFT2ImageType; - -static PyObject *PyFT2Image_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - PyFT2Image *self; - self = (PyFT2Image *)type->tp_alloc(type, 0); - self->x = NULL; - return (PyObject *)self; -} - -static int PyFT2Image_init(PyFT2Image *self, PyObject *args, PyObject *kwds) -{ - double width; - double height; - - if (!PyArg_ParseTuple(args, "dd:FT2Image", &width, &height)) { - return -1; - } - - CALL_CPP_INIT("FT2Image", (self->x = new FT2Image(width, height))); - - return 0; -} - -static void PyFT2Image_dealloc(PyFT2Image *self) -{ - delete self->x; - Py_TYPE(self)->tp_free((PyObject *)self); -} - const char *PyFT2Image_draw_rect_filled__doc__ = - "draw_rect_filled(self, x0, y0, x1, y1)\n" - "--\n\n" - "Draw a filled rectangle to the image.\n"; - -static PyObject *PyFT2Image_draw_rect_filled(PyFT2Image *self, PyObject *args) -{ - double x0, y0, x1, y1; - - if (!PyArg_ParseTuple(args, "dddd:draw_rect_filled", &x0, &y0, &x1, &y1)) { - return NULL; - } - - CALL_CPP("draw_rect_filled", (self->x->draw_rect_filled(x0, y0, x1, y1))); + "Draw a filled rectangle to the image."; - Py_RETURN_NONE; -} - -static int PyFT2Image_get_buffer(PyFT2Image *self, Py_buffer *buf, int flags) +static void +PyFT2Image_draw_rect_filled(FT2Image *self, double x0, double y0, double x1, double y1) { - FT2Image *im = self->x; - - Py_INCREF(self); - buf->obj = (PyObject *)self; - buf->buf = im->get_buffer(); - buf->len = im->get_width() * im->get_height(); - buf->readonly = 0; - buf->format = (char *)"B"; - buf->ndim = 2; - self->shape[0] = im->get_height(); - self->shape[1] = im->get_width(); - buf->shape = self->shape; - self->strides[0] = im->get_width(); - self->strides[1] = 1; - buf->strides = self->strides; - buf->suboffsets = NULL; - buf->itemsize = 1; - buf->internal = NULL; - - return 1; -} - -static PyTypeObject* PyFT2Image_init_type() -{ - static PyMethodDef methods[] = { - {"draw_rect_filled", (PyCFunction)PyFT2Image_draw_rect_filled, METH_VARARGS, PyFT2Image_draw_rect_filled__doc__}, - {NULL} - }; - - static PyBufferProcs buffer_procs; - buffer_procs.bf_getbuffer = (getbufferproc)PyFT2Image_get_buffer; - - PyFT2ImageType.tp_name = "matplotlib.ft2font.FT2Image"; - PyFT2ImageType.tp_basicsize = sizeof(PyFT2Image); - PyFT2ImageType.tp_dealloc = (destructor)PyFT2Image_dealloc; - PyFT2ImageType.tp_flags = Py_TPFLAGS_DEFAULT; - PyFT2ImageType.tp_methods = methods; - PyFT2ImageType.tp_new = PyFT2Image_new; - PyFT2ImageType.tp_init = (initproc)PyFT2Image_init; - PyFT2ImageType.tp_as_buffer = &buffer_procs; - - return &PyFT2ImageType; + self->draw_rect_filled(x0, y0, x1, y1); } /********************************************************************** @@ -136,7 +44,6 @@ static PyTypeObject* PyFT2Image_init_type() typedef struct { - PyObject_HEAD size_t glyphInd; long width; long height; @@ -150,16 +57,14 @@ typedef struct FT_BBox bbox; } PyGlyph; -static PyTypeObject PyGlyphType; - -static PyObject *PyGlyph_from_FT2Font(const FT2Font *font) +static PyGlyph * +PyGlyph_from_FT2Font(const FT2Font *font) { const FT_Face &face = font->get_face(); const long hinting_factor = font->get_hinting_factor(); const FT_Glyph &glyph = font->get_last_glyph(); - PyGlyph *self; - self = (PyGlyph *)PyGlyphType.tp_alloc(&PyGlyphType, 0); + PyGlyph *self = new PyGlyph(); self->glyphInd = font->get_last_glyph_index(); FT_Glyph_Get_CBox(glyph, ft_glyph_bbox_subpixels, &self->bbox); @@ -174,48 +79,14 @@ static PyObject *PyGlyph_from_FT2Font(const FT2Font *font) self->vertBearingY = face->glyph->metrics.vertBearingY; self->vertAdvance = face->glyph->metrics.vertAdvance; - return (PyObject *)self; -} - -static void PyGlyph_dealloc(PyGlyph *self) -{ - Py_TYPE(self)->tp_free((PyObject *)self); -} - -static PyObject *PyGlyph_get_bbox(PyGlyph *self, void *closure) -{ - return Py_BuildValue( - "llll", self->bbox.xMin, self->bbox.yMin, self->bbox.xMax, self->bbox.yMax); + return self; } -static PyTypeObject *PyGlyph_init_type() +static py::tuple +PyGlyph_get_bbox(PyGlyph *self) { - static PyMemberDef members[] = { - {(char *)"width", T_LONG, offsetof(PyGlyph, width), READONLY, (char *)""}, - {(char *)"height", T_LONG, offsetof(PyGlyph, height), READONLY, (char *)""}, - {(char *)"horiBearingX", T_LONG, offsetof(PyGlyph, horiBearingX), READONLY, (char *)""}, - {(char *)"horiBearingY", T_LONG, offsetof(PyGlyph, horiBearingY), READONLY, (char *)""}, - {(char *)"horiAdvance", T_LONG, offsetof(PyGlyph, horiAdvance), READONLY, (char *)""}, - {(char *)"linearHoriAdvance", T_LONG, offsetof(PyGlyph, linearHoriAdvance), READONLY, (char *)""}, - {(char *)"vertBearingX", T_LONG, offsetof(PyGlyph, vertBearingX), READONLY, (char *)""}, - {(char *)"vertBearingY", T_LONG, offsetof(PyGlyph, vertBearingY), READONLY, (char *)""}, - {(char *)"vertAdvance", T_LONG, offsetof(PyGlyph, vertAdvance), READONLY, (char *)""}, - {NULL} - }; - - static PyGetSetDef getset[] = { - {(char *)"bbox", (getter)PyGlyph_get_bbox, NULL, NULL, NULL}, - {NULL} - }; - - PyGlyphType.tp_name = "matplotlib.ft2font.Glyph"; - PyGlyphType.tp_basicsize = sizeof(PyGlyph); - PyGlyphType.tp_dealloc = (destructor)PyGlyph_dealloc; - PyGlyphType.tp_flags = Py_TPFLAGS_DEFAULT; - PyGlyphType.tp_members = members; - PyGlyphType.tp_getset = getset; - - return &PyGlyphType; + return py::make_tuple(self->bbox.xMin, self->bbox.yMin, + self->bbox.xMax, self->bbox.yMax); } /********************************************************************** @@ -224,40 +95,33 @@ static PyTypeObject *PyGlyph_init_type() struct PyFT2Font { - PyObject_HEAD FT2Font *x; - PyObject *py_file; + py::object py_file; FT_StreamRec stream; - Py_ssize_t shape[2]; - Py_ssize_t strides[2]; - Py_ssize_t suboffsets[2]; - std::vector fallbacks; -}; + py::list fallbacks; -static PyTypeObject PyFT2FontType; + ~PyFT2Font() + { + delete this->x; + } +}; -static unsigned long read_from_file_callback(FT_Stream stream, - unsigned long offset, - unsigned char *buffer, - unsigned long count) +static unsigned long +read_from_file_callback(FT_Stream stream, unsigned long offset, unsigned char *buffer, + unsigned long count) { - PyObject *py_file = ((PyFT2Font *)stream->descriptor.pointer)->py_file; - PyObject *seek_result = NULL, *read_result = NULL; + PyFT2Font *self = (PyFT2Font *)stream->descriptor.pointer; Py_ssize_t n_read = 0; - if (!(seek_result = PyObject_CallMethod(py_file, "seek", "k", offset)) - || !(read_result = PyObject_CallMethod(py_file, "read", "k", count))) { - goto exit; - } - char *tmpbuf; - if (PyBytes_AsStringAndSize(read_result, &tmpbuf, &n_read) == -1) { - goto exit; - } - memcpy(buffer, tmpbuf, n_read); -exit: - Py_XDECREF(seek_result); - Py_XDECREF(read_result); - if (PyErr_Occurred()) { - PyErr_WriteUnraisable(py_file); + try { + char *tmpbuf; + auto seek_result = self->py_file.attr("seek")(offset); + auto read_result = self->py_file.attr("read")(count); + if (PyBytes_AsStringAndSize(read_result.ptr(), &tmpbuf, &n_read) == -1) { + throw py::error_already_set(); + } + memcpy(buffer, tmpbuf, n_read); + } catch (py::error_already_set &eas) { + eas.discard_as_unraisable(__func__); if (!count) { return 1; // Non-zero signals error, when count == 0. } @@ -265,28 +129,24 @@ static unsigned long read_from_file_callback(FT_Stream stream, return (unsigned long)n_read; } -static void close_file_callback(FT_Stream stream) +static void +close_file_callback(FT_Stream stream) { PyObject *type, *value, *traceback; PyErr_Fetch(&type, &value, &traceback); PyFT2Font *self = (PyFT2Font *)stream->descriptor.pointer; - PyObject *close_result = NULL; - if (!(close_result = PyObject_CallMethod(self->py_file, "close", ""))) { - goto exit; - } -exit: - Py_XDECREF(close_result); - Py_CLEAR(self->py_file); - if (PyErr_Occurred()) { - PyErr_WriteUnraisable((PyObject*)self); + try { + self->py_file.attr("close")(); + } catch (py::error_already_set &eas) { + eas.discard_as_unraisable(__func__); } + self->py_file = py::object(); PyErr_Restore(type, value, traceback); } static void ft_glyph_warn(FT_ULong charcode, std::set family_names) { - PyObject *text_helpers = NULL, *tmp = NULL; std::set::iterator it = family_names.begin(); std::stringstream ss; ss<<*it; @@ -294,33 +154,12 @@ ft_glyph_warn(FT_ULong charcode, std::set family_names) ss<<", "<<*it; } - if (!(text_helpers = PyImport_ImportModule("matplotlib._text_helpers")) || - !(tmp = PyObject_CallMethod(text_helpers, - "warn_on_missing_glyph", "(k, s)", - charcode, ss.str().c_str()))) { - goto exit; - } -exit: - Py_XDECREF(text_helpers); - Py_XDECREF(tmp); - if (PyErr_Occurred()) { - throw mpl::exception(); - } -} - -static PyObject *PyFT2Font_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - PyFT2Font *self; - self = (PyFT2Font *)type->tp_alloc(type, 0); - self->x = NULL; - self->py_file = NULL; - memset(&self->stream, 0, sizeof(FT_StreamRec)); - return (PyObject *)self; + auto text_helpers = py::module_::import("matplotlib._text_helpers"); + auto warn_on_missing_glyph = text_helpers.attr("warn_on_missing_glyph"); + warn_on_missing_glyph(charcode, ss.str()); } const char *PyFT2Font_init__doc__ = - "FT2Font(filename, hinting_factor=8, *, _fallback_list=None, _kerning_factor=0)\n" - "--\n\n" "Create a new FT2Font object.\n" "\n" "Parameters\n" @@ -341,353 +180,204 @@ const char *PyFT2Font_init__doc__ = "\n" " .. warning::\n" " This API is private: do not use it directly\n" - "\n" - "Attributes\n" - "----------\n" - "num_faces : int\n" - " Number of faces in file.\n" - "face_flags, style_flags : int\n" - " Face and style flags; see the ft2font constants.\n" - "num_glyphs : int\n" - " Number of glyphs in the face.\n" - "family_name, style_name : str\n" - " Face family and style name.\n" - "num_fixed_sizes : int\n" - " Number of bitmap in the face.\n" - "scalable : bool\n" - " Whether face is scalable; attributes after this one are only\n" - " defined for scalable faces.\n" - "bbox : tuple[int, int, int, int]\n" - " Face global bounding box (xmin, ymin, xmax, ymax).\n" - "units_per_EM : int\n" - " Number of font units covered by the EM.\n" - "ascender, descender : int\n" - " Ascender and descender in 26.6 units.\n" - "height : int\n" - " Height in 26.6 units; used to compute a default line spacing\n" - " (baseline-to-baseline distance).\n" - "max_advance_width, max_advance_height : int\n" - " Maximum horizontal and vertical cursor advance for all glyphs.\n" - "underline_position, underline_thickness : int\n" - " Vertical position and thickness of the underline bar.\n" - "postscript_name : str\n" - " PostScript name of the font.\n"; - -static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds) +; + +static PyFT2Font * +PyFT2Font_init(py::object filename, long hinting_factor = 8, + py::object fallback_list_or_none = py::none(), int kerning_factor = 0) { - PyObject *filename = NULL, *open = NULL, *data = NULL, *fallback_list = NULL; - FT_Open_Args open_args; - long hinting_factor = 8; - int kerning_factor = 0; - const char *names[] = { - "filename", "hinting_factor", "_fallback_list", "_kerning_factor", NULL - }; - std::vector fallback_fonts; - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "O|l$Oi:FT2Font", (char **)names, &filename, - &hinting_factor, &fallback_list, &kerning_factor)) { - return -1; - } if (hinting_factor <= 0) { - PyErr_SetString(PyExc_ValueError, - "hinting_factor must be greater than 0"); - goto exit; + throw py::value_error("hinting_factor must be greater than 0"); } + PyFT2Font *self = new PyFT2Font(); + self->x = NULL; + memset(&self->stream, 0, sizeof(FT_StreamRec)); self->stream.base = NULL; self->stream.size = 0x7fffffff; // Unknown size. self->stream.pos = 0; self->stream.descriptor.pointer = self; self->stream.read = &read_from_file_callback; + FT_Open_Args open_args; memset((void *)&open_args, 0, sizeof(FT_Open_Args)); open_args.flags = FT_OPEN_STREAM; open_args.stream = &self->stream; - if (fallback_list) { - if (!PyList_Check(fallback_list)) { - PyErr_SetString(PyExc_TypeError, "Fallback list must be a list"); - goto exit; + std::vector fallback_fonts; + if (!fallback_list_or_none.is_none()) { + if (!py::isinstance(fallback_list_or_none)) { + throw py::type_error("Fallback list must be a list"); } - Py_ssize_t size = PyList_Size(fallback_list); + auto fallback_list = fallback_list_or_none.cast(); // go through fallbacks once to make sure the types are right - for (Py_ssize_t i = 0; i < size; ++i) { - // this returns a borrowed reference - PyObject* item = PyList_GetItem(fallback_list, i); - if (!PyObject_IsInstance(item, PyObject_Type(reinterpret_cast(self)))) { - PyErr_SetString(PyExc_TypeError, "Fallback fonts must be FT2Font objects."); - goto exit; + for (auto item : fallback_list) { + if (!py::isinstance(item)) { + throw py::type_error("Fallback fonts must be FT2Font objects."); } } // go through a second time to add them to our lists - for (Py_ssize_t i = 0; i < size; ++i) { - // this returns a borrowed reference - PyObject* item = PyList_GetItem(fallback_list, i); - // Increase the ref count, we will undo this in dealloc this makes - // sure things do not get gc'd under us! - Py_INCREF(item); - self->fallbacks.push_back(item); + for (auto item : fallback_list) { + self->fallbacks.append(item); // Also (locally) cache the underlying FT2Font objects. As long as // the Python objects are kept alive, these pointer are good. - FT2Font *fback = reinterpret_cast(item)->x; + FT2Font *fback = py::cast(item)->x; fallback_fonts.push_back(fback); } } - if (PyBytes_Check(filename) || PyUnicode_Check(filename)) { - if (!(open = PyDict_GetItemString(PyEval_GetBuiltins(), "open")) // Borrowed reference. - || !(self->py_file = PyObject_CallFunction(open, "Os", filename, "rb"))) { - goto exit; - } + if (py::isinstance(filename) || py::isinstance(filename)) { + self->py_file = py::module_::import("io").attr("open")(filename, "rb"); self->stream.close = &close_file_callback; - } else if (!PyObject_HasAttrString(filename, "read") - || !(data = PyObject_CallMethod(filename, "read", "i", 0)) - || !PyBytes_Check(data)) { - PyErr_SetString(PyExc_TypeError, - "First argument must be a path to a font file or a binary-mode file object"); - Py_CLEAR(data); - goto exit; } else { + try { + // This will catch various issues: + // 1. `read` not being an attribute. + // 2. `read` raising an error. + // 3. `read` returning something other than `bytes`. + auto data = filename.attr("read")(0).cast(); + } catch (const std::exception&) { + throw py::type_error( + "First argument must be a path to a font file or a binary-mode file object"); + } self->py_file = filename; self->stream.close = NULL; - Py_INCREF(filename); } - Py_CLEAR(data); - CALL_CPP_FULL( - "FT2Font", - (self->x = new FT2Font(open_args, hinting_factor, fallback_fonts, ft_glyph_warn)), - Py_CLEAR(self->py_file), -1); + self->x = new FT2Font(open_args, hinting_factor, fallback_fonts, ft_glyph_warn); - CALL_CPP_INIT("FT2Font->set_kerning_factor", (self->x->set_kerning_factor(kerning_factor))); + self->x->set_kerning_factor(kerning_factor); -exit: - return PyErr_Occurred() ? -1 : 0; -} - -static void PyFT2Font_dealloc(PyFT2Font *self) -{ - delete self->x; - for (size_t i = 0; i < self->fallbacks.size(); i++) { - Py_DECREF(self->fallbacks[i]); - } - - Py_XDECREF(self->py_file); - Py_TYPE(self)->tp_free((PyObject *)self); + return self; } const char *PyFT2Font_clear__doc__ = - "clear(self)\n" - "--\n\n" - "Clear all the glyphs, reset for a new call to `.set_text`.\n"; + "Clear all the glyphs, reset for a new call to `.set_text`."; -static PyObject *PyFT2Font_clear(PyFT2Font *self, PyObject *args) +static void +PyFT2Font_clear(PyFT2Font *self) { - CALL_CPP("clear", (self->x->clear())); - - Py_RETURN_NONE; + self->x->clear(); } const char *PyFT2Font_set_size__doc__ = - "set_size(self, ptsize, dpi)\n" - "--\n\n" - "Set the point size and dpi of the text.\n"; + "Set the point size and dpi of the text."; -static PyObject *PyFT2Font_set_size(PyFT2Font *self, PyObject *args) +static void +PyFT2Font_set_size(PyFT2Font *self, double ptsize, double dpi) { - double ptsize; - double dpi; - - if (!PyArg_ParseTuple(args, "dd:set_size", &ptsize, &dpi)) { - return NULL; - } - - CALL_CPP("set_size", (self->x->set_size(ptsize, dpi))); - - Py_RETURN_NONE; + self->x->set_size(ptsize, dpi); } const char *PyFT2Font_set_charmap__doc__ = - "set_charmap(self, i)\n" - "--\n\n" - "Make the i-th charmap current.\n"; + "Make the i-th charmap current."; -static PyObject *PyFT2Font_set_charmap(PyFT2Font *self, PyObject *args) +static void +PyFT2Font_set_charmap(PyFT2Font *self, int i) { - int i; - - if (!PyArg_ParseTuple(args, "i:set_charmap", &i)) { - return NULL; - } - - CALL_CPP("set_charmap", (self->x->set_charmap(i))); - - Py_RETURN_NONE; + self->x->set_charmap(i); } const char *PyFT2Font_select_charmap__doc__ = - "select_charmap(self, i)\n" - "--\n\n" - "Select a charmap by its FT_Encoding number.\n"; + "Select a charmap by its FT_Encoding number."; -static PyObject *PyFT2Font_select_charmap(PyFT2Font *self, PyObject *args) +static void +PyFT2Font_select_charmap(PyFT2Font *self, unsigned long i) { - unsigned long i; - - if (!PyArg_ParseTuple(args, "k:select_charmap", &i)) { - return NULL; - } - - CALL_CPP("select_charmap", self->x->select_charmap(i)); - - Py_RETURN_NONE; + self->x->select_charmap(i); } const char *PyFT2Font_get_kerning__doc__ = - "get_kerning(self, left, right, mode)\n" - "--\n\n" "Get the kerning between *left* and *right* glyph indices.\n" - "*mode* is a kerning mode constant:\n\n" + "\n" + "*mode* is a kerning mode constant:\n" + "\n" "- KERNING_DEFAULT - Return scaled and grid-fitted kerning distances\n" "- KERNING_UNFITTED - Return scaled but un-grid-fitted kerning distances\n" "- KERNING_UNSCALED - Return the kerning vector in original font units\n"; -static PyObject *PyFT2Font_get_kerning(PyFT2Font *self, PyObject *args) +static int +PyFT2Font_get_kerning(PyFT2Font *self, FT_UInt left, FT_UInt right, FT_UInt mode) { - FT_UInt left, right, mode; - int result; bool fallback = true; - if (!PyArg_ParseTuple(args, "III:get_kerning", &left, &right, &mode)) { - return NULL; - } - - CALL_CPP("get_kerning", (result = self->x->get_kerning(left, right, mode, fallback))); - - return PyLong_FromLong(result); + return self->x->get_kerning(left, right, mode, fallback); } const char *PyFT2Font_get_fontmap__doc__ = - "_get_fontmap(self, string)\n" - "--\n\n" "Get a mapping between characters and the font that includes them.\n" "A dictionary mapping unicode characters to PyFT2Font objects."; -static PyObject *PyFT2Font_get_fontmap(PyFT2Font *self, PyObject *args, PyObject *kwds) -{ - PyObject *textobj; - const char *names[] = { "string", NULL }; - - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "O:_get_fontmap", (char **)names, &textobj)) { - return NULL; - } +static py::dict +PyFT2Font_get_fontmap(PyFT2Font *self, std::u32string text) +{ std::set codepoints; - size_t size; - if (PyUnicode_Check(textobj)) { - size = PyUnicode_GET_LENGTH(textobj); - for (size_t i = 0; i < size; ++i) { - codepoints.insert(PyUnicode_ReadChar(textobj, i)); - } - } else { - PyErr_SetString(PyExc_TypeError, "string must be str"); - return NULL; - } - PyObject *char_to_font; - if (!(char_to_font = PyDict_New())) { - return NULL; + for (auto code : text) { + codepoints.insert(code); } - for (auto it = codepoints.begin(); it != codepoints.end(); ++it) { - auto x = *it; - PyObject* target_font; + + py::dict char_to_font; + for (auto code : codepoints) { + py::object target_font; int index; - if (self->x->get_char_fallback_index(x, index)) { + if (self->x->get_char_fallback_index(code, index)) { if (index >= 0) { target_font = self->fallbacks[index]; } else { - target_font = (PyObject *)self; + target_font = py::cast(self); } } else { // TODO Handle recursion! - target_font = (PyObject *)self; + target_font = py::cast(self); } - PyObject *key = NULL; - bool error = (!(key = PyUnicode_FromFormat("%c", x)) - || (PyDict_SetItem(char_to_font, key, target_font) == -1)); - Py_XDECREF(key); - if (error) { - Py_DECREF(char_to_font); - PyErr_SetString(PyExc_ValueError, "Something went very wrong"); - return NULL; - } + auto key = py::cast(std::u32string(1, code)); + char_to_font[key] = target_font; } return char_to_font; } - const char *PyFT2Font_set_text__doc__ = - "set_text(self, string, angle=0.0, flags=32)\n" - "--\n\n" "Set the text *string* and *angle*.\n" "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" "the default value is LOAD_FORCE_AUTOHINT.\n" "You must call this before `.draw_glyphs_to_bitmap`.\n" "A sequence of x,y positions in 26.6 subpixels is returned; divide by 64 for pixels.\n"; -static PyObject *PyFT2Font_set_text(PyFT2Font *self, PyObject *args, PyObject *kwds) +static py::array_t +PyFT2Font_set_text(PyFT2Font *self, std::u32string text, double angle = 0.0, + FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT) { - PyObject *textobj; - double angle = 0.0; - FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; std::vector xys; - const char *names[] = { "string", "angle", "flags", NULL }; - - /* This makes a technically incorrect assumption that FT_Int32 is - int. In theory it can also be long, if the size of int is less - than 32 bits. This is very unlikely on modern platforms. */ - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "O|di:set_text", (char **)names, &textobj, &angle, &flags)) { - return NULL; - } - std::vector codepoints; size_t size; - if (PyUnicode_Check(textobj)) { - size = PyUnicode_GET_LENGTH(textobj); - codepoints.resize(size); - for (size_t i = 0; i < size; ++i) { - codepoints[i] = PyUnicode_ReadChar(textobj, i); - } - } else { - PyErr_SetString(PyExc_TypeError, "set_text requires str-input."); - return NULL; + size = text.size(); + codepoints.resize(size); + for (size_t i = 0; i < size; ++i) { + codepoints[i] = text[i]; } uint32_t* codepoints_array = NULL; if (size > 0) { codepoints_array = &codepoints[0]; } - CALL_CPP("set_text", self->x->set_text(size, codepoints_array, angle, flags, xys)); + self->x->set_text(size, codepoints_array, angle, flags, xys); return convert_xys_to_array(xys); } const char *PyFT2Font_get_num_glyphs__doc__ = - "get_num_glyphs(self)\n" - "--\n\n" - "Return the number of loaded glyphs.\n"; + "Return the number of loaded glyphs."; -static PyObject *PyFT2Font_get_num_glyphs(PyFT2Font *self, PyObject *args) +static size_t +PyFT2Font_get_num_glyphs(PyFT2Font *self) { - return PyLong_FromSize_t(self->x->get_num_glyphs()); + return self->x->get_num_glyphs(); } const char *PyFT2Font_load_char__doc__ = - "load_char(self, charcode, flags=32)\n" - "--\n\n" "Load character with *charcode* in current fontfile and set glyph.\n" "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" "the default value is LOAD_FORCE_AUTOHINT.\n" @@ -702,30 +392,19 @@ const char *PyFT2Font_load_char__doc__ = "- vertBearingY: top side bearing in vertical layouts\n" "- vertAdvance: advance height for vertical layout\n"; -static PyObject *PyFT2Font_load_char(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyGlyph * +PyFT2Font_load_char(PyFT2Font *self, long charcode, + FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT) { - long charcode; bool fallback = true; - FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; - const char *names[] = { "charcode", "flags", NULL }; - - /* This makes a technically incorrect assumption that FT_Int32 is - int. In theory it can also be long, if the size of int is less - than 32 bits. This is very unlikely on modern platforms. */ - if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|i:load_char", (char **)names, &charcode, - &flags)) { - return NULL; - } - FT2Font *ft_object = NULL; - CALL_CPP("load_char", (self->x->load_char(charcode, flags, ft_object, fallback))); + + self->x->load_char(charcode, flags, ft_object, fallback); return PyGlyph_from_FT2Font(ft_object); } const char *PyFT2Font_load_glyph__doc__ = - "load_glyph(self, glyphindex, flags=32)\n" - "--\n\n" "Load character with *glyphindex* in current fontfile and set glyph.\n" "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" "the default value is LOAD_FORCE_AUTOHINT.\n" @@ -740,99 +419,72 @@ const char *PyFT2Font_load_glyph__doc__ = "- vertBearingY: top side bearing in vertical layouts\n" "- vertAdvance: advance height for vertical layout\n"; -static PyObject *PyFT2Font_load_glyph(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyGlyph * +PyFT2Font_load_glyph(PyFT2Font *self, FT_UInt glyph_index, + FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT) { - FT_UInt glyph_index; - FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; bool fallback = true; - const char *names[] = { "glyph_index", "flags", NULL }; - - /* This makes a technically incorrect assumption that FT_Int32 is - int. In theory it can also be long, if the size of int is less - than 32 bits. This is very unlikely on modern platforms. */ - if (!PyArg_ParseTupleAndKeywords(args, kwds, "I|i:load_glyph", (char **)names, &glyph_index, - &flags)) { - return NULL; - } - FT2Font *ft_object = NULL; - CALL_CPP("load_glyph", (self->x->load_glyph(glyph_index, flags, ft_object, fallback))); + + self->x->load_glyph(glyph_index, flags, ft_object, fallback); return PyGlyph_from_FT2Font(ft_object); } const char *PyFT2Font_get_width_height__doc__ = - "get_width_height(self)\n" - "--\n\n" "Get the width and height in 26.6 subpixels of the current string set by `.set_text`.\n" "The rotation of the string is accounted for. To get width and height\n" "in pixels, divide these values by 64.\n"; -static PyObject *PyFT2Font_get_width_height(PyFT2Font *self, PyObject *args) +static py::tuple +PyFT2Font_get_width_height(PyFT2Font *self) { long width, height; - CALL_CPP("get_width_height", (self->x->get_width_height(&width, &height))); + self->x->get_width_height(&width, &height); - return Py_BuildValue("ll", width, height); + return py::make_tuple(width, height); } const char *PyFT2Font_get_bitmap_offset__doc__ = - "get_bitmap_offset(self)\n" - "--\n\n" "Get the (x, y) offset in 26.6 subpixels for the bitmap if ink hangs left or below (0, 0).\n" "Since Matplotlib only supports left-to-right text, y is always 0.\n"; -static PyObject *PyFT2Font_get_bitmap_offset(PyFT2Font *self, PyObject *args) +static py::tuple +PyFT2Font_get_bitmap_offset(PyFT2Font *self) { long x, y; - CALL_CPP("get_bitmap_offset", (self->x->get_bitmap_offset(&x, &y))); + self->x->get_bitmap_offset(&x, &y); - return Py_BuildValue("ll", x, y); + return py::make_tuple(x, y); } const char *PyFT2Font_get_descent__doc__ = - "get_descent(self)\n" - "--\n\n" "Get the descent in 26.6 subpixels of the current string set by `.set_text`.\n" "The rotation of the string is accounted for. To get the descent\n" "in pixels, divide this value by 64.\n"; -static PyObject *PyFT2Font_get_descent(PyFT2Font *self, PyObject *args) +static long +PyFT2Font_get_descent(PyFT2Font *self) { - long descent; - - CALL_CPP("get_descent", (descent = self->x->get_descent())); - - return PyLong_FromLong(descent); + return self->x->get_descent(); } const char *PyFT2Font_draw_glyphs_to_bitmap__doc__ = - "draw_glyphs_to_bitmap(self, antialiased=True)\n" - "--\n\n" "Draw the glyphs that were loaded by `.set_text` to the bitmap.\n" + "\n" "The bitmap size will be automatically set to include the glyphs.\n"; -static PyObject *PyFT2Font_draw_glyphs_to_bitmap(PyFT2Font *self, PyObject *args, PyObject *kwds) +static void +PyFT2Font_draw_glyphs_to_bitmap(PyFT2Font *self, bool antialiased = true) { - bool antialiased = true; - const char *names[] = { "antialiased", NULL }; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O&:draw_glyphs_to_bitmap", - (char **)names, &convert_bool, &antialiased)) { - return NULL; - } - - CALL_CPP("draw_glyphs_to_bitmap", (self->x->draw_glyphs_to_bitmap(antialiased))); - - Py_RETURN_NONE; + self->x->draw_glyphs_to_bitmap(antialiased); } const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = - "draw_glyph_to_bitmap(self, image, x, y, glyph, antialiased=True)\n" - "--\n\n" - "Draw a single glyph to *image* at pixel locations *x*, *y*\n" + "Draw a single glyph to the bitmap at pixel locations x, y.\n" + "\n" "Note it is your responsibility to create the image manually\n" "with the correct size before this call is made.\n" "\n" @@ -841,84 +493,48 @@ const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = "who want to render individual glyphs (e.g., returned by `.load_char`)\n" "at precise locations.\n"; -static PyObject *PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, PyObject *args, PyObject *kwds) +static void +PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, FT2Image &image, double xd, double yd, + PyGlyph *glyph, bool antialiased = true) { - PyFT2Image *image; - double xd, yd; - PyGlyph *glyph; - bool antialiased = true; - const char *names[] = { "image", "x", "y", "glyph", "antialiased", NULL }; - - if (!PyArg_ParseTupleAndKeywords(args, - kwds, - "O!ddO!|O&:draw_glyph_to_bitmap", - (char **)names, - &PyFT2ImageType, - &image, - &xd, - &yd, - &PyGlyphType, - &glyph, - &convert_bool, - &antialiased)) { - return NULL; - } - - CALL_CPP("draw_glyph_to_bitmap", - self->x->draw_glyph_to_bitmap(*(image->x), xd, yd, glyph->glyphInd, antialiased)); - - Py_RETURN_NONE; + self->x->draw_glyph_to_bitmap(image, xd, yd, glyph->glyphInd, antialiased); } const char *PyFT2Font_get_glyph_name__doc__ = - "get_glyph_name(self, index)\n" - "--\n\n" "Retrieve the ASCII name of a given glyph *index* in a face.\n" "\n" "Due to Matplotlib's internal design, for fonts that do not contain glyph\n" "names (per FT_FACE_FLAG_GLYPH_NAMES), this returns a made-up name which\n" "does *not* roundtrip through `.get_name_index`.\n"; -static PyObject *PyFT2Font_get_glyph_name(PyFT2Font *self, PyObject *args) +static py::str +PyFT2Font_get_glyph_name(PyFT2Font *self, unsigned int glyph_number) { - unsigned int glyph_number; std::string buffer; bool fallback = true; - if (!PyArg_ParseTuple(args, "I:get_glyph_name", &glyph_number)) { - return NULL; - } buffer.resize(128); - CALL_CPP("get_glyph_name", - (self->x->get_glyph_name(glyph_number, buffer, fallback))); - return PyUnicode_FromString(buffer.c_str()); + self->x->get_glyph_name(glyph_number, buffer, fallback); + // pybind11 uses the entire string's size(), so trim all the NULLs off the end. + auto len = buffer.find('\0'); + if (len != buffer.npos) { + buffer.resize(len); + } + return buffer; } const char *PyFT2Font_get_charmap__doc__ = - "get_charmap(self)\n" - "--\n\n" "Return a dict that maps the character codes of the selected charmap\n" "(Unicode by default) to their corresponding glyph indices.\n"; -static PyObject *PyFT2Font_get_charmap(PyFT2Font *self, PyObject *args) +static py::dict +PyFT2Font_get_charmap(PyFT2Font *self) { - PyObject *charmap; - if (!(charmap = PyDict_New())) { - return NULL; - } + py::dict charmap; FT_UInt index; FT_ULong code = FT_Get_First_Char(self->x->get_face(), &index); while (index != 0) { - PyObject *key = NULL, *val = NULL; - bool error = (!(key = PyLong_FromLong(code)) - || !(val = PyLong_FromLong(index)) - || (PyDict_SetItem(charmap, key, val) == -1)); - Py_XDECREF(key); - Py_XDECREF(val); - if (error) { - Py_DECREF(charmap); - return NULL; - } + charmap[py::cast(code)] = py::cast(index); code = FT_Get_Next_Char(self->x->get_face(), code, &index); } return charmap; @@ -926,638 +542,632 @@ static PyObject *PyFT2Font_get_charmap(PyFT2Font *self, PyObject *args) const char *PyFT2Font_get_char_index__doc__ = - "get_char_index(self, codepoint)\n" - "--\n\n" - "Return the glyph index corresponding to a character *codepoint*.\n"; + "Return the glyph index corresponding to a character *codepoint*."; -static PyObject *PyFT2Font_get_char_index(PyFT2Font *self, PyObject *args) +static FT_UInt +PyFT2Font_get_char_index(PyFT2Font *self, FT_ULong ccode) { - FT_UInt index; - FT_ULong ccode; bool fallback = true; - if (!PyArg_ParseTuple(args, "k:get_char_index", &ccode)) { - return NULL; - } - - CALL_CPP("get_char_index", index = self->x->get_char_index(ccode, fallback)); - - return PyLong_FromLong(index); + return self->x->get_char_index(ccode, fallback); } const char *PyFT2Font_get_sfnt__doc__ = - "get_sfnt(self)\n" - "--\n\n" "Load the entire SFNT names table, as a dict whose keys are\n" "(platform-ID, ISO-encoding-scheme, language-code, and description)\n" "tuples.\n"; -static PyObject *PyFT2Font_get_sfnt(PyFT2Font *self, PyObject *args) +static py::dict +PyFT2Font_get_sfnt(PyFT2Font *self) { - PyObject *names; - if (!(self->x->get_face()->face_flags & FT_FACE_FLAG_SFNT)) { - PyErr_SetString(PyExc_ValueError, "No SFNT name table"); - return NULL; + throw py::value_error("No SFNT name table"); } size_t count = FT_Get_Sfnt_Name_Count(self->x->get_face()); - names = PyDict_New(); - if (names == NULL) { - return NULL; - } + py::dict names; for (FT_UInt j = 0; j < count; ++j) { FT_SfntName sfnt; FT_Error error = FT_Get_Sfnt_Name(self->x->get_face(), j, &sfnt); if (error) { - Py_DECREF(names); - PyErr_SetString(PyExc_ValueError, "Could not get SFNT name"); - return NULL; + throw py::value_error("Could not get SFNT name"); } - PyObject *key = Py_BuildValue( - "HHHH", sfnt.platform_id, sfnt.encoding_id, sfnt.language_id, sfnt.name_id); - if (key == NULL) { - Py_DECREF(names); - return NULL; - } - - PyObject *val = PyBytes_FromStringAndSize((const char *)sfnt.string, sfnt.string_len); - if (val == NULL) { - Py_DECREF(key); - Py_DECREF(names); - return NULL; - } - - if (PyDict_SetItem(names, key, val)) { - Py_DECREF(key); - Py_DECREF(val); - Py_DECREF(names); - return NULL; - } - - Py_DECREF(key); - Py_DECREF(val); + auto key = py::make_tuple( + sfnt.platform_id, sfnt.encoding_id, sfnt.language_id, sfnt.name_id); + auto val = py::bytes(reinterpret_cast(sfnt.string), + sfnt.string_len); + names[key] = val; } return names; } const char *PyFT2Font_get_name_index__doc__ = - "get_name_index(self, name)\n" - "--\n\n" "Return the glyph index of a given glyph *name*.\n" "The glyph index 0 means 'undefined character code'.\n"; -static PyObject *PyFT2Font_get_name_index(PyFT2Font *self, PyObject *args) +static long +PyFT2Font_get_name_index(PyFT2Font *self, char *glyphname) { - char *glyphname; - long name_index; - if (!PyArg_ParseTuple(args, "s:get_name_index", &glyphname)) { - return NULL; - } - CALL_CPP("get_name_index", name_index = self->x->get_name_index(glyphname)); - return PyLong_FromLong(name_index); + return self->x->get_name_index(glyphname); } const char *PyFT2Font_get_ps_font_info__doc__ = - "get_ps_font_info(self)\n" - "--\n\n" - "Return the information in the PS Font Info structure.\n"; + "Return the information in the PS Font Info structure."; -static PyObject *PyFT2Font_get_ps_font_info(PyFT2Font *self, PyObject *args) +static py::tuple +PyFT2Font_get_ps_font_info(PyFT2Font *self) { PS_FontInfoRec fontinfo; FT_Error error = FT_Get_PS_Font_Info(self->x->get_face(), &fontinfo); if (error) { - PyErr_SetString(PyExc_ValueError, "Could not get PS font info"); - return NULL; + throw py::value_error("Could not get PS font info"); } - return Py_BuildValue("ssssslbhH", - fontinfo.version ? fontinfo.version : "", - fontinfo.notice ? fontinfo.notice : "", - fontinfo.full_name ? fontinfo.full_name : "", - fontinfo.family_name ? fontinfo.family_name : "", - fontinfo.weight ? fontinfo.weight : "", - fontinfo.italic_angle, - fontinfo.is_fixed_pitch, - fontinfo.underline_position, - fontinfo.underline_thickness); + return py::make_tuple( + fontinfo.version ? fontinfo.version : "", + fontinfo.notice ? fontinfo.notice : "", + fontinfo.full_name ? fontinfo.full_name : "", + fontinfo.family_name ? fontinfo.family_name : "", + fontinfo.weight ? fontinfo.weight : "", + fontinfo.italic_angle, + fontinfo.is_fixed_pitch, + fontinfo.underline_position, + fontinfo.underline_thickness); } const char *PyFT2Font_get_sfnt_table__doc__ = - "get_sfnt_table(self, name)\n" - "--\n\n" "Return one of the following SFNT tables: head, maxp, OS/2, hhea, " - "vhea, post, or pclt.\n"; - -static PyObject *PyFT2Font_get_sfnt_table(PyFT2Font *self, PyObject *args) -{ - char *tagname; - if (!PyArg_ParseTuple(args, "s:get_sfnt_table", &tagname)) { - return NULL; - } - - int tag; - const char *tags[] = { "head", "maxp", "OS/2", "hhea", "vhea", "post", "pclt", NULL }; + "vhea, post, or pclt."; + +static std::optional +PyFT2Font_get_sfnt_table(PyFT2Font *self, std::string tagname) +{ + FT_Sfnt_Tag tag; + const std::unordered_map names = { + {"head", FT_SFNT_HEAD}, + {"maxp", FT_SFNT_MAXP}, + {"OS/2", FT_SFNT_OS2}, + {"hhea", FT_SFNT_HHEA}, + {"vhea", FT_SFNT_VHEA}, + {"post", FT_SFNT_POST}, + {"pclt", FT_SFNT_PCLT}, + }; - for (tag = 0; tags[tag] != NULL; tag++) { - if (strncmp(tagname, tags[tag], 5) == 0) { - break; - } + try { + tag = names.at(tagname); + } catch (const std::out_of_range&) { + return std::nullopt; } - void *table = FT_Get_Sfnt_Table(self->x->get_face(), (FT_Sfnt_Tag)tag); + void *table = FT_Get_Sfnt_Table(self->x->get_face(), tag); if (!table) { - Py_RETURN_NONE; + return std::nullopt; } switch (tag) { - case 0: { - char head_dict[] = - "{s:(h,H), s:(h,H), s:l, s:l, s:H, s:H," - "s:(I,I), s:(I,I), s:h, s:h, s:h, s:h, s:H, s:H, s:h, s:h, s:h}"; - TT_Header *t = (TT_Header *)table; - return Py_BuildValue(head_dict, - "version", FIXED_MAJOR(t->Table_Version), FIXED_MINOR(t->Table_Version), - "fontRevision", FIXED_MAJOR(t->Font_Revision), FIXED_MINOR(t->Font_Revision), - "checkSumAdjustment", t->CheckSum_Adjust, - "magicNumber", t->Magic_Number, - "flags", t->Flags, - "unitsPerEm", t->Units_Per_EM, - // FreeType 2.6.1 defines these two timestamps as FT_Long, - // but they should be unsigned (fixed in 2.10.0): - // https://gitlab.freedesktop.org/freetype/freetype/-/commit/3e8ec291ffcfa03c8ecba1cdbfaa55f5577f5612 - // It's actually read from the file structure as two 32-bit - // values, so we need to cast down in size to prevent sign - // extension from producing huge 64-bit values. - "created", static_cast(t->Created[0]), static_cast(t->Created[1]), - "modified", static_cast(t->Modified[0]), static_cast(t->Modified[1]), - "xMin", t->xMin, - "yMin", t->yMin, - "xMax", t->xMax, - "yMax", t->yMax, - "macStyle", t->Mac_Style, - "lowestRecPPEM", t->Lowest_Rec_PPEM, - "fontDirectionHint", t->Font_Direction, - "indexToLocFormat", t->Index_To_Loc_Format, - "glyphDataFormat", t->Glyph_Data_Format); + case FT_SFNT_HEAD: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->Table_Version), + FIXED_MINOR(t->Table_Version)), + "fontRevision"_a=py::make_tuple(FIXED_MAJOR(t->Font_Revision), + FIXED_MINOR(t->Font_Revision)), + "checkSumAdjustment"_a=t->CheckSum_Adjust, + "magicNumber"_a=t->Magic_Number, + "flags"_a=t->Flags, + "unitsPerEm"_a=t->Units_Per_EM, + // FreeType 2.6.1 defines these two timestamps as FT_Long, but they should + // be unsigned (fixed in 2.10.0): + // https://gitlab.freedesktop.org/freetype/freetype/-/commit/3e8ec291ffcfa03c8ecba1cdbfaa55f5577f5612 + // It's actually read from the file structure as two 32-bit values, so we + // need to cast down in size to prevent sign extension from producing huge + // 64-bit values. + "created"_a=py::make_tuple(static_cast(t->Created[0]), + static_cast(t->Created[1])), + "modified"_a=py::make_tuple(static_cast(t->Modified[0]), + static_cast(t->Modified[1])), + "xMin"_a=t->xMin, + "yMin"_a=t->yMin, + "xMax"_a=t->xMax, + "yMax"_a=t->yMax, + "macStyle"_a=t->Mac_Style, + "lowestRecPPEM"_a=t->Lowest_Rec_PPEM, + "fontDirectionHint"_a=t->Font_Direction, + "indexToLocFormat"_a=t->Index_To_Loc_Format, + "glyphDataFormat"_a=t->Glyph_Data_Format); } - case 1: { - char maxp_dict[] = - "{s:(h,H), s:H, s:H, s:H, s:H, s:H, s:H," - "s:H, s:H, s:H, s:H, s:H, s:H, s:H, s:H}"; - TT_MaxProfile *t = (TT_MaxProfile *)table; - return Py_BuildValue(maxp_dict, - "version", FIXED_MAJOR(t->version), FIXED_MINOR(t->version), - "numGlyphs", t->numGlyphs, - "maxPoints", t->maxPoints, - "maxContours", t->maxContours, - "maxComponentPoints", t->maxCompositePoints, - "maxComponentContours", t->maxCompositeContours, - "maxZones", t->maxZones, - "maxTwilightPoints", t->maxTwilightPoints, - "maxStorage", t->maxStorage, - "maxFunctionDefs", t->maxFunctionDefs, - "maxInstructionDefs", t->maxInstructionDefs, - "maxStackElements", t->maxStackElements, - "maxSizeOfInstructions", t->maxSizeOfInstructions, - "maxComponentElements", t->maxComponentElements, - "maxComponentDepth", t->maxComponentDepth); + case FT_SFNT_MAXP: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->version), + FIXED_MINOR(t->version)), + "numGlyphs"_a=t->numGlyphs, + "maxPoints"_a=t->maxPoints, + "maxContours"_a=t->maxContours, + "maxComponentPoints"_a=t->maxCompositePoints, + "maxComponentContours"_a=t->maxCompositeContours, + "maxZones"_a=t->maxZones, + "maxTwilightPoints"_a=t->maxTwilightPoints, + "maxStorage"_a=t->maxStorage, + "maxFunctionDefs"_a=t->maxFunctionDefs, + "maxInstructionDefs"_a=t->maxInstructionDefs, + "maxStackElements"_a=t->maxStackElements, + "maxSizeOfInstructions"_a=t->maxSizeOfInstructions, + "maxComponentElements"_a=t->maxComponentElements, + "maxComponentDepth"_a=t->maxComponentDepth); } - case 2: { - char os_2_dict[] = - "{s:H, s:h, s:H, s:H, s:H, s:h, s:h, s:h," - "s:h, s:h, s:h, s:h, s:h, s:h, s:h, s:h, s:y#, s:(kkkk)," - "s:y#, s:H, s:H, s:H}"; - TT_OS2 *t = (TT_OS2 *)table; - return Py_BuildValue(os_2_dict, - "version", t->version, - "xAvgCharWidth", t->xAvgCharWidth, - "usWeightClass", t->usWeightClass, - "usWidthClass", t->usWidthClass, - "fsType", t->fsType, - "ySubscriptXSize", t->ySubscriptXSize, - "ySubscriptYSize", t->ySubscriptYSize, - "ySubscriptXOffset", t->ySubscriptXOffset, - "ySubscriptYOffset", t->ySubscriptYOffset, - "ySuperscriptXSize", t->ySuperscriptXSize, - "ySuperscriptYSize", t->ySuperscriptYSize, - "ySuperscriptXOffset", t->ySuperscriptXOffset, - "ySuperscriptYOffset", t->ySuperscriptYOffset, - "yStrikeoutSize", t->yStrikeoutSize, - "yStrikeoutPosition", t->yStrikeoutPosition, - "sFamilyClass", t->sFamilyClass, - "panose", t->panose, Py_ssize_t(10), - "ulCharRange", t->ulUnicodeRange1, t->ulUnicodeRange2, t->ulUnicodeRange3, t->ulUnicodeRange4, - "achVendID", t->achVendID, Py_ssize_t(4), - "fsSelection", t->fsSelection, - "fsFirstCharIndex", t->usFirstCharIndex, - "fsLastCharIndex", t->usLastCharIndex); + case FT_SFNT_OS2: { + auto t = static_cast(table); + return py::dict( + "version"_a=t->version, + "xAvgCharWidth"_a=t->xAvgCharWidth, + "usWeightClass"_a=t->usWeightClass, + "usWidthClass"_a=t->usWidthClass, + "fsType"_a=t->fsType, + "ySubscriptXSize"_a=t->ySubscriptXSize, + "ySubscriptYSize"_a=t->ySubscriptYSize, + "ySubscriptXOffset"_a=t->ySubscriptXOffset, + "ySubscriptYOffset"_a=t->ySubscriptYOffset, + "ySuperscriptXSize"_a=t->ySuperscriptXSize, + "ySuperscriptYSize"_a=t->ySuperscriptYSize, + "ySuperscriptXOffset"_a=t->ySuperscriptXOffset, + "ySuperscriptYOffset"_a=t->ySuperscriptYOffset, + "yStrikeoutSize"_a=t->yStrikeoutSize, + "yStrikeoutPosition"_a=t->yStrikeoutPosition, + "sFamilyClass"_a=t->sFamilyClass, + "panose"_a=py::bytes(reinterpret_cast(t->panose), 10), + "ulCharRange"_a=py::make_tuple(t->ulUnicodeRange1, t->ulUnicodeRange2, + t->ulUnicodeRange3, t->ulUnicodeRange4), + "achVendID"_a=py::bytes(reinterpret_cast(t->achVendID), 4), + "fsSelection"_a=t->fsSelection, + "fsFirstCharIndex"_a=t->usFirstCharIndex, + "fsLastCharIndex"_a=t->usLastCharIndex); } - case 3: { - char hhea_dict[] = - "{s:(h,H), s:h, s:h, s:h, s:H, s:h, s:h, s:h," - "s:h, s:h, s:h, s:h, s:H}"; - TT_HoriHeader *t = (TT_HoriHeader *)table; - return Py_BuildValue(hhea_dict, - "version", FIXED_MAJOR(t->Version), FIXED_MINOR(t->Version), - "ascent", t->Ascender, - "descent", t->Descender, - "lineGap", t->Line_Gap, - "advanceWidthMax", t->advance_Width_Max, - "minLeftBearing", t->min_Left_Side_Bearing, - "minRightBearing", t->min_Right_Side_Bearing, - "xMaxExtent", t->xMax_Extent, - "caretSlopeRise", t->caret_Slope_Rise, - "caretSlopeRun", t->caret_Slope_Run, - "caretOffset", t->caret_Offset, - "metricDataFormat", t->metric_Data_Format, - "numOfLongHorMetrics", t->number_Of_HMetrics); + case FT_SFNT_HHEA: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->Version), + FIXED_MINOR(t->Version)), + "ascent"_a=t->Ascender, + "descent"_a=t->Descender, + "lineGap"_a=t->Line_Gap, + "advanceWidthMax"_a=t->advance_Width_Max, + "minLeftBearing"_a=t->min_Left_Side_Bearing, + "minRightBearing"_a=t->min_Right_Side_Bearing, + "xMaxExtent"_a=t->xMax_Extent, + "caretSlopeRise"_a=t->caret_Slope_Rise, + "caretSlopeRun"_a=t->caret_Slope_Run, + "caretOffset"_a=t->caret_Offset, + "metricDataFormat"_a=t->metric_Data_Format, + "numOfLongHorMetrics"_a=t->number_Of_HMetrics); } - case 4: { - char vhea_dict[] = - "{s:(h,H), s:h, s:h, s:h, s:H, s:h, s:h, s:h," - "s:h, s:h, s:h, s:h, s:H}"; - TT_VertHeader *t = (TT_VertHeader *)table; - return Py_BuildValue(vhea_dict, - "version", FIXED_MAJOR(t->Version), FIXED_MINOR(t->Version), - "vertTypoAscender", t->Ascender, - "vertTypoDescender", t->Descender, - "vertTypoLineGap", t->Line_Gap, - "advanceHeightMax", t->advance_Height_Max, - "minTopSideBearing", t->min_Top_Side_Bearing, - "minBottomSizeBearing", t->min_Bottom_Side_Bearing, - "yMaxExtent", t->yMax_Extent, - "caretSlopeRise", t->caret_Slope_Rise, - "caretSlopeRun", t->caret_Slope_Run, - "caretOffset", t->caret_Offset, - "metricDataFormat", t->metric_Data_Format, - "numOfLongVerMetrics", t->number_Of_VMetrics); + case FT_SFNT_VHEA: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->Version), + FIXED_MINOR(t->Version)), + "vertTypoAscender"_a=t->Ascender, + "vertTypoDescender"_a=t->Descender, + "vertTypoLineGap"_a=t->Line_Gap, + "advanceHeightMax"_a=t->advance_Height_Max, + "minTopSideBearing"_a=t->min_Top_Side_Bearing, + "minBottomSizeBearing"_a=t->min_Bottom_Side_Bearing, + "yMaxExtent"_a=t->yMax_Extent, + "caretSlopeRise"_a=t->caret_Slope_Rise, + "caretSlopeRun"_a=t->caret_Slope_Run, + "caretOffset"_a=t->caret_Offset, + "metricDataFormat"_a=t->metric_Data_Format, + "numOfLongVerMetrics"_a=t->number_Of_VMetrics); } - case 5: { - char post_dict[] = "{s:(h,H), s:(h,H), s:h, s:h, s:k, s:k, s:k, s:k, s:k}"; - TT_Postscript *t = (TT_Postscript *)table; - return Py_BuildValue(post_dict, - "format", FIXED_MAJOR(t->FormatType), FIXED_MINOR(t->FormatType), - "italicAngle", FIXED_MAJOR(t->italicAngle), FIXED_MINOR(t->italicAngle), - "underlinePosition", t->underlinePosition, - "underlineThickness", t->underlineThickness, - "isFixedPitch", t->isFixedPitch, - "minMemType42", t->minMemType42, - "maxMemType42", t->maxMemType42, - "minMemType1", t->minMemType1, - "maxMemType1", t->maxMemType1); + case FT_SFNT_POST: { + auto t = static_cast(table); + return py::dict( + "format"_a=py::make_tuple(FIXED_MAJOR(t->FormatType), + FIXED_MINOR(t->FormatType)), + "italicAngle"_a=py::make_tuple(FIXED_MAJOR(t->italicAngle), + FIXED_MINOR(t->italicAngle)), + "underlinePosition"_a=t->underlinePosition, + "underlineThickness"_a=t->underlineThickness, + "isFixedPitch"_a=t->isFixedPitch, + "minMemType42"_a=t->minMemType42, + "maxMemType42"_a=t->maxMemType42, + "minMemType1"_a=t->minMemType1, + "maxMemType1"_a=t->maxMemType1); } - case 6: { - char pclt_dict[] = - "{s:(h,H), s:k, s:H, s:H, s:H, s:H, s:H, s:H, s:y#, s:y#, s:b, " - "s:b, s:b}"; - TT_PCLT *t = (TT_PCLT *)table; - return Py_BuildValue(pclt_dict, - "version", FIXED_MAJOR(t->Version), FIXED_MINOR(t->Version), - "fontNumber", t->FontNumber, - "pitch", t->Pitch, - "xHeight", t->xHeight, - "style", t->Style, - "typeFamily", t->TypeFamily, - "capHeight", t->CapHeight, - "symbolSet", t->SymbolSet, - "typeFace", t->TypeFace, Py_ssize_t(16), - "characterComplement", t->CharacterComplement, Py_ssize_t(8), - "strokeWeight", t->StrokeWeight, - "widthType", t->WidthType, - "serifStyle", t->SerifStyle); + case FT_SFNT_PCLT: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->Version), + FIXED_MINOR(t->Version)), + "fontNumber"_a=t->FontNumber, + "pitch"_a=t->Pitch, + "xHeight"_a=t->xHeight, + "style"_a=t->Style, + "typeFamily"_a=t->TypeFamily, + "capHeight"_a=t->CapHeight, + "symbolSet"_a=t->SymbolSet, + "typeFace"_a=py::bytes(reinterpret_cast(t->TypeFace), 16), + "characterComplement"_a=py::bytes( + reinterpret_cast(t->CharacterComplement), 8), + "strokeWeight"_a=t->StrokeWeight, + "widthType"_a=t->WidthType, + "serifStyle"_a=t->SerifStyle); } default: - Py_RETURN_NONE; + return std::nullopt; } } const char *PyFT2Font_get_path__doc__ = - "get_path(self)\n" - "--\n\n" - "Get the path data from the currently loaded glyph as a tuple of vertices, " - "codes.\n"; + "Get the path data from the currently loaded glyph as a tuple of vertices, codes."; -static PyObject *PyFT2Font_get_path(PyFT2Font *self, PyObject *args) +static py::tuple +PyFT2Font_get_path(PyFT2Font *self) { std::vector vertices; std::vector codes; - CALL_CPP("get_path", self->x->get_path(vertices, codes)); + self->x->get_path(vertices, codes); - npy_intp length = codes.size(); - npy_intp vertices_dims[2] = { length, 2 }; - numpy::array_view vertices_arr(vertices_dims); - memcpy(vertices_arr.data(), vertices.data(), sizeof(double) * vertices.size()); - npy_intp codes_dims[1] = { length }; - numpy::array_view codes_arr(codes_dims); - memcpy(codes_arr.data(), codes.data(), codes.size()); + py::ssize_t length = codes.size(); + py::ssize_t vertices_dims[2] = { length, 2 }; + py::array_t vertices_arr(vertices_dims); + if (length > 0) { + memcpy(vertices_arr.mutable_data(), vertices.data(), vertices_arr.nbytes()); + } + py::ssize_t codes_dims[1] = { length }; + py::array_t codes_arr(codes_dims); + if (length > 0) { + memcpy(codes_arr.mutable_data(), codes.data(), codes_arr.nbytes()); + } - return Py_BuildValue("NN", vertices_arr.pyobj(), codes_arr.pyobj()); + return py::make_tuple(vertices_arr, codes_arr); } const char *PyFT2Font_get_image__doc__ = - "get_image(self)\n" - "--\n\n" - "Return the underlying image buffer for this font object.\n"; + "Return the underlying image buffer for this font object."; -static PyObject *PyFT2Font_get_image(PyFT2Font *self, PyObject *args) +static py::array +PyFT2Font_get_image(PyFT2Font *self) { FT2Image &im = self->x->get_image(); - npy_intp dims[] = {(npy_intp)im.get_height(), (npy_intp)im.get_width() }; - return PyArray_SimpleNewFromData(2, dims, NPY_UBYTE, im.get_buffer()); + py::ssize_t dims[] = { + static_cast(im.get_height()), + static_cast(im.get_width()) + }; + return py::array_t(dims, im.get_buffer()); } -static PyObject *PyFT2Font_postscript_name(PyFT2Font *self, void *closure) +static const char * +PyFT2Font_postscript_name(PyFT2Font *self) { const char *ps_name = FT_Get_Postscript_Name(self->x->get_face()); if (ps_name == NULL) { ps_name = "UNAVAILABLE"; } - return PyUnicode_FromString(ps_name); + return ps_name; } -static PyObject *PyFT2Font_num_faces(PyFT2Font *self, void *closure) +static FT_Long +PyFT2Font_num_faces(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->num_faces); + return self->x->get_face()->num_faces; } -static PyObject *PyFT2Font_family_name(PyFT2Font *self, void *closure) +static const char * +PyFT2Font_family_name(PyFT2Font *self) { const char *name = self->x->get_face()->family_name; if (name == NULL) { name = "UNAVAILABLE"; } - return PyUnicode_FromString(name); + return name; } -static PyObject *PyFT2Font_style_name(PyFT2Font *self, void *closure) +static const char * +PyFT2Font_style_name(PyFT2Font *self) { const char *name = self->x->get_face()->style_name; if (name == NULL) { name = "UNAVAILABLE"; } - return PyUnicode_FromString(name); + return name; } -static PyObject *PyFT2Font_face_flags(PyFT2Font *self, void *closure) +static FT_Long +PyFT2Font_face_flags(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->face_flags); + return self->x->get_face()->face_flags; } -static PyObject *PyFT2Font_style_flags(PyFT2Font *self, void *closure) +static FT_Long +PyFT2Font_style_flags(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->style_flags); + return self->x->get_face()->style_flags; } -static PyObject *PyFT2Font_num_glyphs(PyFT2Font *self, void *closure) +static FT_Long +PyFT2Font_num_glyphs(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->num_glyphs); + return self->x->get_face()->num_glyphs; } -static PyObject *PyFT2Font_num_fixed_sizes(PyFT2Font *self, void *closure) +static FT_Int +PyFT2Font_num_fixed_sizes(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->num_fixed_sizes); + return self->x->get_face()->num_fixed_sizes; } -static PyObject *PyFT2Font_num_charmaps(PyFT2Font *self, void *closure) +static FT_Int +PyFT2Font_num_charmaps(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->num_charmaps); + return self->x->get_face()->num_charmaps; } -static PyObject *PyFT2Font_scalable(PyFT2Font *self, void *closure) +static bool +PyFT2Font_scalable(PyFT2Font *self) { if (FT_IS_SCALABLE(self->x->get_face())) { - Py_RETURN_TRUE; + return true; } - Py_RETURN_FALSE; + return false; } -static PyObject *PyFT2Font_units_per_EM(PyFT2Font *self, void *closure) +static FT_UShort +PyFT2Font_units_per_EM(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->units_per_EM); + return self->x->get_face()->units_per_EM; } -static PyObject *PyFT2Font_get_bbox(PyFT2Font *self, void *closure) +static py::tuple +PyFT2Font_get_bbox(PyFT2Font *self) { FT_BBox *bbox = &(self->x->get_face()->bbox); - return Py_BuildValue("llll", - bbox->xMin, bbox->yMin, bbox->xMax, bbox->yMax); + return py::make_tuple(bbox->xMin, bbox->yMin, bbox->xMax, bbox->yMax); } -static PyObject *PyFT2Font_ascender(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_ascender(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->ascender); + return self->x->get_face()->ascender; } -static PyObject *PyFT2Font_descender(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_descender(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->descender); + return self->x->get_face()->descender; } -static PyObject *PyFT2Font_height(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_height(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->height); + return self->x->get_face()->height; } -static PyObject *PyFT2Font_max_advance_width(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_max_advance_width(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->max_advance_width); + return self->x->get_face()->max_advance_width; } -static PyObject *PyFT2Font_max_advance_height(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_max_advance_height(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->max_advance_height); + return self->x->get_face()->max_advance_height; } -static PyObject *PyFT2Font_underline_position(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_underline_position(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->underline_position); + return self->x->get_face()->underline_position; } -static PyObject *PyFT2Font_underline_thickness(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_underline_thickness(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->underline_thickness); + return self->x->get_face()->underline_thickness; } -static PyObject *PyFT2Font_fname(PyFT2Font *self, void *closure) +static py::str +PyFT2Font_fname(PyFT2Font *self) { if (self->stream.close) { // Called passed a filename to the constructor. - return PyObject_GetAttrString(self->py_file, "name"); + return self->py_file.attr("name"); } else { - Py_INCREF(self->py_file); - return self->py_file; + return py::cast(self->py_file); } } -static int PyFT2Font_get_buffer(PyFT2Font *self, Py_buffer *buf, int flags) +PYBIND11_MODULE(ft2font, m) { - FT2Image &im = self->x->get_image(); - - Py_INCREF(self); - buf->obj = (PyObject *)self; - buf->buf = im.get_buffer(); - buf->len = im.get_width() * im.get_height(); - buf->readonly = 0; - buf->format = (char *)"B"; - buf->ndim = 2; - self->shape[0] = im.get_height(); - self->shape[1] = im.get_width(); - buf->shape = self->shape; - self->strides[0] = im.get_width(); - self->strides[1] = 1; - buf->strides = self->strides; - buf->suboffsets = NULL; - buf->itemsize = 1; - buf->internal = NULL; - - return 1; -} - -static PyTypeObject *PyFT2Font_init_type() -{ - static PyGetSetDef getset[] = { - {(char *)"postscript_name", (getter)PyFT2Font_postscript_name, NULL, NULL, NULL}, - {(char *)"num_faces", (getter)PyFT2Font_num_faces, NULL, NULL, NULL}, - {(char *)"family_name", (getter)PyFT2Font_family_name, NULL, NULL, NULL}, - {(char *)"style_name", (getter)PyFT2Font_style_name, NULL, NULL, NULL}, - {(char *)"face_flags", (getter)PyFT2Font_face_flags, NULL, NULL, NULL}, - {(char *)"style_flags", (getter)PyFT2Font_style_flags, NULL, NULL, NULL}, - {(char *)"num_glyphs", (getter)PyFT2Font_num_glyphs, NULL, NULL, NULL}, - {(char *)"num_fixed_sizes", (getter)PyFT2Font_num_fixed_sizes, NULL, NULL, NULL}, - {(char *)"num_charmaps", (getter)PyFT2Font_num_charmaps, NULL, NULL, NULL}, - {(char *)"scalable", (getter)PyFT2Font_scalable, NULL, NULL, NULL}, - {(char *)"units_per_EM", (getter)PyFT2Font_units_per_EM, NULL, NULL, NULL}, - {(char *)"bbox", (getter)PyFT2Font_get_bbox, NULL, NULL, NULL}, - {(char *)"ascender", (getter)PyFT2Font_ascender, NULL, NULL, NULL}, - {(char *)"descender", (getter)PyFT2Font_descender, NULL, NULL, NULL}, - {(char *)"height", (getter)PyFT2Font_height, NULL, NULL, NULL}, - {(char *)"max_advance_width", (getter)PyFT2Font_max_advance_width, NULL, NULL, NULL}, - {(char *)"max_advance_height", (getter)PyFT2Font_max_advance_height, NULL, NULL, NULL}, - {(char *)"underline_position", (getter)PyFT2Font_underline_position, NULL, NULL, NULL}, - {(char *)"underline_thickness", (getter)PyFT2Font_underline_thickness, NULL, NULL, NULL}, - {(char *)"fname", (getter)PyFT2Font_fname, NULL, NULL, NULL}, - {NULL} - }; - - static PyMethodDef methods[] = { - {"clear", (PyCFunction)PyFT2Font_clear, METH_NOARGS, PyFT2Font_clear__doc__}, - {"set_size", (PyCFunction)PyFT2Font_set_size, METH_VARARGS, PyFT2Font_set_size__doc__}, - {"set_charmap", (PyCFunction)PyFT2Font_set_charmap, METH_VARARGS, PyFT2Font_set_charmap__doc__}, - {"select_charmap", (PyCFunction)PyFT2Font_select_charmap, METH_VARARGS, PyFT2Font_select_charmap__doc__}, - {"get_kerning", (PyCFunction)PyFT2Font_get_kerning, METH_VARARGS, PyFT2Font_get_kerning__doc__}, - {"set_text", (PyCFunction)PyFT2Font_set_text, METH_VARARGS|METH_KEYWORDS, PyFT2Font_set_text__doc__}, - {"_get_fontmap", (PyCFunction)PyFT2Font_get_fontmap, METH_VARARGS|METH_KEYWORDS, PyFT2Font_get_fontmap__doc__}, - {"get_num_glyphs", (PyCFunction)PyFT2Font_get_num_glyphs, METH_NOARGS, PyFT2Font_get_num_glyphs__doc__}, - {"load_char", (PyCFunction)PyFT2Font_load_char, METH_VARARGS|METH_KEYWORDS, PyFT2Font_load_char__doc__}, - {"load_glyph", (PyCFunction)PyFT2Font_load_glyph, METH_VARARGS|METH_KEYWORDS, PyFT2Font_load_glyph__doc__}, - {"get_width_height", (PyCFunction)PyFT2Font_get_width_height, METH_NOARGS, PyFT2Font_get_width_height__doc__}, - {"get_bitmap_offset", (PyCFunction)PyFT2Font_get_bitmap_offset, METH_NOARGS, PyFT2Font_get_bitmap_offset__doc__}, - {"get_descent", (PyCFunction)PyFT2Font_get_descent, METH_NOARGS, PyFT2Font_get_descent__doc__}, - {"draw_glyphs_to_bitmap", (PyCFunction)PyFT2Font_draw_glyphs_to_bitmap, METH_VARARGS|METH_KEYWORDS, PyFT2Font_draw_glyphs_to_bitmap__doc__}, - {"draw_glyph_to_bitmap", (PyCFunction)PyFT2Font_draw_glyph_to_bitmap, METH_VARARGS|METH_KEYWORDS, PyFT2Font_draw_glyph_to_bitmap__doc__}, - {"get_glyph_name", (PyCFunction)PyFT2Font_get_glyph_name, METH_VARARGS, PyFT2Font_get_glyph_name__doc__}, - {"get_charmap", (PyCFunction)PyFT2Font_get_charmap, METH_NOARGS, PyFT2Font_get_charmap__doc__}, - {"get_char_index", (PyCFunction)PyFT2Font_get_char_index, METH_VARARGS, PyFT2Font_get_char_index__doc__}, - {"get_sfnt", (PyCFunction)PyFT2Font_get_sfnt, METH_NOARGS, PyFT2Font_get_sfnt__doc__}, - {"get_name_index", (PyCFunction)PyFT2Font_get_name_index, METH_VARARGS, PyFT2Font_get_name_index__doc__}, - {"get_ps_font_info", (PyCFunction)PyFT2Font_get_ps_font_info, METH_NOARGS, PyFT2Font_get_ps_font_info__doc__}, - {"get_sfnt_table", (PyCFunction)PyFT2Font_get_sfnt_table, METH_VARARGS, PyFT2Font_get_sfnt_table__doc__}, - {"get_path", (PyCFunction)PyFT2Font_get_path, METH_NOARGS, PyFT2Font_get_path__doc__}, - {"get_image", (PyCFunction)PyFT2Font_get_image, METH_NOARGS, PyFT2Font_get_image__doc__}, - {NULL} + auto ia = [m]() -> const void* { + import_array(); + return &m; }; - - static PyBufferProcs buffer_procs; - buffer_procs.bf_getbuffer = (getbufferproc)PyFT2Font_get_buffer; - - PyFT2FontType.tp_name = "matplotlib.ft2font.FT2Font"; - PyFT2FontType.tp_doc = PyFT2Font_init__doc__; - PyFT2FontType.tp_basicsize = sizeof(PyFT2Font); - PyFT2FontType.tp_dealloc = (destructor)PyFT2Font_dealloc; - PyFT2FontType.tp_flags = Py_TPFLAGS_DEFAULT; - PyFT2FontType.tp_methods = methods; - PyFT2FontType.tp_getset = getset; - PyFT2FontType.tp_new = PyFT2Font_new; - PyFT2FontType.tp_init = (initproc)PyFT2Font_init; - PyFT2FontType.tp_as_buffer = &buffer_procs; - - return &PyFT2FontType; -} - -static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "ft2font" }; - -PyMODINIT_FUNC PyInit_ft2font(void) -{ - import_array(); + if (ia() == NULL) { + throw py::error_already_set(); + } if (FT_Init_FreeType(&_ft2Library)) { // initialize library - return PyErr_Format( - PyExc_RuntimeError, "Could not initialize the freetype2 library"); + throw std::runtime_error("Could not initialize the freetype2 library"); } FT_Int major, minor, patch; char version_string[64]; FT_Library_Version(_ft2Library, &major, &minor, &patch); snprintf(version_string, sizeof(version_string), "%d.%d.%d", major, minor, patch); - PyObject *m; - if (!(m = PyModule_Create(&moduledef)) || - prepare_and_add_type(PyFT2Image_init_type(), m) || - prepare_and_add_type(PyFT2Font_init_type(), m) || - // Glyph is not constructible from Python, thus not added to the module. - PyType_Ready(PyGlyph_init_type()) || - PyModule_AddStringConstant(m, "__freetype_version__", version_string) || - PyModule_AddStringConstant(m, "__freetype_build_type__", FREETYPE_BUILD_TYPE) || - PyModule_AddIntConstant(m, "SCALABLE", FT_FACE_FLAG_SCALABLE) || - PyModule_AddIntConstant(m, "FIXED_SIZES", FT_FACE_FLAG_FIXED_SIZES) || - PyModule_AddIntConstant(m, "FIXED_WIDTH", FT_FACE_FLAG_FIXED_WIDTH) || - PyModule_AddIntConstant(m, "SFNT", FT_FACE_FLAG_SFNT) || - PyModule_AddIntConstant(m, "HORIZONTAL", FT_FACE_FLAG_HORIZONTAL) || - PyModule_AddIntConstant(m, "VERTICAL", FT_FACE_FLAG_VERTICAL) || - PyModule_AddIntConstant(m, "KERNING", FT_FACE_FLAG_KERNING) || - PyModule_AddIntConstant(m, "FAST_GLYPHS", FT_FACE_FLAG_FAST_GLYPHS) || - PyModule_AddIntConstant(m, "MULTIPLE_MASTERS", FT_FACE_FLAG_MULTIPLE_MASTERS) || - PyModule_AddIntConstant(m, "GLYPH_NAMES", FT_FACE_FLAG_GLYPH_NAMES) || - PyModule_AddIntConstant(m, "EXTERNAL_STREAM", FT_FACE_FLAG_EXTERNAL_STREAM) || - PyModule_AddIntConstant(m, "ITALIC", FT_STYLE_FLAG_ITALIC) || - PyModule_AddIntConstant(m, "BOLD", FT_STYLE_FLAG_BOLD) || - PyModule_AddIntConstant(m, "KERNING_DEFAULT", FT_KERNING_DEFAULT) || - PyModule_AddIntConstant(m, "KERNING_UNFITTED", FT_KERNING_UNFITTED) || - PyModule_AddIntConstant(m, "KERNING_UNSCALED", FT_KERNING_UNSCALED) || - PyModule_AddIntConstant(m, "LOAD_DEFAULT", FT_LOAD_DEFAULT) || - PyModule_AddIntConstant(m, "LOAD_NO_SCALE", FT_LOAD_NO_SCALE) || - PyModule_AddIntConstant(m, "LOAD_NO_HINTING", FT_LOAD_NO_HINTING) || - PyModule_AddIntConstant(m, "LOAD_RENDER", FT_LOAD_RENDER) || - PyModule_AddIntConstant(m, "LOAD_NO_BITMAP", FT_LOAD_NO_BITMAP) || - PyModule_AddIntConstant(m, "LOAD_VERTICAL_LAYOUT", FT_LOAD_VERTICAL_LAYOUT) || - PyModule_AddIntConstant(m, "LOAD_FORCE_AUTOHINT", FT_LOAD_FORCE_AUTOHINT) || - PyModule_AddIntConstant(m, "LOAD_CROP_BITMAP", FT_LOAD_CROP_BITMAP) || - PyModule_AddIntConstant(m, "LOAD_PEDANTIC", FT_LOAD_PEDANTIC) || - PyModule_AddIntConstant(m, "LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH", FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH) || - PyModule_AddIntConstant(m, "LOAD_NO_RECURSE", FT_LOAD_NO_RECURSE) || - PyModule_AddIntConstant(m, "LOAD_IGNORE_TRANSFORM", FT_LOAD_IGNORE_TRANSFORM) || - PyModule_AddIntConstant(m, "LOAD_MONOCHROME", FT_LOAD_MONOCHROME) || - PyModule_AddIntConstant(m, "LOAD_LINEAR_DESIGN", FT_LOAD_LINEAR_DESIGN) || - PyModule_AddIntConstant(m, "LOAD_NO_AUTOHINT", (unsigned long)FT_LOAD_NO_AUTOHINT) || - PyModule_AddIntConstant(m, "LOAD_TARGET_NORMAL", (unsigned long)FT_LOAD_TARGET_NORMAL) || - PyModule_AddIntConstant(m, "LOAD_TARGET_LIGHT", (unsigned long)FT_LOAD_TARGET_LIGHT) || - PyModule_AddIntConstant(m, "LOAD_TARGET_MONO", (unsigned long)FT_LOAD_TARGET_MONO) || - PyModule_AddIntConstant(m, "LOAD_TARGET_LCD", (unsigned long)FT_LOAD_TARGET_LCD) || - PyModule_AddIntConstant(m, "LOAD_TARGET_LCD_V", (unsigned long)FT_LOAD_TARGET_LCD_V)) { - FT_Done_FreeType(_ft2Library); - Py_XDECREF(m); - return NULL; - } - - return m; + py::class_(m, "FT2Image", py::is_final(), py::buffer_protocol()) + .def(py::init(), "width"_a, "height"_a) + .def("draw_rect_filled", &PyFT2Image_draw_rect_filled, + "x0"_a, "y0"_a, "x1"_a, "y1"_a, + PyFT2Image_draw_rect_filled__doc__) + .def_buffer([](FT2Image &self) -> py::buffer_info { + std::vector shape { self.get_height(), self.get_width() }; + std::vector strides { self.get_width(), 1 }; + return py::buffer_info(self.get_buffer(), shape, strides); + }); + + py::class_(m, "Glyph", py::is_final()) + .def(py::init<>([]() -> PyGlyph { + // Glyph is not useful from Python, so mark it as not constructible. + throw std::runtime_error("Glyph is not constructible"); + })) + .def_readonly("width", &PyGlyph::width) + .def_readonly("height", &PyGlyph::height) + .def_readonly("horiBearingX", &PyGlyph::horiBearingX) + .def_readonly("horiBearingY", &PyGlyph::horiBearingY) + .def_readonly("horiAdvance", &PyGlyph::horiAdvance) + .def_readonly("linearHoriAdvance", &PyGlyph::linearHoriAdvance) + .def_readonly("vertBearingX", &PyGlyph::vertBearingX) + .def_readonly("vertBearingY", &PyGlyph::vertBearingY) + .def_readonly("vertAdvance", &PyGlyph::vertAdvance) + .def_property_readonly("bbox", &PyGlyph_get_bbox); + + py::class_(m, "FT2Font", py::is_final(), py::buffer_protocol()) + .def(py::init(&PyFT2Font_init), + "filename"_a, "hinting_factor"_a=8, py::kw_only(), + "_fallback_list"_a=py::none(), "_kerning_factor"_a=0, + PyFT2Font_init__doc__) + .def("clear", &PyFT2Font_clear, PyFT2Font_clear__doc__) + .def("set_size", &PyFT2Font_set_size, "ptsize"_a, "dpi"_a, + PyFT2Font_set_size__doc__) + .def("set_charmap", &PyFT2Font_set_charmap, "i"_a, + PyFT2Font_set_charmap__doc__) + .def("select_charmap", &PyFT2Font_select_charmap, "i"_a, + PyFT2Font_select_charmap__doc__) + .def("get_kerning", &PyFT2Font_get_kerning, "left"_a, "right"_a, "mode"_a, + PyFT2Font_get_kerning__doc__) + .def("set_text", &PyFT2Font_set_text, + "string"_a, "angle"_a=0.0, "flags"_a=FT_LOAD_FORCE_AUTOHINT, + PyFT2Font_set_text__doc__) + .def("_get_fontmap", &PyFT2Font_get_fontmap, "string"_a, + PyFT2Font_get_fontmap__doc__) + .def("get_num_glyphs", &PyFT2Font_get_num_glyphs, PyFT2Font_get_num_glyphs__doc__) + .def("load_char", &PyFT2Font_load_char, + "charcode"_a, "flags"_a=FT_LOAD_FORCE_AUTOHINT, + PyFT2Font_load_char__doc__) + .def("load_glyph", &PyFT2Font_load_glyph, + "glyph_index"_a, "flags"_a=FT_LOAD_FORCE_AUTOHINT, + PyFT2Font_load_glyph__doc__) + .def("get_width_height", &PyFT2Font_get_width_height, + PyFT2Font_get_width_height__doc__) + .def("get_bitmap_offset", &PyFT2Font_get_bitmap_offset, + PyFT2Font_get_bitmap_offset__doc__) + .def("get_descent", &PyFT2Font_get_descent, PyFT2Font_get_descent__doc__) + .def("draw_glyphs_to_bitmap", &PyFT2Font_draw_glyphs_to_bitmap, + py::kw_only(), "antialiased"_a=true, + PyFT2Font_draw_glyphs_to_bitmap__doc__) + .def("draw_glyph_to_bitmap", &PyFT2Font_draw_glyph_to_bitmap, + "image"_a, "x"_a, "y"_a, "glyph"_a, py::kw_only(), "antialiased"_a=true, + PyFT2Font_draw_glyph_to_bitmap__doc__) + .def("get_glyph_name", &PyFT2Font_get_glyph_name, "index"_a, + PyFT2Font_get_glyph_name__doc__) + .def("get_charmap", &PyFT2Font_get_charmap, PyFT2Font_get_charmap__doc__) + .def("get_char_index", &PyFT2Font_get_char_index, "codepoint"_a, + PyFT2Font_get_char_index__doc__) + .def("get_sfnt", &PyFT2Font_get_sfnt, PyFT2Font_get_sfnt__doc__) + .def("get_name_index", &PyFT2Font_get_name_index, "name"_a, + PyFT2Font_get_name_index__doc__) + .def("get_ps_font_info", &PyFT2Font_get_ps_font_info, + PyFT2Font_get_ps_font_info__doc__) + .def("get_sfnt_table", &PyFT2Font_get_sfnt_table, "name"_a, + PyFT2Font_get_sfnt_table__doc__) + .def("get_path", &PyFT2Font_get_path, PyFT2Font_get_path__doc__) + .def("get_image", &PyFT2Font_get_image, PyFT2Font_get_image__doc__) + + .def_property_readonly("postscript_name", &PyFT2Font_postscript_name, + "PostScript name of the font.") + .def_property_readonly("num_faces", &PyFT2Font_num_faces, + "Number of faces in file.") + .def_property_readonly("family_name", &PyFT2Font_family_name, + "Face family name.") + .def_property_readonly("style_name", &PyFT2Font_style_name, + "Style name.") + .def_property_readonly("face_flags", &PyFT2Font_face_flags, + "Face flags; see the ft2font constants.") + .def_property_readonly("style_flags", &PyFT2Font_style_flags, + "Style flags; see the ft2font constants.") + .def_property_readonly("num_glyphs", &PyFT2Font_num_glyphs, + "Number of glyphs in the face.") + .def_property_readonly("num_fixed_sizes", &PyFT2Font_num_fixed_sizes, + "Number of bitmap in the face.") + .def_property_readonly("num_charmaps", &PyFT2Font_num_charmaps) + .def_property_readonly("scalable", &PyFT2Font_scalable, + "Whether face is scalable; attributes after this one " + "are only defined for scalable faces.") + .def_property_readonly("units_per_EM", &PyFT2Font_units_per_EM, + "Number of font units covered by the EM.") + .def_property_readonly("bbox", &PyFT2Font_get_bbox, + "Face global bounding box (xmin, ymin, xmax, ymax).") + .def_property_readonly("ascender", &PyFT2Font_ascender, + "Ascender in 26.6 units.") + .def_property_readonly("descender", &PyFT2Font_descender, + "Descender in 26.6 units.") + .def_property_readonly("height", &PyFT2Font_height, + "Height in 26.6 units; used to compute a default line " + "spacing (baseline-to-baseline distance).") + .def_property_readonly("max_advance_width", &PyFT2Font_max_advance_width, + "Maximum horizontal cursor advance for all glyphs.") + .def_property_readonly("max_advance_height", &PyFT2Font_max_advance_height, + "Maximum vertical cursor advance for all glyphs.") + .def_property_readonly("underline_position", &PyFT2Font_underline_position, + "Vertical position of the underline bar.") + .def_property_readonly("underline_thickness", &PyFT2Font_underline_thickness, + "Thickness of the underline bar.") + .def_property_readonly("fname", &PyFT2Font_fname) + + .def_buffer([](PyFT2Font &self) -> py::buffer_info { + FT2Image &im = self.x->get_image(); + std::vector shape { im.get_height(), im.get_width() }; + std::vector strides { im.get_width(), 1 }; + return py::buffer_info(im.get_buffer(), shape, strides); + }); + + m.attr("__freetype_version__") = version_string; + m.attr("__freetype_build_type__") = FREETYPE_BUILD_TYPE; + m.attr("SCALABLE") = FT_FACE_FLAG_SCALABLE; + m.attr("FIXED_SIZES") = FT_FACE_FLAG_FIXED_SIZES; + m.attr("FIXED_WIDTH") = FT_FACE_FLAG_FIXED_WIDTH; + m.attr("SFNT") = FT_FACE_FLAG_SFNT; + m.attr("HORIZONTAL") = FT_FACE_FLAG_HORIZONTAL; + m.attr("VERTICAL") = FT_FACE_FLAG_VERTICAL; + m.attr("KERNING") = FT_FACE_FLAG_KERNING; + m.attr("FAST_GLYPHS") = FT_FACE_FLAG_FAST_GLYPHS; + m.attr("MULTIPLE_MASTERS") = FT_FACE_FLAG_MULTIPLE_MASTERS; + m.attr("GLYPH_NAMES") = FT_FACE_FLAG_GLYPH_NAMES; + m.attr("EXTERNAL_STREAM") = FT_FACE_FLAG_EXTERNAL_STREAM; + m.attr("ITALIC") = FT_STYLE_FLAG_ITALIC; + m.attr("BOLD") = FT_STYLE_FLAG_BOLD; + m.attr("KERNING_DEFAULT") = (int)FT_KERNING_DEFAULT; + m.attr("KERNING_UNFITTED") = (int)FT_KERNING_UNFITTED; + m.attr("KERNING_UNSCALED") = (int)FT_KERNING_UNSCALED; + m.attr("LOAD_DEFAULT") = FT_LOAD_DEFAULT; + m.attr("LOAD_NO_SCALE") = FT_LOAD_NO_SCALE; + m.attr("LOAD_NO_HINTING") = FT_LOAD_NO_HINTING; + m.attr("LOAD_RENDER") = FT_LOAD_RENDER; + m.attr("LOAD_NO_BITMAP") = FT_LOAD_NO_BITMAP; + m.attr("LOAD_VERTICAL_LAYOUT") = FT_LOAD_VERTICAL_LAYOUT; + m.attr("LOAD_FORCE_AUTOHINT") = FT_LOAD_FORCE_AUTOHINT; + m.attr("LOAD_CROP_BITMAP") = FT_LOAD_CROP_BITMAP; + m.attr("LOAD_PEDANTIC") = FT_LOAD_PEDANTIC; + m.attr("LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH") = FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH; + m.attr("LOAD_NO_RECURSE") = FT_LOAD_NO_RECURSE; + m.attr("LOAD_IGNORE_TRANSFORM") = FT_LOAD_IGNORE_TRANSFORM; + m.attr("LOAD_MONOCHROME") = FT_LOAD_MONOCHROME; + m.attr("LOAD_LINEAR_DESIGN") = FT_LOAD_LINEAR_DESIGN; + m.attr("LOAD_NO_AUTOHINT") = (unsigned long)FT_LOAD_NO_AUTOHINT; + m.attr("LOAD_TARGET_NORMAL") = (unsigned long)FT_LOAD_TARGET_NORMAL; + m.attr("LOAD_TARGET_LIGHT") = (unsigned long)FT_LOAD_TARGET_LIGHT; + m.attr("LOAD_TARGET_MONO") = (unsigned long)FT_LOAD_TARGET_MONO; + m.attr("LOAD_TARGET_LCD") = (unsigned long)FT_LOAD_TARGET_LCD; + m.attr("LOAD_TARGET_LCD_V") = (unsigned long)FT_LOAD_TARGET_LCD_V; } diff --git a/src/meson.build b/src/meson.build index a046b3306ab8..4edd8451aad2 100644 --- a/src/meson.build +++ b/src/meson.build @@ -94,7 +94,7 @@ extension_data = { 'py_converters.cpp', ), 'dependencies': [ - freetype_dep, numpy_dep, agg_dep.partial_dependency(includes: true), + freetype_dep, pybind11_dep, numpy_dep, agg_dep.partial_dependency(includes: true), ], 'cpp_args': [ '-DFREETYPE_BUILD_TYPE="@0@"'.format( From a9d4633b6f6583042f432df6bf467390f9e9a1fd Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 7 Sep 2024 03:28:58 -0400 Subject: [PATCH 0233/1230] Simplify FT2Font.get_font Inline `convert_xys_to_array` and modify the arguments to take a C++ container, so we don't need a less-safe pointer, and we don't need to copy another time over. --- src/ft2font.cpp | 10 +++++----- src/ft2font.h | 6 +++--- src/ft2font_wrapper.cpp | 33 +++++++-------------------------- 3 files changed, 15 insertions(+), 34 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 34a602562735..f9700fa2ee7d 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -397,7 +397,7 @@ void FT2Font::set_kerning_factor(int factor) } void FT2Font::set_text( - size_t N, uint32_t *codepoints, double angle, FT_Int32 flags, std::vector &xys) + std::u32string_view text, double angle, FT_Int32 flags, std::vector &xys) { FT_Matrix matrix; /* transformation matrix */ @@ -420,7 +420,7 @@ void FT2Font::set_text( FT_UInt previous = 0; FT2Font *previous_ft_object = NULL; - for (size_t n = 0; n < N; n++) { + for (auto codepoint : text) { FT_UInt glyph_index = 0; FT_BBox glyph_bbox; FT_Pos last_advance; @@ -429,14 +429,14 @@ void FT2Font::set_text( std::set glyph_seen_fonts; FT2Font *ft_object_with_glyph = this; bool was_found = load_char_with_fallback(ft_object_with_glyph, glyph_index, glyphs, - char_to_font, glyph_to_font, codepoints[n], flags, + char_to_font, glyph_to_font, codepoint, flags, charcode_error, glyph_error, glyph_seen_fonts, false); if (!was_found) { - ft_glyph_warn((FT_ULong)codepoints[n], glyph_seen_fonts); + ft_glyph_warn((FT_ULong)codepoint, glyph_seen_fonts); // render missing glyph tofu // come back to top-most font ft_object_with_glyph = this; - char_to_font[codepoints[n]] = ft_object_with_glyph; + char_to_font[codepoint] = ft_object_with_glyph; glyph_to_font[glyph_index] = ft_object_with_glyph; ft_object_with_glyph->load_glyph(glyph_index, flags, ft_object_with_glyph, false); } diff --git a/src/ft2font.h b/src/ft2font.h index 7891c6050341..79b0e1ccc518 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -6,9 +6,9 @@ #ifndef MPL_FT2FONT_H #define MPL_FT2FONT_H -#include #include #include +#include #include #include @@ -77,8 +77,8 @@ class FT2Font void set_size(double ptsize, double dpi); void set_charmap(int i); void select_charmap(unsigned long i); - void set_text( - size_t N, uint32_t *codepoints, double angle, FT_Int32 flags, std::vector &xys); + void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags, + std::vector &xys); int get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, bool fallback); int get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, FT_Vector &delta); void set_kerning_factor(int factor); diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 27ba249ec916..704ffe51c0c0 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -6,7 +6,6 @@ #include "ft2font.h" #include "numpy/arrayobject.h" -#include #include #include #include @@ -14,17 +13,6 @@ namespace py = pybind11; using namespace pybind11::literals; -static py::array_t -convert_xys_to_array(std::vector &xys) -{ - py::ssize_t dims[] = { static_cast(xys.size()) / 2, 2 }; - py::array_t result(dims); - if (xys.size() > 0) { - memcpy(result.mutable_data(), xys.data(), result.nbytes()); - } - return result; -} - /********************************************************************** * FT2Image * */ @@ -346,26 +334,19 @@ const char *PyFT2Font_set_text__doc__ = "A sequence of x,y positions in 26.6 subpixels is returned; divide by 64 for pixels.\n"; static py::array_t -PyFT2Font_set_text(PyFT2Font *self, std::u32string text, double angle = 0.0, +PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0, FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT) { std::vector xys; - std::vector codepoints; - size_t size; - size = text.size(); - codepoints.resize(size); - for (size_t i = 0; i < size; ++i) { - codepoints[i] = text[i]; - } + self->x->set_text(text, angle, flags, xys); - uint32_t* codepoints_array = NULL; - if (size > 0) { - codepoints_array = &codepoints[0]; + py::ssize_t dims[] = { static_cast(xys.size()) / 2, 2 }; + py::array_t result(dims); + if (xys.size() > 0) { + memcpy(result.mutable_data(), xys.data(), result.nbytes()); } - self->x->set_text(size, codepoints_array, angle, flags, xys); - - return convert_xys_to_array(xys); + return result; } const char *PyFT2Font_get_num_glyphs__doc__ = From 1140e149bf3fbb331f4fb5b31e613d1146642d90 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 7 Sep 2024 04:41:09 -0400 Subject: [PATCH 0234/1230] Use STL type for FT2Font fallback list This allows pybind11 to generate the type hints for us, and it also takes care of checking the list and its contents are the right type. --- lib/matplotlib/tests/test_ft2font.py | 4 ++-- src/ft2font_wrapper.cpp | 22 ++++++---------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 1bfa990bd8f5..ace4cea5865e 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -152,10 +152,10 @@ def test_ft2font_invalid_args(tmp_path): with pytest.raises(ValueError, match='hinting_factor must be greater than 0'): ft2font.FT2Font(file, 0) - with pytest.raises(TypeError, match='Fallback list must be a list'): + with pytest.raises(TypeError, match='incompatible constructor arguments'): # failing to be a list will fail before the 0 ft2font.FT2Font(file, _fallback_list=(0,)) # type: ignore[arg-type] - with pytest.raises(TypeError, match='Fallback fonts must be FT2Font objects.'): + with pytest.raises(TypeError, match='incompatible constructor arguments'): ft2font.FT2Font(file, _fallback_list=[0]) # type: ignore[list-item] # kerning_factor argument. diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 704ffe51c0c0..6021e0a17535 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -172,7 +172,8 @@ const char *PyFT2Font_init__doc__ = static PyFT2Font * PyFT2Font_init(py::object filename, long hinting_factor = 8, - py::object fallback_list_or_none = py::none(), int kerning_factor = 0) + std::optional> fallback_list = std::nullopt, + int kerning_factor = 0) { if (hinting_factor <= 0) { throw py::value_error("hinting_factor must be greater than 0"); @@ -192,24 +193,13 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, open_args.stream = &self->stream; std::vector fallback_fonts; - if (!fallback_list_or_none.is_none()) { - if (!py::isinstance(fallback_list_or_none)) { - throw py::type_error("Fallback list must be a list"); - } - auto fallback_list = fallback_list_or_none.cast(); - - // go through fallbacks once to make sure the types are right - for (auto item : fallback_list) { - if (!py::isinstance(item)) { - throw py::type_error("Fallback fonts must be FT2Font objects."); - } - } - // go through a second time to add them to our lists - for (auto item : fallback_list) { + if (fallback_list) { + // go through fallbacks to add them to our lists + for (auto item : *fallback_list) { self->fallbacks.append(item); // Also (locally) cache the underlying FT2Font objects. As long as // the Python objects are kept alive, these pointer are good. - FT2Font *fback = py::cast(item)->x; + FT2Font *fback = item->x; fallback_fonts.push_back(fback); } } From 6ef1b97c928c4641d5c8ea5da5a1e1c45733c2a5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 9 Sep 2024 23:06:47 -0400 Subject: [PATCH 0235/1230] Replace std::to_chars with plain snprintf The former is not available on the macOS deployment target we use for wheels. We could revert back to `PyOS_snprintf`, but C++11 contains `snprintf`, and it seems to guarantee the same things. --- src/ft2font.cpp | 15 +++++++++++---- src/ft2font_wrapper.cpp | 5 ----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index f9700fa2ee7d..dc6bc419d43e 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -1,7 +1,7 @@ /* -*- mode: c++; c-basic-offset: 4 -*- */ #include -#include +#include #include #include #include @@ -727,13 +727,20 @@ void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer, if (!FT_HAS_GLYPH_NAMES(face)) { /* Note that this generated name must match the name that is generated by ttconv in ttfont_CharStrings_getname. */ - buffer.replace(0, 3, "uni"); - std::to_chars(buffer.data() + 3, buffer.data() + buffer.size(), - glyph_number, 16); + auto len = snprintf(buffer.data(), buffer.size(), "uni%08x", glyph_number); + if (len >= 0) { + buffer.resize(len); + } else { + throw std::runtime_error("Failed to convert glyph to standard name"); + } } else { if (FT_Error error = FT_Get_Glyph_Name(face, glyph_number, buffer.data(), buffer.size())) { throw_ft_error("Could not get glyph names", error); } + auto len = buffer.find('\0'); + if (len != buffer.npos) { + buffer.resize(len); + } } } diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 6021e0a17535..9791dc7e2e06 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -486,11 +486,6 @@ PyFT2Font_get_glyph_name(PyFT2Font *self, unsigned int glyph_number) buffer.resize(128); self->x->get_glyph_name(glyph_number, buffer, fallback); - // pybind11 uses the entire string's size(), so trim all the NULLs off the end. - auto len = buffer.find('\0'); - if (len != buffer.npos) { - buffer.resize(len); - } return buffer; } From a0649e792797e16e5c2d84e267c9a848b9905272 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 10 Sep 2024 01:46:18 -0400 Subject: [PATCH 0236/1230] DOC: Hide pybind11 base object from inheritance And also ignore the `numpy.float64` reference. The latter seems to be broken since Sphinx tries to auto-link type hints as `py:class`, but it's an alias in NumPy making it a `py:attr` in their inventory. --- doc/conf.py | 15 +++++++++++++++ doc/missing-references.json | 3 +++ 2 files changed, 18 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index d153aead739c..ea6b1a3fa444 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -230,6 +230,20 @@ def tutorials_download_error(record): autodoc_docstring_signature = True autodoc_default_options = {'members': None, 'undoc-members': None} + +def autodoc_process_bases(app, name, obj, options, bases): + """ + Hide pybind11 base object from inheritance tree. + + Note, *bases* must be modified in place. + """ + for cls in bases[:]: + if not isinstance(cls, type): + continue + if cls.__module__ == 'pybind11_builtins' and cls.__name__ == 'pybind11_object': + bases.remove(cls) + + # make sure to ignore warnings that stem from simply inspecting deprecated # class-level attributes warnings.filterwarnings('ignore', category=DeprecationWarning, @@ -847,5 +861,6 @@ def setup(app): bld_type = 'rel' app.add_config_value('skip_sub_dirs', 0, '') app.add_config_value('releaselevel', bld_type, 'env') + app.connect('autodoc-process-bases', autodoc_process_bases) if sphinx.version_info[:2] < (7, 1): app.connect('html-page-context', add_html_cache_busting, priority=1000) diff --git a/doc/missing-references.json b/doc/missing-references.json index a0eb69308eb4..089434172e58 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -268,6 +268,9 @@ ":1", "doc/api/_as_gen/mpl_toolkits.axisartist.floating_axes.rst:32::1" ], + "numpy.float64": [ + "doc/docstring of matplotlib.ft2font.PyCapsule.set_text:1" + ], "numpy.uint8": [ ":1" ] From a02506ff9503c695b426dcd758f94cc5bd9abde1 Mon Sep 17 00:00:00 2001 From: Moritz Wolter Date: Thu, 12 Sep 2024 10:38:16 +0200 Subject: [PATCH 0237/1230] add brackets to satisfy the new sequence requirement --- galleries/examples/animation/frame_grabbing_sgskip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galleries/examples/animation/frame_grabbing_sgskip.py b/galleries/examples/animation/frame_grabbing_sgskip.py index 08155d2c61a7..9801075e3929 100644 --- a/galleries/examples/animation/frame_grabbing_sgskip.py +++ b/galleries/examples/animation/frame_grabbing_sgskip.py @@ -39,5 +39,5 @@ for i in range(100): x0 += 0.1 * np.random.randn() y0 += 0.1 * np.random.randn() - l.set_data(x0, y0) + l.set_data([x0], [y0]) writer.grab_frame() From 37c66d1c3bee261f0a9c938fbc94af9238646126 Mon Sep 17 00:00:00 2001 From: Moritz Wolter Date: Thu, 12 Sep 2024 10:43:36 +0200 Subject: [PATCH 0238/1230] shorten example description. --- galleries/examples/animation/frame_grabbing_sgskip.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/galleries/examples/animation/frame_grabbing_sgskip.py b/galleries/examples/animation/frame_grabbing_sgskip.py index 9801075e3929..dcc2ca01afd9 100644 --- a/galleries/examples/animation/frame_grabbing_sgskip.py +++ b/galleries/examples/animation/frame_grabbing_sgskip.py @@ -6,8 +6,6 @@ Use a MovieWriter directly to grab individual frames and write them to a file. This avoids any event loop integration, and thus works even with the Agg backend. This is not recommended for use in an interactive setting. - -Output generated via `matplotlib.animation.Animation.to_jshtml`. """ import numpy as np From 7b941c8a924c07215984810649ddee2a4ed51a7a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 12 Sep 2024 05:28:44 -0400 Subject: [PATCH 0239/1230] Backport PR #28805: add brackets to satisfy the new sequence requirement --- galleries/examples/animation/frame_grabbing_sgskip.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/galleries/examples/animation/frame_grabbing_sgskip.py b/galleries/examples/animation/frame_grabbing_sgskip.py index 08155d2c61a7..dcc2ca01afd9 100644 --- a/galleries/examples/animation/frame_grabbing_sgskip.py +++ b/galleries/examples/animation/frame_grabbing_sgskip.py @@ -6,8 +6,6 @@ Use a MovieWriter directly to grab individual frames and write them to a file. This avoids any event loop integration, and thus works even with the Agg backend. This is not recommended for use in an interactive setting. - -Output generated via `matplotlib.animation.Animation.to_jshtml`. """ import numpy as np @@ -39,5 +37,5 @@ for i in range(100): x0 += 0.1 * np.random.randn() y0 += 0.1 * np.random.randn() - l.set_data(x0, y0) + l.set_data([x0], [y0]) writer.grab_frame() From 96654d64dfea78901c9d912aca62c47fd4fb287c Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 12 Sep 2024 05:28:44 -0400 Subject: [PATCH 0240/1230] Backport PR #28805: add brackets to satisfy the new sequence requirement --- galleries/examples/animation/frame_grabbing_sgskip.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/galleries/examples/animation/frame_grabbing_sgskip.py b/galleries/examples/animation/frame_grabbing_sgskip.py index 08155d2c61a7..dcc2ca01afd9 100644 --- a/galleries/examples/animation/frame_grabbing_sgskip.py +++ b/galleries/examples/animation/frame_grabbing_sgskip.py @@ -6,8 +6,6 @@ Use a MovieWriter directly to grab individual frames and write them to a file. This avoids any event loop integration, and thus works even with the Agg backend. This is not recommended for use in an interactive setting. - -Output generated via `matplotlib.animation.Animation.to_jshtml`. """ import numpy as np @@ -39,5 +37,5 @@ for i in range(100): x0 += 0.1 * np.random.randn() y0 += 0.1 * np.random.randn() - l.set_data(x0, y0) + l.set_data([x0], [y0]) writer.grab_frame() From 36af98e371ed4af49b98488385cef3ab88ca2d19 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:40:37 +0200 Subject: [PATCH 0241/1230] DOC: Add a plot to margins() to visualize the effect --- lib/matplotlib/axes/_base.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 52963c1d1ff5..7d72b8caedfa 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2745,6 +2745,34 @@ def margins(self, *margins, x=None, y=None, tight=True): arguments (positional or otherwise) are provided, the current margins will remain unchanged and simply be returned. + .. plot:: + + import numpy as np + import matplotlib.pyplot as plt + + x, y = np.meshgrid(np.linspace(0, 1, 10), np.linspace(0, 1, 10)) + fig, ax = plt.subplots() + ax.plot(x, y, 'o', color='lightblue') + ax.margins(1.5, 0.5) + ax.set_title("margins(x=1.5, y=0.5)") + + def arrow(p1, p2, **props): + ax.annotate("", p1, p2, + arrowprops=dict(arrowstyle="<->", shrinkA=0, shrinkB=0, **props)) + + arrow((-1.5, 0), (0, 0), color="orange") + arrow((0, 0), (1, 0), color="sienna") + arrow((1, 0), (2.5, 0), color="orange") + ax.text(-0.75, -0.1, "x margin * x data range", ha="center", + color="orange") + ax.text(0.5, -0.1, "x data range", ha="center", color="sienna") + + arrow((1, -0.5), (1, 0), color="tab:green") + arrow((1, 0), (1, 1), color="darkgreen") + arrow((1, 1), (1, 1.5), color="tab:green") + ax.text(1.1, 1.25, "y margin * y data range", color="tab:green") + ax.text(1.1, 0.5, "y data range", color="darkgreen") + Specifying any margin changes only the autoscaling; for example, if *xmargin* is not None, then *xmargin* times the X data interval will be added to each end of that interval before From 3b3576d1f47f072f2a343e8042d8a8156b5c32de Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 12 Sep 2024 16:24:48 -0400 Subject: [PATCH 0242/1230] Adding tags to many examples (#28569) * Add example tags to the color examples directory * Add example tags to the pie and polar examples * Add tags to the subplots, axes, and figures examples * Add tags to the lines, bars, and markers examples * Add missing newline at end of joinstyle.py example * Fix typos in tag directives. * Fix rogue capitalization in tag of two_scales.py * Fix typo Co-authored-by: Elliott Sales de Andrade --------- Co-authored-by: Elliott Sales de Andrade --- galleries/examples/color/color_by_yvalue.py | 7 +++++++ galleries/examples/color/color_cycle_default.py | 7 +++++++ galleries/examples/color/color_demo.py | 6 ++++++ galleries/examples/color/colorbar_basics.py | 7 +++++++ galleries/examples/color/colormap_reference.py | 5 +++++ galleries/examples/color/custom_cmap.py | 6 ++++++ galleries/examples/color/individual_colors_from_cmap.py | 7 +++++++ galleries/examples/color/named_colors.py | 5 +++++ galleries/examples/color/set_alpha.py | 6 ++++++ galleries/examples/lines_bars_and_markers/bar_colors.py | 7 +++++++ .../examples/lines_bars_and_markers/bar_label_demo.py | 6 ++++++ galleries/examples/lines_bars_and_markers/bar_stacked.py | 6 ++++++ galleries/examples/lines_bars_and_markers/barchart.py | 6 ++++++ galleries/examples/lines_bars_and_markers/barh.py | 6 ++++++ galleries/examples/lines_bars_and_markers/broken_barh.py | 7 +++++++ galleries/examples/lines_bars_and_markers/capstyle.py | 5 +++++ .../lines_bars_and_markers/categorical_variables.py | 6 ++++++ galleries/examples/lines_bars_and_markers/cohere.py | 7 +++++++ galleries/examples/lines_bars_and_markers/csd_demo.py | 7 +++++++ .../examples/lines_bars_and_markers/curve_error_band.py | 6 ++++++ .../lines_bars_and_markers/errorbar_limits_simple.py | 6 ++++++ .../lines_bars_and_markers/errorbar_subsample.py | 7 +++++++ .../lines_bars_and_markers/eventcollection_demo.py | 6 ++++++ .../examples/lines_bars_and_markers/eventplot_demo.py | 7 +++++++ galleries/examples/lines_bars_and_markers/fill.py | 6 ++++++ .../lines_bars_and_markers/fill_between_alpha.py | 8 ++++++++ .../examples/lines_bars_and_markers/fill_between_demo.py | 7 +++++++ .../lines_bars_and_markers/fill_betweenx_demo.py | 6 ++++++ .../examples/lines_bars_and_markers/gradient_bar.py | 8 ++++++++ galleries/examples/lines_bars_and_markers/hat_graph.py | 6 ++++++ .../horizontal_barchart_distribution.py | 7 +++++++ galleries/examples/lines_bars_and_markers/joinstyle.py | 3 +++ .../lines_bars_and_markers/line_demo_dash_control.py | 7 +++++++ .../lines_bars_and_markers/lines_with_ticks_demo.py | 7 +++++++ galleries/examples/lines_bars_and_markers/linestyles.py | 6 ++++++ .../examples/lines_bars_and_markers/marker_reference.py | 6 ++++++ .../examples/lines_bars_and_markers/markevery_demo.py | 7 +++++++ galleries/examples/lines_bars_and_markers/masked_demo.py | 6 ++++++ .../examples/lines_bars_and_markers/multicolored_line.py | 8 ++++++++ .../lines_bars_and_markers/multivariate_marker_plot.py | 8 ++++++++ galleries/examples/lines_bars_and_markers/psd_demo.py | 7 +++++++ .../examples/lines_bars_and_markers/scatter_demo2.py | 8 ++++++++ .../examples/lines_bars_and_markers/scatter_hist.py | 7 +++++++ .../examples/lines_bars_and_markers/scatter_masked.py | 7 +++++++ .../examples/lines_bars_and_markers/scatter_star_poly.py | 6 ++++++ .../lines_bars_and_markers/scatter_with_legend.py | 6 ++++++ galleries/examples/lines_bars_and_markers/simple_plot.py | 5 +++++ .../examples/lines_bars_and_markers/span_regions.py | 6 ++++++ .../examples/lines_bars_and_markers/spectrum_demo.py | 7 +++++++ .../examples/lines_bars_and_markers/stackplot_demo.py | 6 ++++++ galleries/examples/lines_bars_and_markers/stairs_demo.py | 5 +++++ galleries/examples/lines_bars_and_markers/stem_plot.py | 5 +++++ galleries/examples/lines_bars_and_markers/step_demo.py | 6 ++++++ galleries/examples/lines_bars_and_markers/timeline.py | 6 ++++++ .../examples/lines_bars_and_markers/vline_hline_demo.py | 6 ++++++ .../examples/lines_bars_and_markers/xcorr_acorr_demo.py | 5 +++++ galleries/examples/pie_and_polar_charts/bar_of_pie.py | 8 ++++++++ galleries/examples/pie_and_polar_charts/nested_pie.py | 6 ++++++ .../pie_and_polar_charts/pie_and_donut_labels.py | 7 +++++++ galleries/examples/pie_and_polar_charts/pie_features.py | 5 +++++ galleries/examples/pie_and_polar_charts/polar_bar.py | 7 +++++++ galleries/examples/pie_and_polar_charts/polar_demo.py | 5 +++++ .../examples/pie_and_polar_charts/polar_error_caps.py | 7 +++++++ galleries/examples/pie_and_polar_charts/polar_legend.py | 6 ++++++ galleries/examples/pie_and_polar_charts/polar_scatter.py | 6 ++++++ .../subplots_axes_and_figures/align_labels_demo.py | 8 ++++++++ .../subplots_axes_and_figures/auto_subplots_adjust.py | 7 +++++++ .../subplots_axes_and_figures/axes_box_aspect.py | 6 ++++++ .../examples/subplots_axes_and_figures/axes_demo.py | 8 ++++++++ .../examples/subplots_axes_and_figures/axes_margins.py | 8 ++++++++ .../examples/subplots_axes_and_figures/axes_props.py | 7 +++++++ .../subplots_axes_and_figures/axes_zoom_effect.py | 7 +++++++ .../examples/subplots_axes_and_figures/axhspan_demo.py | 6 ++++++ .../subplots_axes_and_figures/axis_equal_demo.py | 8 ++++++++ .../subplots_axes_and_figures/axis_labels_demo.py | 7 +++++++ .../examples/subplots_axes_and_figures/broken_axis.py | 7 +++++++ .../subplots_axes_and_figures/custom_figure_class.py | 7 +++++++ .../subplots_axes_and_figures/demo_constrained_layout.py | 7 +++++++ .../subplots_axes_and_figures/demo_tight_layout.py | 7 +++++++ .../fahrenheit_celsius_scales.py | 7 +++++++ .../subplots_axes_and_figures/figure_size_units.py | 6 ++++++ .../examples/subplots_axes_and_figures/figure_title.py | 8 ++++++++ .../examples/subplots_axes_and_figures/ganged_plots.py | 7 +++++++ galleries/examples/subplots_axes_and_figures/geo_demo.py | 6 ++++++ .../subplots_axes_and_figures/gridspec_and_subplots.py | 6 ++++++ .../subplots_axes_and_figures/gridspec_multicolumn.py | 6 ++++++ .../subplots_axes_and_figures/gridspec_nested.py | 6 ++++++ .../examples/subplots_axes_and_figures/invert_axes.py | 7 +++++++ .../examples/subplots_axes_and_figures/secondary_axis.py | 6 ++++++ .../subplots_axes_and_figures/share_axis_lims_views.py | 7 +++++++ .../subplots_axes_and_figures/shared_axis_demo.py | 7 +++++++ .../examples/subplots_axes_and_figures/subfigures.py | 7 +++++++ galleries/examples/subplots_axes_and_figures/subplot.py | 7 +++++++ .../subplots_axes_and_figures/subplots_adjust.py | 7 +++++++ .../examples/subplots_axes_and_figures/subplots_demo.py | 9 +++++++++ .../examples/subplots_axes_and_figures/two_scales.py | 6 ++++++ .../subplots_axes_and_figures/zoom_inset_axes.py | 6 ++++++ 97 files changed, 633 insertions(+) diff --git a/galleries/examples/color/color_by_yvalue.py b/galleries/examples/color/color_by_yvalue.py index c9bee252aec4..193f840db39e 100644 --- a/galleries/examples/color/color_by_yvalue.py +++ b/galleries/examples/color/color_by_yvalue.py @@ -30,3 +30,10 @@ # in this example: # # - `matplotlib.axes.Axes.plot` / `matplotlib.pyplot.plot` +# +# .. tags:: +# +# styling: color +# styling: conditional +# plot-type: line +# level: beginner diff --git a/galleries/examples/color/color_cycle_default.py b/galleries/examples/color/color_cycle_default.py index a41ff5f63ff8..16f6634937c0 100644 --- a/galleries/examples/color/color_cycle_default.py +++ b/galleries/examples/color/color_cycle_default.py @@ -50,3 +50,10 @@ # - `matplotlib.axes.Axes.axvline` / `matplotlib.pyplot.axvline` # - `matplotlib.axes.Axes.set_facecolor` # - `matplotlib.figure.Figure.suptitle` +# +# .. tags:: +# +# styling: color +# styling: colormap +# plot-type: line +# level: beginner diff --git a/galleries/examples/color/color_demo.py b/galleries/examples/color/color_demo.py index 8c4b7756cc3e..6822efc3faa7 100644 --- a/galleries/examples/color/color_demo.py +++ b/galleries/examples/color/color_demo.py @@ -75,3 +75,9 @@ # - `matplotlib.axes.Axes.set_xlabel` # - `matplotlib.axes.Axes.set_ylabel` # - `matplotlib.axes.Axes.tick_params` +# +# .. tags:: +# +# styling: color +# plot-type: line +# level: beginner diff --git a/galleries/examples/color/colorbar_basics.py b/galleries/examples/color/colorbar_basics.py index 506789916637..8a35f8ac2b68 100644 --- a/galleries/examples/color/colorbar_basics.py +++ b/galleries/examples/color/colorbar_basics.py @@ -56,3 +56,10 @@ # - `matplotlib.figure.Figure.colorbar` / `matplotlib.pyplot.colorbar` # - `matplotlib.colorbar.Colorbar.minorticks_on` # - `matplotlib.colorbar.Colorbar.minorticks_off` +# +# .. tags:: +# +# component: colorbar +# styling: color +# plot-type: imshow +# level: beginner diff --git a/galleries/examples/color/colormap_reference.py b/galleries/examples/color/colormap_reference.py index 38e91ad25408..6f550161f2e9 100644 --- a/galleries/examples/color/colormap_reference.py +++ b/galleries/examples/color/colormap_reference.py @@ -95,3 +95,8 @@ def plot_color_gradients(cmap_category, cmap_list): # - `matplotlib.axes.Axes.imshow` # - `matplotlib.figure.Figure.text` # - `matplotlib.axes.Axes.set_axis_off` +# +# .. tags:: +# +# styling: colormap +# purpose: reference diff --git a/galleries/examples/color/custom_cmap.py b/galleries/examples/color/custom_cmap.py index 0a73b0c3135a..616ab9f279fd 100644 --- a/galleries/examples/color/custom_cmap.py +++ b/galleries/examples/color/custom_cmap.py @@ -280,3 +280,9 @@ # - `matplotlib.cm` # - `matplotlib.cm.ScalarMappable.set_cmap` # - `matplotlib.cm.ColormapRegistry.register` +# +# .. tags:: +# +# styling: colormap +# plot-type: imshow +# level: intermediate diff --git a/galleries/examples/color/individual_colors_from_cmap.py b/galleries/examples/color/individual_colors_from_cmap.py index 1a14bd6b2ae1..cdd176eb3be1 100644 --- a/galleries/examples/color/individual_colors_from_cmap.py +++ b/galleries/examples/color/individual_colors_from_cmap.py @@ -63,3 +63,10 @@ # # - `matplotlib.colors.Colormap` # - `matplotlib.colors.Colormap.resampled` +# +# .. tags:: +# +# component: colormap +# styling: color +# plot-type: line +# level: intermediate diff --git a/galleries/examples/color/named_colors.py b/galleries/examples/color/named_colors.py index d9a7259da773..a5bcf00cb0cb 100644 --- a/galleries/examples/color/named_colors.py +++ b/galleries/examples/color/named_colors.py @@ -121,3 +121,8 @@ def plot_colortable(colors, *, ncols=4, sort_colors=True): # - `matplotlib.figure.Figure.subplots_adjust` # - `matplotlib.axes.Axes.text` # - `matplotlib.patches.Rectangle` +# +# .. tags:: +# +# styling: color +# purpose: reference diff --git a/galleries/examples/color/set_alpha.py b/galleries/examples/color/set_alpha.py index 4130fe1109ef..b8ba559f5f4a 100644 --- a/galleries/examples/color/set_alpha.py +++ b/galleries/examples/color/set_alpha.py @@ -51,3 +51,9 @@ # # - `matplotlib.axes.Axes.bar` # - `matplotlib.pyplot.subplots` +# +# .. tags:: +# +# styling: color +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/bar_colors.py b/galleries/examples/lines_bars_and_markers/bar_colors.py index f173b50c0672..1692c222957d 100644 --- a/galleries/examples/lines_bars_and_markers/bar_colors.py +++ b/galleries/examples/lines_bars_and_markers/bar_colors.py @@ -24,3 +24,10 @@ ax.legend(title='Fruit color') plt.show() + +# %% +# .. tags:: +# +# styling: color +# plot-style: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/bar_label_demo.py b/galleries/examples/lines_bars_and_markers/bar_label_demo.py index 8393407d1c57..2e43dbb18013 100644 --- a/galleries/examples/lines_bars_and_markers/bar_label_demo.py +++ b/galleries/examples/lines_bars_and_markers/bar_label_demo.py @@ -118,3 +118,9 @@ # - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` # - `matplotlib.axes.Axes.barh` / `matplotlib.pyplot.barh` # - `matplotlib.axes.Axes.bar_label` / `matplotlib.pyplot.bar_label` +# +# .. tags:: +# +# component: label +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/bar_stacked.py b/galleries/examples/lines_bars_and_markers/bar_stacked.py index 81ee305e7072..f1f97e89da13 100644 --- a/galleries/examples/lines_bars_and_markers/bar_stacked.py +++ b/galleries/examples/lines_bars_and_markers/bar_stacked.py @@ -34,3 +34,9 @@ ax.legend(loc="upper right") plt.show() + +# %% +# .. tags:: +# +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/barchart.py b/galleries/examples/lines_bars_and_markers/barchart.py index c533ca2eda37..f2157a89c0cd 100644 --- a/galleries/examples/lines_bars_and_markers/barchart.py +++ b/galleries/examples/lines_bars_and_markers/barchart.py @@ -49,3 +49,9 @@ # # - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` # - `matplotlib.axes.Axes.bar_label` / `matplotlib.pyplot.bar_label` +# +# .. tags:: +# +# component: label +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/barh.py b/galleries/examples/lines_bars_and_markers/barh.py index 4de8bc85d3d5..5493c7456c75 100644 --- a/galleries/examples/lines_bars_and_markers/barh.py +++ b/galleries/examples/lines_bars_and_markers/barh.py @@ -26,3 +26,9 @@ ax.set_title('How fast do you want to go today?') plt.show() + +# %% +# .. tags:: +# +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/broken_barh.py b/galleries/examples/lines_bars_and_markers/broken_barh.py index 6da38f1e465f..e1550385155a 100644 --- a/galleries/examples/lines_bars_and_markers/broken_barh.py +++ b/galleries/examples/lines_bars_and_markers/broken_barh.py @@ -24,3 +24,10 @@ horizontalalignment='right', verticalalignment='top') plt.show() + +# %% +# .. tags:: +# +# component: annotation +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/capstyle.py b/galleries/examples/lines_bars_and_markers/capstyle.py index d17f86c6be58..4927c904caa7 100644 --- a/galleries/examples/lines_bars_and_markers/capstyle.py +++ b/galleries/examples/lines_bars_and_markers/capstyle.py @@ -14,3 +14,8 @@ CapStyle.demo() plt.show() + +# %% +# .. tags:: +# +# purpose: reference diff --git a/galleries/examples/lines_bars_and_markers/categorical_variables.py b/galleries/examples/lines_bars_and_markers/categorical_variables.py index e28dda0dda47..4cceb38fbd4d 100644 --- a/galleries/examples/lines_bars_and_markers/categorical_variables.py +++ b/galleries/examples/lines_bars_and_markers/categorical_variables.py @@ -32,3 +32,9 @@ ax.legend() plt.show() + +# %% +# .. tags:: +# +# plot-type: specialty +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/cohere.py b/galleries/examples/lines_bars_and_markers/cohere.py index 917188292311..f02788ea1d69 100644 --- a/galleries/examples/lines_bars_and_markers/cohere.py +++ b/galleries/examples/lines_bars_and_markers/cohere.py @@ -31,3 +31,10 @@ axs[1].set_ylabel('Coherence') plt.show() + +# %% +# .. tags:: +# +# domain: signal-processing +# plot-type: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/csd_demo.py b/galleries/examples/lines_bars_and_markers/csd_demo.py index 6d7a9746e88e..76d9f0825223 100644 --- a/galleries/examples/lines_bars_and_markers/csd_demo.py +++ b/galleries/examples/lines_bars_and_markers/csd_demo.py @@ -38,3 +38,10 @@ ax2.set_ylabel('CSD (dB)') plt.show() + +# %% +# .. tags:: +# +# domain: signal-processing +# plot-type: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/curve_error_band.py b/galleries/examples/lines_bars_and_markers/curve_error_band.py index 61c73e415163..320d2e710be6 100644 --- a/galleries/examples/lines_bars_and_markers/curve_error_band.py +++ b/galleries/examples/lines_bars_and_markers/curve_error_band.py @@ -85,3 +85,9 @@ def draw_error_band(ax, x, y, err, **kwargs): # # - `matplotlib.patches.PathPatch` # - `matplotlib.path.Path` +# +# .. tags:: +# +# component: error +# plot-type: line +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/errorbar_limits_simple.py b/galleries/examples/lines_bars_and_markers/errorbar_limits_simple.py index aff01eece49a..d9c8375c61fb 100644 --- a/galleries/examples/lines_bars_and_markers/errorbar_limits_simple.py +++ b/galleries/examples/lines_bars_and_markers/errorbar_limits_simple.py @@ -60,3 +60,9 @@ # in this example: # # - `matplotlib.axes.Axes.errorbar` / `matplotlib.pyplot.errorbar` +# +# .. tags:: +# +# component: error +# plot-type: errorbar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/errorbar_subsample.py b/galleries/examples/lines_bars_and_markers/errorbar_subsample.py index e5aa84577231..009286e28ea9 100644 --- a/galleries/examples/lines_bars_and_markers/errorbar_subsample.py +++ b/galleries/examples/lines_bars_and_markers/errorbar_subsample.py @@ -38,3 +38,10 @@ fig.suptitle('Errorbar subsampling') plt.show() + +# %% +# .. tags:: +# +# component: error +# plot-type: errorbar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/eventcollection_demo.py b/galleries/examples/lines_bars_and_markers/eventcollection_demo.py index 18783e1649bc..1aa2fa622812 100644 --- a/galleries/examples/lines_bars_and_markers/eventcollection_demo.py +++ b/galleries/examples/lines_bars_and_markers/eventcollection_demo.py @@ -60,3 +60,9 @@ # display the plot plt.show() + +# %% +# .. tags:: +# +# plot-type: eventplot +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/eventplot_demo.py b/galleries/examples/lines_bars_and_markers/eventplot_demo.py index b76999ef05d5..17797c2f697a 100644 --- a/galleries/examples/lines_bars_and_markers/eventplot_demo.py +++ b/galleries/examples/lines_bars_and_markers/eventplot_demo.py @@ -60,3 +60,10 @@ linelengths=linelengths2, orientation='vertical') plt.show() + +# %% +# .. tags:: +# +# plot-type: eventplot +# level: beginner +# purpose: showcase diff --git a/galleries/examples/lines_bars_and_markers/fill.py b/galleries/examples/lines_bars_and_markers/fill.py index a9cba03c273c..4eba083fa825 100644 --- a/galleries/examples/lines_bars_and_markers/fill.py +++ b/galleries/examples/lines_bars_and_markers/fill.py @@ -85,3 +85,9 @@ def _koch_snowflake_complex(order): # # - `matplotlib.axes.Axes.fill` / `matplotlib.pyplot.fill` # - `matplotlib.axes.Axes.axis` / `matplotlib.pyplot.axis` +# +# .. tags:: +# +# styling: shape +# level: beginner +# purpose: showcase diff --git a/galleries/examples/lines_bars_and_markers/fill_between_alpha.py b/galleries/examples/lines_bars_and_markers/fill_between_alpha.py index 1dadc4309e2e..f462f6bf2428 100644 --- a/galleries/examples/lines_bars_and_markers/fill_between_alpha.py +++ b/galleries/examples/lines_bars_and_markers/fill_between_alpha.py @@ -137,3 +137,11 @@ # :doc:`/gallery/subplots_axes_and_figures/axhspan_demo`. plt.show() + +# %% +# .. tags:: +# +# styling: alpha +# plot-type: fill_between +# level: intermediate +# purpose: showcase diff --git a/galleries/examples/lines_bars_and_markers/fill_between_demo.py b/galleries/examples/lines_bars_and_markers/fill_between_demo.py index 5afdd722360f..feb325a3f9db 100644 --- a/galleries/examples/lines_bars_and_markers/fill_between_demo.py +++ b/galleries/examples/lines_bars_and_markers/fill_between_demo.py @@ -139,3 +139,10 @@ # # - `matplotlib.axes.Axes.fill_between` / `matplotlib.pyplot.fill_between` # - `matplotlib.axes.Axes.get_xaxis_transform` +# +# .. tags:: +# +# styling: conditional +# plot-type: fill_between +# level: beginner +# purpose: showcase diff --git a/galleries/examples/lines_bars_and_markers/fill_betweenx_demo.py b/galleries/examples/lines_bars_and_markers/fill_betweenx_demo.py index 472f42fdbfc4..ebd8d2a24a7b 100644 --- a/galleries/examples/lines_bars_and_markers/fill_betweenx_demo.py +++ b/galleries/examples/lines_bars_and_markers/fill_betweenx_demo.py @@ -52,3 +52,9 @@ # would be to interpolate all arrays to a very fine grid before plotting. plt.show() + +# %% +# .. tags:: +# +# plot-type: fill_between +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/gradient_bar.py b/galleries/examples/lines_bars_and_markers/gradient_bar.py index 4cd86f26590f..2e9e2c8aa4aa 100644 --- a/galleries/examples/lines_bars_and_markers/gradient_bar.py +++ b/galleries/examples/lines_bars_and_markers/gradient_bar.py @@ -71,3 +71,11 @@ def gradient_bar(ax, x, y, width=0.5, bottom=0): y = np.random.rand(N) gradient_bar(ax, x, y, width=0.7) plt.show() + +# %% +# .. tags:: +# +# styling: color +# plot-type: imshow +# level: intermediate +# purpose: showcase diff --git a/galleries/examples/lines_bars_and_markers/hat_graph.py b/galleries/examples/lines_bars_and_markers/hat_graph.py index 0091c3c95d51..0f6d934f1cb1 100644 --- a/galleries/examples/lines_bars_and_markers/hat_graph.py +++ b/galleries/examples/lines_bars_and_markers/hat_graph.py @@ -78,3 +78,9 @@ def label_bars(heights, rects): # # - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` # - `matplotlib.axes.Axes.annotate` / `matplotlib.pyplot.annotate` +# +# .. tags:: +# +# component: annotate +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/horizontal_barchart_distribution.py b/galleries/examples/lines_bars_and_markers/horizontal_barchart_distribution.py index ae638a90c3fd..3f7e499f38e7 100644 --- a/galleries/examples/lines_bars_and_markers/horizontal_barchart_distribution.py +++ b/galleries/examples/lines_bars_and_markers/horizontal_barchart_distribution.py @@ -78,3 +78,10 @@ def survey(results, category_names): # - `matplotlib.axes.Axes.barh` / `matplotlib.pyplot.barh` # - `matplotlib.axes.Axes.bar_label` / `matplotlib.pyplot.bar_label` # - `matplotlib.axes.Axes.legend` / `matplotlib.pyplot.legend` +# +# .. tags:: +# +# domain: statistics +# component: label +# plot-type: bar +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/joinstyle.py b/galleries/examples/lines_bars_and_markers/joinstyle.py index 27ad9ede96b1..09ae03e07692 100644 --- a/galleries/examples/lines_bars_and_markers/joinstyle.py +++ b/galleries/examples/lines_bars_and_markers/joinstyle.py @@ -14,3 +14,6 @@ JoinStyle.demo() plt.show() + +# %% +# .. tags:: purpose: reference, styling: linestyle diff --git a/galleries/examples/lines_bars_and_markers/line_demo_dash_control.py b/galleries/examples/lines_bars_and_markers/line_demo_dash_control.py index 5952809125de..3b3880794d3d 100644 --- a/galleries/examples/lines_bars_and_markers/line_demo_dash_control.py +++ b/galleries/examples/lines_bars_and_markers/line_demo_dash_control.py @@ -47,3 +47,10 @@ ax.legend(handlelength=4) plt.show() + +# %% +# .. tags:: +# +# styling: linestyle +# plot-style: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/lines_with_ticks_demo.py b/galleries/examples/lines_bars_and_markers/lines_with_ticks_demo.py index 00776d89caff..fba7eb9f045e 100644 --- a/galleries/examples/lines_bars_and_markers/lines_with_ticks_demo.py +++ b/galleries/examples/lines_bars_and_markers/lines_with_ticks_demo.py @@ -30,3 +30,10 @@ ax.legend() plt.show() + +# %% +# .. tags:: +# +# styling: linestyle +# plot-type: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/linestyles.py b/galleries/examples/lines_bars_and_markers/linestyles.py index f3ce3aa6e7ec..9f1829670c65 100644 --- a/galleries/examples/lines_bars_and_markers/linestyles.py +++ b/galleries/examples/lines_bars_and_markers/linestyles.py @@ -73,3 +73,9 @@ def plot_linestyles(ax, linestyles, title): plt.tight_layout() plt.show() + +# %% +# .. tags:: +# +# styling: linestyle +# purpose: reference diff --git a/galleries/examples/lines_bars_and_markers/marker_reference.py b/galleries/examples/lines_bars_and_markers/marker_reference.py index f99afb08e143..32b6291cbc76 100644 --- a/galleries/examples/lines_bars_and_markers/marker_reference.py +++ b/galleries/examples/lines_bars_and_markers/marker_reference.py @@ -240,3 +240,9 @@ def split_list(a_list): format_axes(ax) plt.show() + +# %% +# .. tags:: +# +# component: marker +# purpose: reference diff --git a/galleries/examples/lines_bars_and_markers/markevery_demo.py b/galleries/examples/lines_bars_and_markers/markevery_demo.py index cc02fb5ee576..919e12cde952 100644 --- a/galleries/examples/lines_bars_and_markers/markevery_demo.py +++ b/galleries/examples/lines_bars_and_markers/markevery_demo.py @@ -96,3 +96,10 @@ ax.plot(theta, r, 'o', ls='-', ms=4, markevery=markevery) plt.show() + +# %% +# .. tags:: +# +# component: marker +# plot-type: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/masked_demo.py b/galleries/examples/lines_bars_and_markers/masked_demo.py index a0b63ae30fe4..842c5c022f0b 100644 --- a/galleries/examples/lines_bars_and_markers/masked_demo.py +++ b/galleries/examples/lines_bars_and_markers/masked_demo.py @@ -48,3 +48,9 @@ plt.legend() plt.title('Masked and NaN data') plt.show() + +# %% +# .. tags:: +# +# plot-type: line +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/multicolored_line.py b/galleries/examples/lines_bars_and_markers/multicolored_line.py index 3d14ecaf8567..8c72d28e9e67 100644 --- a/galleries/examples/lines_bars_and_markers/multicolored_line.py +++ b/galleries/examples/lines_bars_and_markers/multicolored_line.py @@ -188,3 +188,11 @@ def colored_line_between_pts(x, y, c, ax, **lc_kwargs): ax2.set_title("Color between points") plt.show() + +# %% +# .. tags:: +# +# styling: color +# styling: linestyle +# plot-type: line +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/multivariate_marker_plot.py b/galleries/examples/lines_bars_and_markers/multivariate_marker_plot.py index 13609422e690..1f149c030abe 100644 --- a/galleries/examples/lines_bars_and_markers/multivariate_marker_plot.py +++ b/galleries/examples/lines_bars_and_markers/multivariate_marker_plot.py @@ -45,3 +45,11 @@ ax.set_ylabel("Y position [m]") plt.show() + +# %% +# .. tags:: +# +# component: marker +# styling: color +# level: beginner +# purpose: fun diff --git a/galleries/examples/lines_bars_and_markers/psd_demo.py b/galleries/examples/lines_bars_and_markers/psd_demo.py index fa0a8565b6ff..edbfc79289af 100644 --- a/galleries/examples/lines_bars_and_markers/psd_demo.py +++ b/galleries/examples/lines_bars_and_markers/psd_demo.py @@ -178,3 +178,10 @@ ax1.set_ylim(yrange) plt.show() + +# %% +# .. tags:: +# +# domain: signal-processing +# plot-type: line +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/scatter_demo2.py b/galleries/examples/lines_bars_and_markers/scatter_demo2.py index c3d57c423d69..33e532bb9af9 100644 --- a/galleries/examples/lines_bars_and_markers/scatter_demo2.py +++ b/galleries/examples/lines_bars_and_markers/scatter_demo2.py @@ -34,3 +34,11 @@ fig.tight_layout() plt.show() + +# %% +# .. tags:: +# +# component: marker +# component: color +# plot-style: scatter +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/scatter_hist.py b/galleries/examples/lines_bars_and_markers/scatter_hist.py index 95a373961aa1..505edf6d627f 100644 --- a/galleries/examples/lines_bars_and_markers/scatter_hist.py +++ b/galleries/examples/lines_bars_and_markers/scatter_hist.py @@ -120,3 +120,10 @@ def scatter_hist(x, y, ax, ax_histx, ax_histy): # - `matplotlib.axes.Axes.inset_axes` # - `matplotlib.axes.Axes.scatter` # - `matplotlib.axes.Axes.hist` +# +# .. tags:: +# +# component: axes +# plot-type: scatter +# plot-type: histogram +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/scatter_masked.py b/galleries/examples/lines_bars_and_markers/scatter_masked.py index 2bf6e03a46d0..97132e85192e 100644 --- a/galleries/examples/lines_bars_and_markers/scatter_masked.py +++ b/galleries/examples/lines_bars_and_markers/scatter_masked.py @@ -30,3 +30,10 @@ plt.plot(r0 * np.cos(theta), r0 * np.sin(theta)) plt.show() + +# %% +# .. tags:: +# +# component: marker +# plot-type: scatter +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/scatter_star_poly.py b/galleries/examples/lines_bars_and_markers/scatter_star_poly.py index d97408333455..c2fee968ad7b 100644 --- a/galleries/examples/lines_bars_and_markers/scatter_star_poly.py +++ b/galleries/examples/lines_bars_and_markers/scatter_star_poly.py @@ -51,3 +51,9 @@ axs[1, 2].set_title("marker=(5, 2)") plt.show() + +# %% +# .. tags:: +# +# component: marker +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/scatter_with_legend.py b/galleries/examples/lines_bars_and_markers/scatter_with_legend.py index 5241e3ef1508..e9f19981fe4a 100644 --- a/galleries/examples/lines_bars_and_markers/scatter_with_legend.py +++ b/galleries/examples/lines_bars_and_markers/scatter_with_legend.py @@ -109,3 +109,9 @@ # - `matplotlib.axes.Axes.scatter` / `matplotlib.pyplot.scatter` # - `matplotlib.axes.Axes.legend` / `matplotlib.pyplot.legend` # - `matplotlib.collections.PathCollection.legend_elements` +# +# .. tags:: +# +# component: legend +# plot-type: scatter +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/simple_plot.py b/galleries/examples/lines_bars_and_markers/simple_plot.py index c8182035fc41..bcac888b8c4a 100644 --- a/galleries/examples/lines_bars_and_markers/simple_plot.py +++ b/galleries/examples/lines_bars_and_markers/simple_plot.py @@ -33,3 +33,8 @@ # - `matplotlib.axes.Axes.plot` / `matplotlib.pyplot.plot` # - `matplotlib.pyplot.subplots` # - `matplotlib.figure.Figure.savefig` +# +# .. tags:: +# +# plot-style: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/span_regions.py b/galleries/examples/lines_bars_and_markers/span_regions.py index e73b1af47baa..8128bd3b865b 100644 --- a/galleries/examples/lines_bars_and_markers/span_regions.py +++ b/galleries/examples/lines_bars_and_markers/span_regions.py @@ -29,3 +29,9 @@ # in this example: # # - `matplotlib.axes.Axes.fill_between` +# +# .. tags:: +# +# styling: conditional +# plot-style: fill_between +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/spectrum_demo.py b/galleries/examples/lines_bars_and_markers/spectrum_demo.py index 147d802b6eff..57706e22be9d 100644 --- a/galleries/examples/lines_bars_and_markers/spectrum_demo.py +++ b/galleries/examples/lines_bars_and_markers/spectrum_demo.py @@ -49,3 +49,10 @@ axs["angle"].angle_spectrum(s, Fs=Fs, color='C2') plt.show() + +# %% +# .. tags:: +# +# domain: signal-processing +# plot-type: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/stackplot_demo.py b/galleries/examples/lines_bars_and_markers/stackplot_demo.py index d02a9af73da3..2ed52ed9a2ce 100644 --- a/galleries/examples/lines_bars_and_markers/stackplot_demo.py +++ b/galleries/examples/lines_bars_and_markers/stackplot_demo.py @@ -73,3 +73,9 @@ def add_random_gaussian(a): fig, ax = plt.subplots() ax.stackplot(x, ys, baseline='wiggle') plt.show() + +# %% +# .. tags:: +# +# plot-type: stackplot +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/stairs_demo.py b/galleries/examples/lines_bars_and_markers/stairs_demo.py index 223e8c2aa1e5..9c7506e52b27 100644 --- a/galleries/examples/lines_bars_and_markers/stairs_demo.py +++ b/galleries/examples/lines_bars_and_markers/stairs_demo.py @@ -90,3 +90,8 @@ # # - `matplotlib.axes.Axes.stairs` / `matplotlib.pyplot.stairs` # - `matplotlib.patches.StepPatch` +# +# .. tags:: +# +# plot-type: stairs +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/stem_plot.py b/galleries/examples/lines_bars_and_markers/stem_plot.py index d779197e50cc..cde8fd8e8017 100644 --- a/galleries/examples/lines_bars_and_markers/stem_plot.py +++ b/galleries/examples/lines_bars_and_markers/stem_plot.py @@ -36,3 +36,8 @@ # in this example: # # - `matplotlib.axes.Axes.stem` / `matplotlib.pyplot.stem` +# +# .. tags:: +# +# plot-type: stem +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/step_demo.py b/galleries/examples/lines_bars_and_markers/step_demo.py index 97d2a37eb4c6..f74a069e52f3 100644 --- a/galleries/examples/lines_bars_and_markers/step_demo.py +++ b/galleries/examples/lines_bars_and_markers/step_demo.py @@ -63,3 +63,9 @@ # # - `matplotlib.axes.Axes.step` / `matplotlib.pyplot.step` # - `matplotlib.axes.Axes.plot` / `matplotlib.pyplot.plot` +# +# .. tags:: +# +# plot-type: step +# plot-type: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/timeline.py b/galleries/examples/lines_bars_and_markers/timeline.py index b7f8ec57b1cc..55f90d5103d2 100644 --- a/galleries/examples/lines_bars_and_markers/timeline.py +++ b/galleries/examples/lines_bars_and_markers/timeline.py @@ -128,3 +128,9 @@ def is_feature(release): # - `matplotlib.axis.Axis.set_major_formatter` # - `matplotlib.dates.MonthLocator` # - `matplotlib.dates.DateFormatter` +# +# .. tags:: +# +# component: annotate +# plot-type: line +# level: intermediate diff --git a/galleries/examples/lines_bars_and_markers/vline_hline_demo.py b/galleries/examples/lines_bars_and_markers/vline_hline_demo.py index c2f5d025b15c..4bec4be760ee 100644 --- a/galleries/examples/lines_bars_and_markers/vline_hline_demo.py +++ b/galleries/examples/lines_bars_and_markers/vline_hline_demo.py @@ -32,3 +32,9 @@ hax.set_title('Horizontal lines demo') plt.show() + +# %% +# .. tags:: +# +# plot-type: line +# level: beginner diff --git a/galleries/examples/lines_bars_and_markers/xcorr_acorr_demo.py b/galleries/examples/lines_bars_and_markers/xcorr_acorr_demo.py index eff0d7269a49..7878ef8d7468 100644 --- a/galleries/examples/lines_bars_and_markers/xcorr_acorr_demo.py +++ b/galleries/examples/lines_bars_and_markers/xcorr_acorr_demo.py @@ -34,3 +34,8 @@ # # - `matplotlib.axes.Axes.acorr` / `matplotlib.pyplot.acorr` # - `matplotlib.axes.Axes.xcorr` / `matplotlib.pyplot.xcorr` +# +# .. tags:: +# +# domain: statistics +# level: beginner diff --git a/galleries/examples/pie_and_polar_charts/bar_of_pie.py b/galleries/examples/pie_and_polar_charts/bar_of_pie.py index ef68b3d79971..6f18b964cef7 100644 --- a/galleries/examples/pie_and_polar_charts/bar_of_pie.py +++ b/galleries/examples/pie_and_polar_charts/bar_of_pie.py @@ -81,3 +81,11 @@ # - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` # - `matplotlib.axes.Axes.pie` / `matplotlib.pyplot.pie` # - `matplotlib.patches.ConnectionPatch` +# +# .. tags:: +# +# component: subplot +# plot-type: pie +# plot-type: bar +# level: intermediate +# purpose: showcase diff --git a/galleries/examples/pie_and_polar_charts/nested_pie.py b/galleries/examples/pie_and_polar_charts/nested_pie.py index c83b4f6f84ee..412299bb4446 100644 --- a/galleries/examples/pie_and_polar_charts/nested_pie.py +++ b/galleries/examples/pie_and_polar_charts/nested_pie.py @@ -90,3 +90,9 @@ # - `matplotlib.projections.polar` # - ``Axes.set`` (`matplotlib.artist.Artist.set`) # - `matplotlib.axes.Axes.set_axis_off` +# +# .. tags:: +# +# plot-type: pie +# level: beginner +# purpose: showcase diff --git a/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py b/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py index 7f945d1056f4..13e3019bc7ba 100644 --- a/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py +++ b/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py @@ -132,3 +132,10 @@ def func(pct, allvals): # # - `matplotlib.axes.Axes.pie` / `matplotlib.pyplot.pie` # - `matplotlib.axes.Axes.legend` / `matplotlib.pyplot.legend` +# +# .. tags:: +# +# component: label +# component: annotation +# plot-type: pie +# level: beginner diff --git a/galleries/examples/pie_and_polar_charts/pie_features.py b/galleries/examples/pie_and_polar_charts/pie_features.py index 7794a3d22a7e..47781a31a373 100644 --- a/galleries/examples/pie_and_polar_charts/pie_features.py +++ b/galleries/examples/pie_and_polar_charts/pie_features.py @@ -130,3 +130,8 @@ # in this example: # # - `matplotlib.axes.Axes.pie` / `matplotlib.pyplot.pie` +# +# .. tags:: +# +# plot-type: pie +# level: beginner diff --git a/galleries/examples/pie_and_polar_charts/polar_bar.py b/galleries/examples/pie_and_polar_charts/polar_bar.py index 750032c8710d..ba0a3c25fd40 100644 --- a/galleries/examples/pie_and_polar_charts/polar_bar.py +++ b/galleries/examples/pie_and_polar_charts/polar_bar.py @@ -32,3 +32,10 @@ # # - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` # - `matplotlib.projections.polar` +# +# .. tags:: +# +# plot-type: pie +# plot-type: bar +# level: beginner +# purpose: showcase diff --git a/galleries/examples/pie_and_polar_charts/polar_demo.py b/galleries/examples/pie_and_polar_charts/polar_demo.py index 75a7d61f6244..e4967079d19d 100644 --- a/galleries/examples/pie_and_polar_charts/polar_demo.py +++ b/galleries/examples/pie_and_polar_charts/polar_demo.py @@ -34,3 +34,8 @@ # - `matplotlib.projections.polar.PolarAxes.set_rticks` # - `matplotlib.projections.polar.PolarAxes.set_rmax` # - `matplotlib.projections.polar.PolarAxes.set_rlabel_position` +# +# .. tags:: +# +# plot-type: polar +# level: beginner diff --git a/galleries/examples/pie_and_polar_charts/polar_error_caps.py b/galleries/examples/pie_and_polar_charts/polar_error_caps.py index aa950e40613a..7f77a2c48834 100644 --- a/galleries/examples/pie_and_polar_charts/polar_error_caps.py +++ b/galleries/examples/pie_and_polar_charts/polar_error_caps.py @@ -51,3 +51,10 @@ # # - `matplotlib.axes.Axes.errorbar` / `matplotlib.pyplot.errorbar` # - `matplotlib.projections.polar` +# +# .. tags:: +# +# component: error +# plot-type: errorbar +# plot-type: polar +# level: beginner diff --git a/galleries/examples/pie_and_polar_charts/polar_legend.py b/galleries/examples/pie_and_polar_charts/polar_legend.py index 7972b0aaffd4..cef4bc8ccef6 100644 --- a/galleries/examples/pie_and_polar_charts/polar_legend.py +++ b/galleries/examples/pie_and_polar_charts/polar_legend.py @@ -38,3 +38,9 @@ # - `matplotlib.axes.Axes.legend` / `matplotlib.pyplot.legend` # - `matplotlib.projections.polar` # - `matplotlib.projections.polar.PolarAxes` +# +# .. tags:: +# +# component: legend +# plot-type: polar +# level: beginner diff --git a/galleries/examples/pie_and_polar_charts/polar_scatter.py b/galleries/examples/pie_and_polar_charts/polar_scatter.py index c36d74966805..af7dff04f195 100644 --- a/galleries/examples/pie_and_polar_charts/polar_scatter.py +++ b/galleries/examples/pie_and_polar_charts/polar_scatter.py @@ -67,3 +67,9 @@ # - `matplotlib.projections.polar.PolarAxes.set_theta_zero_location` # - `matplotlib.projections.polar.PolarAxes.set_thetamin` # - `matplotlib.projections.polar.PolarAxes.set_thetamax` +# +# .. tags:: +# +# plot-style: polar +# plot-style: scatter +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/align_labels_demo.py b/galleries/examples/subplots_axes_and_figures/align_labels_demo.py index 8e9a70d4ccd9..abb048ba395c 100644 --- a/galleries/examples/subplots_axes_and_figures/align_labels_demo.py +++ b/galleries/examples/subplots_axes_and_figures/align_labels_demo.py @@ -41,3 +41,11 @@ fig.align_titles() plt.show() + +# %% +# .. tags:: +# +# component: label +# component: title +# styling: position +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/auto_subplots_adjust.py b/galleries/examples/subplots_axes_and_figures/auto_subplots_adjust.py index 983a47e4e42c..ec865798d648 100644 --- a/galleries/examples/subplots_axes_and_figures/auto_subplots_adjust.py +++ b/galleries/examples/subplots_axes_and_figures/auto_subplots_adjust.py @@ -85,3 +85,10 @@ def on_draw(event): # - `matplotlib.figure.Figure.subplots_adjust` # - `matplotlib.gridspec.SubplotParams` # - `matplotlib.backend_bases.FigureCanvasBase.mpl_connect` +# +# .. tags:: +# +# component: subplot +# plot-type: line +# styling: position +# level: advanced diff --git a/galleries/examples/subplots_axes_and_figures/axes_box_aspect.py b/galleries/examples/subplots_axes_and_figures/axes_box_aspect.py index 74b64f72c466..e17f21e7d41b 100644 --- a/galleries/examples/subplots_axes_and_figures/axes_box_aspect.py +++ b/galleries/examples/subplots_axes_and_figures/axes_box_aspect.py @@ -154,3 +154,9 @@ # in this example: # # - `matplotlib.axes.Axes.set_box_aspect` +# +# .. tags:: +# +# component: axes +# styling: size +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/axes_demo.py b/galleries/examples/subplots_axes_and_figures/axes_demo.py index f5620a9a980d..07f3ca2070c2 100644 --- a/galleries/examples/subplots_axes_and_figures/axes_demo.py +++ b/galleries/examples/subplots_axes_and_figures/axes_demo.py @@ -43,3 +43,11 @@ left_inset_ax.set(title='Impulse response', xlim=(0, .2), xticks=[], yticks=[]) plt.show() + +# %% +# .. tags:: +# +# component: axes +# plot-type: line +# plot-type: histogram +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/axes_margins.py b/galleries/examples/subplots_axes_and_figures/axes_margins.py index dd113c8c34e0..30298168c8e8 100644 --- a/galleries/examples/subplots_axes_and_figures/axes_margins.py +++ b/galleries/examples/subplots_axes_and_figures/axes_margins.py @@ -86,3 +86,11 @@ def f(t): # - `matplotlib.axes.Axes.use_sticky_edges` # - `matplotlib.axes.Axes.pcolor` / `matplotlib.pyplot.pcolor` # - `matplotlib.patches.Polygon` +# +# .. tags:: +# +# component: axes +# plot-type: line +# plot-type: imshow +# plot-type: pcolor +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/axes_props.py b/galleries/examples/subplots_axes_and_figures/axes_props.py index 106c8e0db1ee..6bbcc88ad5b8 100644 --- a/galleries/examples/subplots_axes_and_figures/axes_props.py +++ b/galleries/examples/subplots_axes_and_figures/axes_props.py @@ -19,3 +19,10 @@ ax.tick_params(labelcolor='r', labelsize='medium', width=3) plt.show() + +# %% +# .. tags:: +# +# component: ticks +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/axes_zoom_effect.py b/galleries/examples/subplots_axes_and_figures/axes_zoom_effect.py index f139d0209427..c8d09de45888 100644 --- a/galleries/examples/subplots_axes_and_figures/axes_zoom_effect.py +++ b/galleries/examples/subplots_axes_and_figures/axes_zoom_effect.py @@ -120,3 +120,10 @@ def zoom_effect02(ax1, ax2, **kwargs): zoom_effect02(axs["zoom2"], axs["main"]) plt.show() + +# %% +# .. tags:: +# +# component: subplot +# component: transform +# level: advanced diff --git a/galleries/examples/subplots_axes_and_figures/axhspan_demo.py b/galleries/examples/subplots_axes_and_figures/axhspan_demo.py index 5544618016d6..971c6002ee71 100644 --- a/galleries/examples/subplots_axes_and_figures/axhspan_demo.py +++ b/galleries/examples/subplots_axes_and_figures/axhspan_demo.py @@ -47,3 +47,9 @@ # .. seealso:: # # `~.Axes.axhline`, `~.Axes.axvline`, `~.Axes.axline` draw infinite lines. +# +# .. tags:: +# +# styling: shape +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/axis_equal_demo.py b/galleries/examples/subplots_axes_and_figures/axis_equal_demo.py index 6ac4d66da0e8..046af386ae59 100644 --- a/galleries/examples/subplots_axes_and_figures/axis_equal_demo.py +++ b/galleries/examples/subplots_axes_and_figures/axis_equal_demo.py @@ -33,3 +33,11 @@ fig.tight_layout() plt.show() + +# %% +# .. tags:: +# +# component: axes +# styling: size +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/axis_labels_demo.py b/galleries/examples/subplots_axes_and_figures/axis_labels_demo.py index ea99b78d8fb0..2d0bc427b1f9 100644 --- a/galleries/examples/subplots_axes_and_figures/axis_labels_demo.py +++ b/galleries/examples/subplots_axes_and_figures/axis_labels_demo.py @@ -18,3 +18,10 @@ cbar.set_label("ZLabel", loc='top') plt.show() + +# %% +# .. tags:: +# +# component: axis +# styling: position +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/broken_axis.py b/galleries/examples/subplots_axes_and_figures/broken_axis.py index 4d6ece305ed6..6305e613e327 100644 --- a/galleries/examples/subplots_axes_and_figures/broken_axis.py +++ b/galleries/examples/subplots_axes_and_figures/broken_axis.py @@ -52,3 +52,10 @@ plt.show() + +# %% +# .. tags:: +# +# component: axis +# plot-type: line +# level: intermediate diff --git a/galleries/examples/subplots_axes_and_figures/custom_figure_class.py b/galleries/examples/subplots_axes_and_figures/custom_figure_class.py index 96c7f1113787..328447062a5b 100644 --- a/galleries/examples/subplots_axes_and_figures/custom_figure_class.py +++ b/galleries/examples/subplots_axes_and_figures/custom_figure_class.py @@ -50,3 +50,10 @@ def __init__(self, *args, watermark=None, **kwargs): # - `matplotlib.pyplot.figure` # - `matplotlib.figure.Figure` # - `matplotlib.figure.Figure.text` +# +# .. tags:: +# +# component: figure +# plot-type: line +# level: intermediate +# purpose: showcase diff --git a/galleries/examples/subplots_axes_and_figures/demo_constrained_layout.py b/galleries/examples/subplots_axes_and_figures/demo_constrained_layout.py index 67891cfed611..b3a59ce048c0 100644 --- a/galleries/examples/subplots_axes_and_figures/demo_constrained_layout.py +++ b/galleries/examples/subplots_axes_and_figures/demo_constrained_layout.py @@ -69,3 +69,10 @@ def example_plot(ax): # # - `matplotlib.gridspec.GridSpec` # - `matplotlib.gridspec.GridSpecFromSubplotSpec` +# +# .. tags:: +# +# component: axes +# component: subplot +# styling: size +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/demo_tight_layout.py b/galleries/examples/subplots_axes_and_figures/demo_tight_layout.py index a8d7524697ea..4ac0f1b99dfc 100644 --- a/galleries/examples/subplots_axes_and_figures/demo_tight_layout.py +++ b/galleries/examples/subplots_axes_and_figures/demo_tight_layout.py @@ -132,3 +132,10 @@ def example_plot(ax): # - `matplotlib.figure.Figure.add_gridspec` # - `matplotlib.figure.Figure.add_subplot` # - `matplotlib.pyplot.subplot2grid` +# +# .. tags:: +# +# component: axes +# component: subplot +# styling: size +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/fahrenheit_celsius_scales.py b/galleries/examples/subplots_axes_and_figures/fahrenheit_celsius_scales.py index 216641657b06..95b92482d5ac 100644 --- a/galleries/examples/subplots_axes_and_figures/fahrenheit_celsius_scales.py +++ b/galleries/examples/subplots_axes_and_figures/fahrenheit_celsius_scales.py @@ -44,3 +44,10 @@ def convert_ax_c_to_celsius(ax_f): plt.show() make_plot() + +# %% +# .. tags:: +# +# component: axes +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/figure_size_units.py b/galleries/examples/subplots_axes_and_figures/figure_size_units.py index 0ce49c937d50..50292ef92b74 100644 --- a/galleries/examples/subplots_axes_and_figures/figure_size_units.py +++ b/galleries/examples/subplots_axes_and_figures/figure_size_units.py @@ -79,3 +79,9 @@ # - `matplotlib.pyplot.figure` # - `matplotlib.pyplot.subplots` # - `matplotlib.pyplot.subplot_mosaic` +# +# .. tags:: +# +# component: figure +# styling: size +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/figure_title.py b/galleries/examples/subplots_axes_and_figures/figure_title.py index 85e5044c4eba..1b0eb1a00b23 100644 --- a/galleries/examples/subplots_axes_and_figures/figure_title.py +++ b/galleries/examples/subplots_axes_and_figures/figure_title.py @@ -51,3 +51,11 @@ fig.supylabel('Stock price relative to max') plt.show() + +# %% +# .. tags:: +# +# component: figure +# component: title +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/ganged_plots.py b/galleries/examples/subplots_axes_and_figures/ganged_plots.py index d2f50fe2e986..3229d64a15b4 100644 --- a/galleries/examples/subplots_axes_and_figures/ganged_plots.py +++ b/galleries/examples/subplots_axes_and_figures/ganged_plots.py @@ -38,3 +38,10 @@ axs[2].set_ylim(-1, 1) plt.show() + +# %% +# .. tags:: +# +# component: subplot +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/geo_demo.py b/galleries/examples/subplots_axes_and_figures/geo_demo.py index f2f22751b215..02680c1fd692 100644 --- a/galleries/examples/subplots_axes_and_figures/geo_demo.py +++ b/galleries/examples/subplots_axes_and_figures/geo_demo.py @@ -40,3 +40,9 @@ plt.grid(True) plt.show() + +# %% +# .. tags:: +# +# plot-type: specialty +# domain: cartography diff --git a/galleries/examples/subplots_axes_and_figures/gridspec_and_subplots.py b/galleries/examples/subplots_axes_and_figures/gridspec_and_subplots.py index cfe5b123e897..9996bde9306a 100644 --- a/galleries/examples/subplots_axes_and_figures/gridspec_and_subplots.py +++ b/galleries/examples/subplots_axes_and_figures/gridspec_and_subplots.py @@ -28,3 +28,9 @@ fig.tight_layout() plt.show() + +# %% +# .. tags:: +# +# component: subplot +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/gridspec_multicolumn.py b/galleries/examples/subplots_axes_and_figures/gridspec_multicolumn.py index a7fa34a10367..3762dec4fdb8 100644 --- a/galleries/examples/subplots_axes_and_figures/gridspec_multicolumn.py +++ b/galleries/examples/subplots_axes_and_figures/gridspec_multicolumn.py @@ -32,3 +32,9 @@ def format_axes(fig): format_axes(fig) plt.show() + +# %% +# .. tags:: +# +# component: subplot +# level: intermediate diff --git a/galleries/examples/subplots_axes_and_figures/gridspec_nested.py b/galleries/examples/subplots_axes_and_figures/gridspec_nested.py index bfcb90cdfc4a..025bdb1185a7 100644 --- a/galleries/examples/subplots_axes_and_figures/gridspec_nested.py +++ b/galleries/examples/subplots_axes_and_figures/gridspec_nested.py @@ -44,3 +44,9 @@ def format_axes(fig): format_axes(fig) plt.show() + +# %% +# .. tags:: +# +# component: subplot +# level: intermediate diff --git a/galleries/examples/subplots_axes_and_figures/invert_axes.py b/galleries/examples/subplots_axes_and_figures/invert_axes.py index 31f4d75680ce..40a4ca2479b7 100644 --- a/galleries/examples/subplots_axes_and_figures/invert_axes.py +++ b/galleries/examples/subplots_axes_and_figures/invert_axes.py @@ -33,3 +33,10 @@ ax2.grid(True) plt.show() + +# %% +# .. tags:: +# +# component: axis +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/secondary_axis.py b/galleries/examples/subplots_axes_and_figures/secondary_axis.py index ab42d3a6c182..d6dfd33f62c1 100644 --- a/galleries/examples/subplots_axes_and_figures/secondary_axis.py +++ b/galleries/examples/subplots_axes_and_figures/secondary_axis.py @@ -211,3 +211,9 @@ def anomaly_to_celsius(x): # # - `matplotlib.axes.Axes.secondary_xaxis` # - `matplotlib.axes.Axes.secondary_yaxis` +# +# .. tags:: +# +# component: axis +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py b/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py index f8073b2c3c31..e0aa04d13def 100644 --- a/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py +++ b/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py @@ -23,3 +23,10 @@ ax2.plot(t, np.sin(4*np.pi*t)) plt.show() + +# %% +# .. tags:: +# +# component: axis +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py b/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py index 6b3b3839a437..a5c000a24a96 100644 --- a/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py +++ b/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py @@ -55,3 +55,10 @@ plt.plot(t, s3) plt.xlim(0.01, 5.0) plt.show() + +# %% +# .. tags:: +# +# component: axis +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/subfigures.py b/galleries/examples/subplots_axes_and_figures/subfigures.py index 6272de975c4d..5060946b59b2 100644 --- a/galleries/examples/subplots_axes_and_figures/subfigures.py +++ b/galleries/examples/subplots_axes_and_figures/subfigures.py @@ -146,3 +146,10 @@ def example_plot(ax, fontsize=12, hide_labels=False): axsRight = subfigs[1].subplots(2, 2) plt.show() + +# %% +# .. tags:: +# +# component: figure +# plot-type: pcolormesh +# level: intermediate diff --git a/galleries/examples/subplots_axes_and_figures/subplot.py b/galleries/examples/subplots_axes_and_figures/subplot.py index 4b78e7a5a840..e23b86fa3e9c 100644 --- a/galleries/examples/subplots_axes_and_figures/subplot.py +++ b/galleries/examples/subplots_axes_and_figures/subplot.py @@ -49,3 +49,10 @@ plt.ylabel('Undamped') plt.show() + +# %% +# .. tags:: +# +# component: subplot +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/subplots_adjust.py b/galleries/examples/subplots_axes_and_figures/subplots_adjust.py index d4393be51fb4..8e3b876adfeb 100644 --- a/galleries/examples/subplots_axes_and_figures/subplots_adjust.py +++ b/galleries/examples/subplots_axes_and_figures/subplots_adjust.py @@ -29,3 +29,10 @@ plt.colorbar(cax=cax) plt.show() + +# %% +# .. tags:: +# +# component: subplot +# plot-type: imshow +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/subplots_demo.py b/galleries/examples/subplots_axes_and_figures/subplots_demo.py index afc71c795365..acd031b8201d 100644 --- a/galleries/examples/subplots_axes_and_figures/subplots_demo.py +++ b/galleries/examples/subplots_axes_and_figures/subplots_demo.py @@ -209,3 +209,12 @@ ax2.plot(x, y ** 2) plt.show() + +# %% +# .. tags:: +# +# component: subplot +# plot-type: line +# plot-type: polar +# level: beginner +# purpose: showcase diff --git a/galleries/examples/subplots_axes_and_figures/two_scales.py b/galleries/examples/subplots_axes_and_figures/two_scales.py index 249a65fd64fe..882fcac7866e 100644 --- a/galleries/examples/subplots_axes_and_figures/two_scales.py +++ b/galleries/examples/subplots_axes_and_figures/two_scales.py @@ -49,3 +49,9 @@ # - `matplotlib.axes.Axes.twinx` / `matplotlib.pyplot.twinx` # - `matplotlib.axes.Axes.twiny` / `matplotlib.pyplot.twiny` # - `matplotlib.axes.Axes.tick_params` / `matplotlib.pyplot.tick_params` +# +# .. tags:: +# +# component: axes +# plot-type: line +# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/zoom_inset_axes.py b/galleries/examples/subplots_axes_and_figures/zoom_inset_axes.py index 4cbd9875e4bc..4453b3ec39f1 100644 --- a/galleries/examples/subplots_axes_and_figures/zoom_inset_axes.py +++ b/galleries/examples/subplots_axes_and_figures/zoom_inset_axes.py @@ -43,3 +43,9 @@ # - `matplotlib.axes.Axes.inset_axes` # - `matplotlib.axes.Axes.indicate_inset_zoom` # - `matplotlib.axes.Axes.imshow` +# +# .. tags:: +# +# component: axes +# plot-type: imshow +# level: intermediate From 317f2383828da9f619180e531b91b1529fa2d492 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 13 Sep 2024 00:26:10 +0200 Subject: [PATCH 0243/1230] Document how to obtain sans-serif usetex math. TL;DR: add the sfmath package to the preamble. Also do some additional minor rewordings of the rest of the usetex tutorial; in particular, remove the comment regarding direct usage of dvi files (which is not particularly relevant for end users, and where the "future" mentioned may still be quite far away (or not)). --- galleries/users_explain/text/usetex.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/galleries/users_explain/text/usetex.py b/galleries/users_explain/text/usetex.py index 0194a0030d48..f0c266819897 100644 --- a/galleries/users_explain/text/usetex.py +++ b/galleries/users_explain/text/usetex.py @@ -102,6 +102,12 @@ :rc:`text.usetex`. As noted above, underscores (``_``) do not require escaping outside of math mode. +.. note:: + LaTeX always defaults to using a serif font for math (even when + ``rcParams["font.family"] = "sans-serif"``). If desired, adding + ``\usepackage{sfmath}`` to ``rcParams["text.latex.preamble"]`` lets LaTeX + output sans-serif math. + PostScript options ================== @@ -129,19 +135,13 @@ :ref:`setting-windows-environment-variables` for details. * Using MiKTeX with Computer Modern fonts, if you get odd \*Agg and PNG - results, go to MiKTeX/Options and update your format files + results, go to MiKTeX/Options and update your format files. * On Ubuntu and Gentoo, the base texlive install does not ship with the type1cm package. You may need to install some of the extra packages to get all the goodies that come bundled with other LaTeX distributions. -* Some progress has been made so Matplotlib uses the dvi files - directly for text layout. This allows LaTeX to be used for text - layout with the pdf and svg backends, as well as the \*Agg and PS - backends. In the future, a LaTeX installation may be the only - external dependency. - .. _usetex-troubleshooting: Troubleshooting @@ -150,7 +150,7 @@ * Try deleting your :file:`.matplotlib/tex.cache` directory. If you don't know where to find :file:`.matplotlib`, see :ref:`locating-matplotlib-config-dir`. -* Make sure LaTeX, dvipng and ghostscript are each working and on your +* Make sure LaTeX, dvipng and Ghostscript are each working and on your :envvar:`PATH`. * Make sure what you are trying to do is possible in a LaTeX document, @@ -159,8 +159,7 @@ * :rc:`text.latex.preamble` is not officially supported. This option provides lots of flexibility, and lots of ways to cause - problems. Please disable this option before reporting problems to - the mailing list. + problems. Please disable this option before reporting problems. * If you still need help, please see :ref:`reporting-problems`. From 5fa2a000e4700294a059212dc3e0847e0db5ea01 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 12 Sep 2024 19:05:23 -0400 Subject: [PATCH 0244/1230] Backport PR #28810: Document how to obtain sans-serif usetex math. --- galleries/users_explain/text/usetex.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/galleries/users_explain/text/usetex.py b/galleries/users_explain/text/usetex.py index 0194a0030d48..f0c266819897 100644 --- a/galleries/users_explain/text/usetex.py +++ b/galleries/users_explain/text/usetex.py @@ -102,6 +102,12 @@ :rc:`text.usetex`. As noted above, underscores (``_``) do not require escaping outside of math mode. +.. note:: + LaTeX always defaults to using a serif font for math (even when + ``rcParams["font.family"] = "sans-serif"``). If desired, adding + ``\usepackage{sfmath}`` to ``rcParams["text.latex.preamble"]`` lets LaTeX + output sans-serif math. + PostScript options ================== @@ -129,19 +135,13 @@ :ref:`setting-windows-environment-variables` for details. * Using MiKTeX with Computer Modern fonts, if you get odd \*Agg and PNG - results, go to MiKTeX/Options and update your format files + results, go to MiKTeX/Options and update your format files. * On Ubuntu and Gentoo, the base texlive install does not ship with the type1cm package. You may need to install some of the extra packages to get all the goodies that come bundled with other LaTeX distributions. -* Some progress has been made so Matplotlib uses the dvi files - directly for text layout. This allows LaTeX to be used for text - layout with the pdf and svg backends, as well as the \*Agg and PS - backends. In the future, a LaTeX installation may be the only - external dependency. - .. _usetex-troubleshooting: Troubleshooting @@ -150,7 +150,7 @@ * Try deleting your :file:`.matplotlib/tex.cache` directory. If you don't know where to find :file:`.matplotlib`, see :ref:`locating-matplotlib-config-dir`. -* Make sure LaTeX, dvipng and ghostscript are each working and on your +* Make sure LaTeX, dvipng and Ghostscript are each working and on your :envvar:`PATH`. * Make sure what you are trying to do is possible in a LaTeX document, @@ -159,8 +159,7 @@ * :rc:`text.latex.preamble` is not officially supported. This option provides lots of flexibility, and lots of ways to cause - problems. Please disable this option before reporting problems to - the mailing list. + problems. Please disable this option before reporting problems. * If you still need help, please see :ref:`reporting-problems`. From a058fae22f55154bd4080b41a5d9b096e8e96d1d Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 12 Sep 2024 19:05:23 -0400 Subject: [PATCH 0245/1230] Backport PR #28810: Document how to obtain sans-serif usetex math. --- galleries/users_explain/text/usetex.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/galleries/users_explain/text/usetex.py b/galleries/users_explain/text/usetex.py index 0194a0030d48..f0c266819897 100644 --- a/galleries/users_explain/text/usetex.py +++ b/galleries/users_explain/text/usetex.py @@ -102,6 +102,12 @@ :rc:`text.usetex`. As noted above, underscores (``_``) do not require escaping outside of math mode. +.. note:: + LaTeX always defaults to using a serif font for math (even when + ``rcParams["font.family"] = "sans-serif"``). If desired, adding + ``\usepackage{sfmath}`` to ``rcParams["text.latex.preamble"]`` lets LaTeX + output sans-serif math. + PostScript options ================== @@ -129,19 +135,13 @@ :ref:`setting-windows-environment-variables` for details. * Using MiKTeX with Computer Modern fonts, if you get odd \*Agg and PNG - results, go to MiKTeX/Options and update your format files + results, go to MiKTeX/Options and update your format files. * On Ubuntu and Gentoo, the base texlive install does not ship with the type1cm package. You may need to install some of the extra packages to get all the goodies that come bundled with other LaTeX distributions. -* Some progress has been made so Matplotlib uses the dvi files - directly for text layout. This allows LaTeX to be used for text - layout with the pdf and svg backends, as well as the \*Agg and PS - backends. In the future, a LaTeX installation may be the only - external dependency. - .. _usetex-troubleshooting: Troubleshooting @@ -150,7 +150,7 @@ * Try deleting your :file:`.matplotlib/tex.cache` directory. If you don't know where to find :file:`.matplotlib`, see :ref:`locating-matplotlib-config-dir`. -* Make sure LaTeX, dvipng and ghostscript are each working and on your +* Make sure LaTeX, dvipng and Ghostscript are each working and on your :envvar:`PATH`. * Make sure what you are trying to do is possible in a LaTeX document, @@ -159,8 +159,7 @@ * :rc:`text.latex.preamble` is not officially supported. This option provides lots of flexibility, and lots of ways to cause - problems. Please disable this option before reporting problems to - the mailing list. + problems. Please disable this option before reporting problems. * If you still need help, please see :ref:`reporting-problems`. From 8ea86fed13b656ef0bbadceada10255e0d17b35a Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 13 Sep 2024 01:29:21 +0200 Subject: [PATCH 0246/1230] DOC: Add a plot to margins() to visualize the effect --- doc/_embedded_plots/axes_margins.py | 42 +++++++++++++++++++++ lib/matplotlib/axes/_base.py | 57 +++++++++-------------------- 2 files changed, 60 insertions(+), 39 deletions(-) create mode 100644 doc/_embedded_plots/axes_margins.py diff --git a/doc/_embedded_plots/axes_margins.py b/doc/_embedded_plots/axes_margins.py new file mode 100644 index 000000000000..d026840c3c15 --- /dev/null +++ b/doc/_embedded_plots/axes_margins.py @@ -0,0 +1,42 @@ +import numpy as np +import matplotlib.pyplot as plt + +fig, ax = plt.subplots(figsize=(6.5, 4)) +x = np.linspace(0, 1, 33) +y = -np.sin(x * 2*np.pi) +ax.plot(x, y, 'o') +ax.margins(0.5, 0.2) +ax.set_title("margins(x=0.5, y=0.2)") + +# fix the Axes limits so that the following helper drawings +# cannot change them further. +ax.set(xlim=ax.get_xlim(), ylim=ax.get_ylim()) + + +def arrow(p1, p2, **props): + ax.annotate("", p1, p2, + arrowprops=dict(arrowstyle="<->", shrinkA=0, shrinkB=0, **props)) + + +axmin, axmax = ax.get_xlim() +aymin, aymax = ax.get_ylim() +xmin, xmax = x.min(), x.max() +ymin, ymax = y.min(), y.max() + +y0 = -0.8 +ax.axvspan(axmin, xmin, color=("orange", 0.1)) +ax.axvspan(xmax, axmax, color=("orange", 0.1)) +arrow((xmin, y0), (xmax, y0), color="sienna") +arrow((xmax, y0), (axmax, y0), color="orange") +ax.text((xmax + axmax)/2, y0+0.05, "x margin\n* x data range", + ha="center", va="bottom", color="orange") +ax.text(0.55, y0+0.1, "x data range", va="bottom", color="sienna") + +x0 = 0.1 +ax.axhspan(aymin, ymin, color=("tab:green", 0.1)) +ax.axhspan(ymax, aymax, color=("tab:green", 0.1)) +arrow((x0, ymin), (x0, ymax), color="darkgreen") +arrow((x0, ymax), (x0, aymax), color="tab:green") +ax.text(x0, (ymax + aymax) / 2, " y margin * y data range", + va="center", color="tab:green") +ax.text(x0, 0.5, " y data range", color="darkgreen") diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 7d72b8caedfa..18cfec831af4 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2736,47 +2736,22 @@ def set_ymargin(self, m): def margins(self, *margins, x=None, y=None, tight=True): """ - Set or retrieve autoscaling margins. + Set or retrieve margins around the data for autoscaling axis limits. - The padding added to each limit of the Axes is the *margin* - times the data interval. All input parameters must be floats - greater than -0.5. Passing both positional and keyword - arguments is invalid and will raise a TypeError. If no - arguments (positional or otherwise) are provided, the current - margins will remain unchanged and simply be returned. - - .. plot:: - - import numpy as np - import matplotlib.pyplot as plt - - x, y = np.meshgrid(np.linspace(0, 1, 10), np.linspace(0, 1, 10)) - fig, ax = plt.subplots() - ax.plot(x, y, 'o', color='lightblue') - ax.margins(1.5, 0.5) - ax.set_title("margins(x=1.5, y=0.5)") + This allows to configure the padding around the data without having to + set explicit limits using `~.Axes.set_xlim` / `~.Axes.set_ylim`. - def arrow(p1, p2, **props): - ax.annotate("", p1, p2, - arrowprops=dict(arrowstyle="<->", shrinkA=0, shrinkB=0, **props)) + Autoscaling determines the axis limits by adding *margin* times the + data interval as padding around the data. See the following illustration: - arrow((-1.5, 0), (0, 0), color="orange") - arrow((0, 0), (1, 0), color="sienna") - arrow((1, 0), (2.5, 0), color="orange") - ax.text(-0.75, -0.1, "x margin * x data range", ha="center", - color="orange") - ax.text(0.5, -0.1, "x data range", ha="center", color="sienna") + .. plot:: _embedded_plots/axes_margins.py - arrow((1, -0.5), (1, 0), color="tab:green") - arrow((1, 0), (1, 1), color="darkgreen") - arrow((1, 1), (1, 1.5), color="tab:green") - ax.text(1.1, 1.25, "y margin * y data range", color="tab:green") - ax.text(1.1, 0.5, "y data range", color="darkgreen") + All input parameters must be floats greater than -0.5. Passing both + positional and keyword arguments is invalid and will raise a TypeError. + If no arguments (positional or otherwise) are provided, the current + margins will remain unchanged and simply be returned. - Specifying any margin changes only the autoscaling; for example, - if *xmargin* is not None, then *xmargin* times the X data - interval will be added to each end of that interval before - it is used in autoscaling. + The default margins are :rc:`axes.xmargin` and :rc:`axes.ymargin`. Parameters ---------- @@ -2808,10 +2783,14 @@ def arrow(p1, p2, **props): Notes ----- If a previously used Axes method such as :meth:`pcolor` has set - :attr:`use_sticky_edges` to `True`, only the limits not set by - the "sticky artists" will be modified. To force all of the - margins to be set, set :attr:`use_sticky_edges` to `False` + `~.Axes.use_sticky_edges` to `True`, only the limits not set by + the "sticky artists" will be modified. To force all + margins to be set, set `~.Axes.use_sticky_edges` to `False` before calling :meth:`margins`. + + See Also + -------- + .Axes.set_xmargin, .Axes.set_ymargin """ if margins and (x is not None or y is not None): From 9468f6dafd90fdcfef1757c17f1d0e97df43997e Mon Sep 17 00:00:00 2001 From: Pierre-antoine Comby Date: Sat, 16 Mar 2024 18:56:08 +0100 Subject: [PATCH 0247/1230] feat: add dunder method for math operations. --- lib/mpl_toolkits/axes_grid1/axes_size.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/mpl_toolkits/axes_grid1/axes_size.py b/lib/mpl_toolkits/axes_grid1/axes_size.py index e417c1a899ac..86e5f70d9824 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_size.py +++ b/lib/mpl_toolkits/axes_grid1/axes_size.py @@ -7,6 +7,10 @@ class (or others) to determine the size of each Axes. The unit Note that this class is nothing more than a simple tuple of two floats. Take a look at the Divider class to see how these two values are used. + +Once created, the unit classes can be modified by simple arithmetic +operations: addition /subtraction with another unit type or a real number and scaling +(multiplication or division) by a real number. """ from numbers import Real @@ -17,14 +21,33 @@ class (or others) to determine the size of each Axes. The unit class _Base: def __rmul__(self, other): + return self * other + + def __mul__(self, other): + if not isinstance(other, Real): + return NotImplemented return Fraction(other, self) + def __div__(self, other): + return (1 / other) * self + def __add__(self, other): if isinstance(other, _Base): return Add(self, other) else: return Add(self, Fixed(other)) + def __neg__(self): + return -1 * self + + def __radd__(self, other): + # other cannot be a _Base instance, because A + B would trigger + # A.__add__(B) first. + return Add(self, Fixed(other)) + + def __sub__(self, other): + return self + (-other) + def get_size(self, renderer): """ Return two-float tuple with relative and absolute sizes. From ac268ec8bee5da1d70a7939d1a47f8a669aa4606 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 5 Oct 2023 21:01:49 -0400 Subject: [PATCH 0248/1230] Move some checks from RendererAgg wrapper into itself --- src/_backend_agg.cpp | 10 ++++++++++ src/_backend_agg.h | 17 +++++++++++++++++ src/_backend_agg_wrapper.cpp | 27 --------------------------- src/meson.build | 2 +- src/py_exceptions.h | 10 +++++++++- 5 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/_backend_agg.cpp b/src/_backend_agg.cpp index ce88f504dc1e..3460b429ec12 100644 --- a/src/_backend_agg.cpp +++ b/src/_backend_agg.cpp @@ -29,6 +29,16 @@ RendererAgg::RendererAgg(unsigned int width, unsigned int height, double dpi) lastclippath(NULL), _fill_color(agg::rgba(1, 1, 1, 0)) { + if (dpi <= 0.0) { + throw std::range_error("dpi must be positive"); + } + + if (width >= 1 << 16 || height >= 1 << 16) { + throw std::range_error( + "Image size of " + std::to_string(width) + "x" + std::to_string(height) + + " pixels is too large. It must be less than 2^16 in each direction."); + } + unsigned stride(width * 4); pixBuffer = new agg::int8u[NUMBYTES]; diff --git a/src/_backend_agg.h b/src/_backend_agg.h index 470d459de341..3bab3bb785f5 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -6,6 +6,8 @@ #ifndef MPL_BACKEND_AGG_H #define MPL_BACKEND_AGG_H +#include + #include #include @@ -40,6 +42,8 @@ #include "array.h" #include "agg_workaround.h" +namespace py = pybind11; + /**********************************************************************/ // a helper class to pass agg::buffer objects around. @@ -1226,6 +1230,19 @@ inline void RendererAgg::draw_gouraud_triangles(GCAgg &gc, ColorArray &colors, agg::trans_affine &trans) { + if (points.shape(0) && !check_trailing_shape(points, "points", 3, 2)) { + throw py::error_already_set(); + } + if (colors.shape(0) && !check_trailing_shape(colors, "colors", 3, 4)) { + throw py::error_already_set(); + } + if (points.shape(0) != colors.shape(0)) { + throw py::value_error( + "points and colors arrays must be the same length, got " + + std::to_string(points.shape(0)) + " points and " + + std::to_string(colors.shape(0)) + "colors"); + } + theRasterizer.reset_clipping(); rendererBase.reset_clipping(true); set_clipbox(gc.cliprect, theRasterizer); diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index eaf4bf6f5f9d..108d3679760d 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -145,20 +145,6 @@ static int PyRendererAgg_init(PyRendererAgg *self, PyObject *args, PyObject *kwd return -1; } - if (dpi <= 0.0) { - PyErr_SetString(PyExc_ValueError, "dpi must be positive"); - return -1; - } - - if (width >= 1 << 16 || height >= 1 << 16) { - PyErr_Format( - PyExc_ValueError, - "Image size of %dx%d pixels is too large. " - "It must be less than 2^16 in each direction.", - width, height); - return -1; - } - CALL_CPP_INIT("RendererAgg", self->x = new RendererAgg(width, height, dpi)) return 0; @@ -420,19 +406,6 @@ PyRendererAgg_draw_gouraud_triangles(PyRendererAgg *self, PyObject *args) &trans)) { return NULL; } - if (points.shape(0) && !check_trailing_shape(points, "points", 3, 2)) { - return NULL; - } - if (colors.shape(0) && !check_trailing_shape(colors, "colors", 3, 4)) { - return NULL; - } - if (points.shape(0) != colors.shape(0)) { - PyErr_Format(PyExc_ValueError, - "points and colors arrays must be the same length, got " - "%" NPY_INTP_FMT " points and %" NPY_INTP_FMT "colors", - points.shape(0), colors.shape(0)); - return NULL; - } CALL_CPP("draw_gouraud_triangles", self->x->draw_gouraud_triangles(gc, points, colors, trans)); diff --git a/src/meson.build b/src/meson.build index a046b3306ab8..b92b1e407ba3 100644 --- a/src/meson.build +++ b/src/meson.build @@ -77,7 +77,7 @@ extension_data = { '_backend_agg.cpp', '_backend_agg_wrapper.cpp', ), - 'dependencies': [agg_dep, numpy_dep, freetype_dep], + 'dependencies': [agg_dep, numpy_dep, freetype_dep, pybind11_dep], }, '_c_internal_utils': { 'subdir': 'matplotlib', diff --git a/src/py_exceptions.h b/src/py_exceptions.h index 7a7e004a4a7a..db1977c413dd 100644 --- a/src/py_exceptions.h +++ b/src/py_exceptions.h @@ -46,9 +46,17 @@ class exception : public std::exception } \ return (errorcode); \ } \ + catch (const std::range_error &e) \ + { \ + PyErr_Format(PyExc_ValueError, "In %s: %s", (name), e.what()); \ + { \ + cleanup; \ + } \ + return (errorcode); \ + } \ catch (const std::runtime_error &e) \ { \ - PyErr_Format(PyExc_RuntimeError, "In %s: %s", (name), e.what()); \ + PyErr_Format(PyExc_RuntimeError, "In %s: %s", (name), e.what()); \ { \ cleanup; \ } \ From 96dd843546ee410ea27e2a3e8f61953a1d3cdc6a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 5 Oct 2023 23:55:07 -0400 Subject: [PATCH 0249/1230] Port Agg wrapper to pybind11 This is a _very_ straightforward port, and several parts can be cleaned up in future commits. --- src/_backend_agg_wrapper.cpp | 674 +++++++++++++---------------------- src/meson.build | 1 + src/py_converters_11.h | 2 + 3 files changed, 244 insertions(+), 433 deletions(-) diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 108d3679760d..1486f4772056 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -1,539 +1,347 @@ +#include +#include #include "mplutils.h" #include "numpy_cpp.h" #include "py_converters.h" #include "_backend_agg.h" +#include "py_converters_11.h" -typedef struct -{ - PyObject_HEAD - RendererAgg *x; - Py_ssize_t shape[3]; - Py_ssize_t strides[3]; - Py_ssize_t suboffsets[3]; -} PyRendererAgg; - -static PyTypeObject PyRendererAggType; - -typedef struct -{ - PyObject_HEAD - BufferRegion *x; - Py_ssize_t shape[3]; - Py_ssize_t strides[3]; - Py_ssize_t suboffsets[3]; -} PyBufferRegion; - -static PyTypeObject PyBufferRegionType; - +namespace py = pybind11; +using namespace pybind11::literals; /********************************************************************** * BufferRegion * */ -static PyObject *PyBufferRegion_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - PyBufferRegion *self; - self = (PyBufferRegion *)type->tp_alloc(type, 0); - self->x = NULL; - return (PyObject *)self; -} - -static void PyBufferRegion_dealloc(PyBufferRegion *self) -{ - delete self->x; - Py_TYPE(self)->tp_free((PyObject *)self); -} - /* TODO: This doesn't seem to be used internally. Remove? */ -static PyObject *PyBufferRegion_set_x(PyBufferRegion *self, PyObject *args) +static void +PyBufferRegion_set_x(BufferRegion *self, int x) { - int x; - if (!PyArg_ParseTuple(args, "i:set_x", &x)) { - return NULL; - } - self->x->get_rect().x1 = x; - - Py_RETURN_NONE; + self->get_rect().x1 = x; } -static PyObject *PyBufferRegion_set_y(PyBufferRegion *self, PyObject *args) +static void +PyBufferRegion_set_y(BufferRegion *self, int y) { - int y; - if (!PyArg_ParseTuple(args, "i:set_y", &y)) { - return NULL; - } - self->x->get_rect().y1 = y; - - Py_RETURN_NONE; + self->get_rect().y1 = y; } -static PyObject *PyBufferRegion_get_extents(PyBufferRegion *self, PyObject *args) +static py::object +PyBufferRegion_get_extents(BufferRegion *self) { - agg::rect_i rect = self->x->get_rect(); + agg::rect_i rect = self->get_rect(); - return Py_BuildValue("IIII", rect.x1, rect.y1, rect.x2, rect.y2); -} - -int PyBufferRegion_get_buffer(PyBufferRegion *self, Py_buffer *buf, int flags) -{ - Py_INCREF(self); - buf->obj = (PyObject *)self; - buf->buf = self->x->get_data(); - buf->len = (Py_ssize_t)self->x->get_width() * (Py_ssize_t)self->x->get_height() * 4; - buf->readonly = 0; - buf->format = (char *)"B"; - buf->ndim = 3; - self->shape[0] = self->x->get_height(); - self->shape[1] = self->x->get_width(); - self->shape[2] = 4; - buf->shape = self->shape; - self->strides[0] = self->x->get_width() * 4; - self->strides[1] = 4; - self->strides[2] = 1; - buf->strides = self->strides; - buf->suboffsets = NULL; - buf->itemsize = 1; - buf->internal = NULL; - - return 1; -} - -static PyTypeObject *PyBufferRegion_init_type() -{ - static PyMethodDef methods[] = { - { "set_x", (PyCFunction)PyBufferRegion_set_x, METH_VARARGS, NULL }, - { "set_y", (PyCFunction)PyBufferRegion_set_y, METH_VARARGS, NULL }, - { "get_extents", (PyCFunction)PyBufferRegion_get_extents, METH_NOARGS, NULL }, - { NULL } - }; - - static PyBufferProcs buffer_procs; - buffer_procs.bf_getbuffer = (getbufferproc)PyBufferRegion_get_buffer; - - PyBufferRegionType.tp_name = "matplotlib.backends._backend_agg.BufferRegion"; - PyBufferRegionType.tp_basicsize = sizeof(PyBufferRegion); - PyBufferRegionType.tp_dealloc = (destructor)PyBufferRegion_dealloc; - PyBufferRegionType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; - PyBufferRegionType.tp_methods = methods; - PyBufferRegionType.tp_new = PyBufferRegion_new; - PyBufferRegionType.tp_as_buffer = &buffer_procs; - - return &PyBufferRegionType; + return py::make_tuple(rect.x1, rect.y1, rect.x2, rect.y2); } /********************************************************************** * RendererAgg * */ -static PyObject *PyRendererAgg_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - PyRendererAgg *self; - self = (PyRendererAgg *)type->tp_alloc(type, 0); - self->x = NULL; - return (PyObject *)self; -} - -static int PyRendererAgg_init(PyRendererAgg *self, PyObject *args, PyObject *kwds) -{ - unsigned int width; - unsigned int height; - double dpi; - int debug = 0; - - if (!PyArg_ParseTuple(args, "IId|i:RendererAgg", &width, &height, &dpi, &debug)) { - return -1; - } - - CALL_CPP_INIT("RendererAgg", self->x = new RendererAgg(width, height, dpi)) - - return 0; -} - -static void PyRendererAgg_dealloc(PyRendererAgg *self) -{ - delete self->x; - Py_TYPE(self)->tp_free((PyObject *)self); -} - -static PyObject *PyRendererAgg_draw_path(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_path(RendererAgg *self, + py::object gc_obj, + mpl::PathIterator path, + agg::trans_affine trans, + py::object face_obj) { GCAgg gc; - mpl::PathIterator path; - agg::trans_affine trans; - PyObject *faceobj = NULL; agg::rgba face; - if (!PyArg_ParseTuple(args, - "O&O&O&|O:draw_path", - &convert_gcagg, - &gc, - &convert_path, - &path, - &convert_trans_affine, - &trans, - &faceobj)) { - return NULL; + if (!convert_gcagg(gc_obj.ptr(), &gc)) { + throw py::error_already_set(); } - if (!convert_face(faceobj, gc, &face)) { - return NULL; + if (!convert_face(face_obj.ptr(), gc, &face)) { + throw py::error_already_set(); } - CALL_CPP("draw_path", (self->x->draw_path(gc, path, trans, face))); - - Py_RETURN_NONE; + self->draw_path(gc, path, trans, face); } -static PyObject *PyRendererAgg_draw_text_image(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_text_image(RendererAgg *self, + py::array_t image_obj, + double x, + double y, + double angle, + py::object gc_obj) { numpy::array_view image; - double x; - double y; - double angle; GCAgg gc; - if (!PyArg_ParseTuple(args, - "O&dddO&:draw_text_image", - &image.converter_contiguous, - &image, - &x, - &y, - &angle, - &convert_gcagg, - &gc)) { - return NULL; + if (!image.converter_contiguous(image_obj.ptr(), &image)) { + throw py::error_already_set(); + } + if (!convert_gcagg(gc_obj.ptr(), &gc)) { + throw py::error_already_set(); } - CALL_CPP("draw_text_image", (self->x->draw_text_image(gc, image, x, y, angle))); - - Py_RETURN_NONE; + self->draw_text_image(gc, image, x, y, angle); } -PyObject *PyRendererAgg_draw_markers(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_markers(RendererAgg *self, + py::object gc_obj, + mpl::PathIterator marker_path, + agg::trans_affine marker_path_trans, + mpl::PathIterator path, + agg::trans_affine trans, + py::object face_obj) { GCAgg gc; - mpl::PathIterator marker_path; - agg::trans_affine marker_path_trans; - mpl::PathIterator path; - agg::trans_affine trans; - PyObject *faceobj = NULL; agg::rgba face; - if (!PyArg_ParseTuple(args, - "O&O&O&O&O&|O:draw_markers", - &convert_gcagg, - &gc, - &convert_path, - &marker_path, - &convert_trans_affine, - &marker_path_trans, - &convert_path, - &path, - &convert_trans_affine, - &trans, - &faceobj)) { - return NULL; + if (!convert_gcagg(gc_obj.ptr(), &gc)) { + throw py::error_already_set(); } - if (!convert_face(faceobj, gc, &face)) { - return NULL; + if (!convert_face(face_obj.ptr(), gc, &face)) { + throw py::error_already_set(); } - CALL_CPP("draw_markers", - (self->x->draw_markers(gc, marker_path, marker_path_trans, path, trans, face))); - - Py_RETURN_NONE; + self->draw_markers(gc, marker_path, marker_path_trans, path, trans, face); } -static PyObject *PyRendererAgg_draw_image(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_image(RendererAgg *self, + py::object gc_obj, + double x, + double y, + py::array_t image_obj) { GCAgg gc; - double x; - double y; numpy::array_view image; - if (!PyArg_ParseTuple(args, - "O&ddO&:draw_image", - &convert_gcagg, - &gc, - &x, - &y, - &image.converter_contiguous, - &image)) { - return NULL; + if (!convert_gcagg(gc_obj.ptr(), &gc)) { + throw py::error_already_set(); + } + if (!image.set(image_obj.ptr())) { + throw py::error_already_set(); } x = mpl_round(x); y = mpl_round(y); gc.alpha = 1.0; - CALL_CPP("draw_image", (self->x->draw_image(gc, x, y, image))); - - Py_RETURN_NONE; + self->draw_image(gc, x, y, image); } -static PyObject * -PyRendererAgg_draw_path_collection(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_path_collection(RendererAgg *self, + py::object gc_obj, + agg::trans_affine master_transform, + py::object paths_obj, + py::object transforms_obj, + py::object offsets_obj, + agg::trans_affine offset_trans, + py::object facecolors_obj, + py::object edgecolors_obj, + py::object linewidths_obj, + py::object dashes_obj, + py::object antialiaseds_obj, + py::object Py_UNUSED(ignored_obj), + // offset position is no longer used + py::object Py_UNUSED(offset_position_obj)) { GCAgg gc; - agg::trans_affine master_transform; mpl::PathGenerator paths; numpy::array_view transforms; numpy::array_view offsets; - agg::trans_affine offset_trans; numpy::array_view facecolors; numpy::array_view edgecolors; numpy::array_view linewidths; DashesVector dashes; numpy::array_view antialiaseds; - PyObject *ignored; - PyObject *offset_position; // offset position is no longer used - - if (!PyArg_ParseTuple(args, - "O&O&O&O&O&O&O&O&O&O&O&OO:draw_path_collection", - &convert_gcagg, - &gc, - &convert_trans_affine, - &master_transform, - &convert_pathgen, - &paths, - &convert_transforms, - &transforms, - &convert_points, - &offsets, - &convert_trans_affine, - &offset_trans, - &convert_colors, - &facecolors, - &convert_colors, - &edgecolors, - &linewidths.converter, - &linewidths, - &convert_dashes_vector, - &dashes, - &antialiaseds.converter, - &antialiaseds, - &ignored, - &offset_position)) { - return NULL; + + if (!convert_gcagg(gc_obj.ptr(), &gc)) { + throw py::error_already_set(); + } + if (!convert_pathgen(paths_obj.ptr(), &paths)) { + throw py::error_already_set(); + } + if (!convert_transforms(transforms_obj.ptr(), &transforms)) { + throw py::error_already_set(); + } + if (!convert_points(offsets_obj.ptr(), &offsets)) { + throw py::error_already_set(); + } + if (!convert_colors(facecolors_obj.ptr(), &facecolors)) { + throw py::error_already_set(); + } + if (!convert_colors(edgecolors_obj.ptr(), &edgecolors)) { + throw py::error_already_set(); + } + if (!linewidths.converter(linewidths_obj.ptr(), &linewidths)) { + throw py::error_already_set(); + } + if (!convert_dashes_vector(dashes_obj.ptr(), &dashes)) { + throw py::error_already_set(); + } + if (!antialiaseds.converter(antialiaseds_obj.ptr(), &antialiaseds)) { + throw py::error_already_set(); } - CALL_CPP("draw_path_collection", - (self->x->draw_path_collection(gc, - master_transform, - paths, - transforms, - offsets, - offset_trans, - facecolors, - edgecolors, - linewidths, - dashes, - antialiaseds))); - - Py_RETURN_NONE; + self->draw_path_collection(gc, + master_transform, + paths, + transforms, + offsets, + offset_trans, + facecolors, + edgecolors, + linewidths, + dashes, + antialiaseds); } -static PyObject *PyRendererAgg_draw_quad_mesh(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_quad_mesh(RendererAgg *self, + py::object gc_obj, + agg::trans_affine master_transform, + unsigned int mesh_width, + unsigned int mesh_height, + py::object coordinates_obj, + py::object offsets_obj, + agg::trans_affine offset_trans, + py::object facecolors_obj, + bool antialiased, + py::object edgecolors_obj) { GCAgg gc; - agg::trans_affine master_transform; - unsigned int mesh_width; - unsigned int mesh_height; numpy::array_view coordinates; numpy::array_view offsets; - agg::trans_affine offset_trans; numpy::array_view facecolors; - bool antialiased; numpy::array_view edgecolors; - if (!PyArg_ParseTuple(args, - "O&O&IIO&O&O&O&O&O&:draw_quad_mesh", - &convert_gcagg, - &gc, - &convert_trans_affine, - &master_transform, - &mesh_width, - &mesh_height, - &coordinates.converter, - &coordinates, - &convert_points, - &offsets, - &convert_trans_affine, - &offset_trans, - &convert_colors, - &facecolors, - &convert_bool, - &antialiased, - &convert_colors, - &edgecolors)) { - return NULL; + if (!convert_gcagg(gc_obj.ptr(), &gc)) { + throw py::error_already_set(); + } + if (!coordinates.converter(coordinates_obj.ptr(), &coordinates)) { + throw py::error_already_set(); + } + if (!convert_points(offsets_obj.ptr(), &offsets)) { + throw py::error_already_set(); + } + if (!convert_colors(facecolors_obj.ptr(), &facecolors)) { + throw py::error_already_set(); + } + if (!convert_colors(edgecolors_obj.ptr(), &edgecolors)) { + throw py::error_already_set(); } - CALL_CPP("draw_quad_mesh", - (self->x->draw_quad_mesh(gc, - master_transform, - mesh_width, - mesh_height, - coordinates, - offsets, - offset_trans, - facecolors, - antialiased, - edgecolors))); - - Py_RETURN_NONE; + self->draw_quad_mesh(gc, + master_transform, + mesh_width, + mesh_height, + coordinates, + offsets, + offset_trans, + facecolors, + antialiased, + edgecolors); } -static PyObject * -PyRendererAgg_draw_gouraud_triangles(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_gouraud_triangles(RendererAgg *self, + py::object gc_obj, + py::object points_obj, + py::object colors_obj, + agg::trans_affine trans) { GCAgg gc; numpy::array_view points; numpy::array_view colors; - agg::trans_affine trans; - - if (!PyArg_ParseTuple(args, - "O&O&O&O&|O:draw_gouraud_triangles", - &convert_gcagg, - &gc, - &points.converter, - &points, - &colors.converter, - &colors, - &convert_trans_affine, - &trans)) { - return NULL; - } - - CALL_CPP("draw_gouraud_triangles", self->x->draw_gouraud_triangles(gc, points, colors, trans)); - - Py_RETURN_NONE; -} - -int PyRendererAgg_get_buffer(PyRendererAgg *self, Py_buffer *buf, int flags) -{ - Py_INCREF(self); - buf->obj = (PyObject *)self; - buf->buf = self->x->pixBuffer; - buf->len = (Py_ssize_t)self->x->get_width() * (Py_ssize_t)self->x->get_height() * 4; - buf->readonly = 0; - buf->format = (char *)"B"; - buf->ndim = 3; - self->shape[0] = self->x->get_height(); - self->shape[1] = self->x->get_width(); - self->shape[2] = 4; - buf->shape = self->shape; - self->strides[0] = self->x->get_width() * 4; - self->strides[1] = 4; - self->strides[2] = 1; - buf->strides = self->strides; - buf->suboffsets = NULL; - buf->itemsize = 1; - buf->internal = NULL; - - return 1; -} -static PyObject *PyRendererAgg_clear(PyRendererAgg *self, PyObject *args) -{ - CALL_CPP("clear", self->x->clear()); - - Py_RETURN_NONE; -} - -static PyObject *PyRendererAgg_copy_from_bbox(PyRendererAgg *self, PyObject *args) -{ - agg::rect_d bbox; - BufferRegion *reg; - PyObject *regobj; - - if (!PyArg_ParseTuple(args, "O&:copy_from_bbox", &convert_rect, &bbox)) { - return 0; + if (!convert_gcagg(gc_obj.ptr(), &gc)) { + throw py::error_already_set(); } - - CALL_CPP("copy_from_bbox", (reg = self->x->copy_from_bbox(bbox))); - - regobj = PyBufferRegion_new(&PyBufferRegionType, NULL, NULL); - ((PyBufferRegion *)regobj)->x = reg; - - return regobj; -} - -static PyObject *PyRendererAgg_restore_region(PyRendererAgg *self, PyObject *args) -{ - PyBufferRegion *regobj; - int xx1 = 0, yy1 = 0, xx2 = 0, yy2 = 0, x = 0, y = 0; - - if (!PyArg_ParseTuple(args, - "O!|iiiiii:restore_region", - &PyBufferRegionType, - ®obj, - &xx1, - &yy1, - &xx2, - &yy2, - &x, - &y)) { - return 0; + if (!points.converter(points_obj.ptr(), &points)) { + throw py::error_already_set(); } - - if (PySequence_Size(args) == 1) { - CALL_CPP("restore_region", self->x->restore_region(*(regobj->x))); - } else { - CALL_CPP("restore_region", self->x->restore_region(*(regobj->x), xx1, yy1, xx2, yy2, x, y)); + if (!colors.converter(colors_obj.ptr(), &colors)) { + throw py::error_already_set(); } - Py_RETURN_NONE; + self->draw_gouraud_triangles(gc, points, colors, trans); } -static PyTypeObject *PyRendererAgg_init_type() +PYBIND11_MODULE(_backend_agg, m) { - static PyMethodDef methods[] = { - {"draw_path", (PyCFunction)PyRendererAgg_draw_path, METH_VARARGS, NULL}, - {"draw_markers", (PyCFunction)PyRendererAgg_draw_markers, METH_VARARGS, NULL}, - {"draw_text_image", (PyCFunction)PyRendererAgg_draw_text_image, METH_VARARGS, NULL}, - {"draw_image", (PyCFunction)PyRendererAgg_draw_image, METH_VARARGS, NULL}, - {"draw_path_collection", (PyCFunction)PyRendererAgg_draw_path_collection, METH_VARARGS, NULL}, - {"draw_quad_mesh", (PyCFunction)PyRendererAgg_draw_quad_mesh, METH_VARARGS, NULL}, - {"draw_gouraud_triangles", (PyCFunction)PyRendererAgg_draw_gouraud_triangles, METH_VARARGS, NULL}, - - {"clear", (PyCFunction)PyRendererAgg_clear, METH_NOARGS, NULL}, - - {"copy_from_bbox", (PyCFunction)PyRendererAgg_copy_from_bbox, METH_VARARGS, NULL}, - {"restore_region", (PyCFunction)PyRendererAgg_restore_region, METH_VARARGS, NULL}, - {NULL} + auto ia = [m]() -> const void* { + import_array(); + return &m; }; - - static PyBufferProcs buffer_procs; - buffer_procs.bf_getbuffer = (getbufferproc)PyRendererAgg_get_buffer; - - PyRendererAggType.tp_name = "matplotlib.backends._backend_agg.RendererAgg"; - PyRendererAggType.tp_basicsize = sizeof(PyRendererAgg); - PyRendererAggType.tp_dealloc = (destructor)PyRendererAgg_dealloc; - PyRendererAggType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; - PyRendererAggType.tp_methods = methods; - PyRendererAggType.tp_init = (initproc)PyRendererAgg_init; - PyRendererAggType.tp_new = PyRendererAgg_new; - PyRendererAggType.tp_as_buffer = &buffer_procs; - - return &PyRendererAggType; -} - -static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "_backend_agg" }; - -PyMODINIT_FUNC PyInit__backend_agg(void) -{ - import_array(); - PyObject *m; - if (!(m = PyModule_Create(&moduledef)) - || prepare_and_add_type(PyRendererAgg_init_type(), m) - // BufferRegion is not constructible from Python, thus not added to the module. - || PyType_Ready(PyBufferRegion_init_type()) - ) { - Py_XDECREF(m); - return NULL; + if (ia() == NULL) { + throw py::error_already_set(); } - return m; + + py::class_(m, "RendererAgg", py::buffer_protocol()) + .def(py::init(), + "width"_a, "height"_a, "dpi"_a) + + .def("draw_path", &PyRendererAgg_draw_path, + "gc"_a, "path"_a, "trans"_a, "face"_a = nullptr) + .def("draw_markers", &PyRendererAgg_draw_markers, + "gc"_a, "marker_path"_a, "marker_path_trans"_a, "path"_a, "trans"_a, + "face"_a = nullptr) + .def("draw_text_image", &PyRendererAgg_draw_text_image, + "image"_a, "x"_a, "y"_a, "angle"_a, "gc"_a) + .def("draw_image", &PyRendererAgg_draw_image, + "gc"_a, "x"_a, "y"_a, "image"_a) + .def("draw_path_collection", &PyRendererAgg_draw_path_collection, + "gc"_a, "master_transform"_a, "paths"_a, "transforms"_a, "offsets"_a, + "offset_trans"_a, "facecolors"_a, "edgecolors"_a, "linewidths"_a, + "dashes"_a, "antialiaseds"_a, "ignored"_a, "offset_position"_a) + .def("draw_quad_mesh", &PyRendererAgg_draw_quad_mesh, + "gc"_a, "master_transform"_a, "mesh_width"_a, "mesh_height"_a, + "coordinates"_a, "offsets"_a, "offset_trans"_a, "facecolors"_a, + "antialiased"_a, "edgecolors"_a) + .def("draw_gouraud_triangles", &PyRendererAgg_draw_gouraud_triangles, + "gc"_a, "points"_a, "colors"_a, "trans"_a = nullptr) + + .def("clear", &RendererAgg::clear) + + .def("copy_from_bbox", &RendererAgg::copy_from_bbox, + "bbox"_a) + .def("restore_region", + py::overload_cast(&RendererAgg::restore_region), + "region"_a) + .def("restore_region", + py::overload_cast(&RendererAgg::restore_region), + "region"_a, "xx1"_a, "yy1"_a, "xx2"_a, "yy2"_a, "x"_a, "y"_a) + + .def_buffer([](RendererAgg *renderer) -> py::buffer_info { + std::vector shape { + renderer->get_height(), + renderer->get_width(), + 4 + }; + std::vector strides { + renderer->get_width() * 4, + 4, + 1 + }; + return py::buffer_info(renderer->pixBuffer, shape, strides); + }); + + py::class_(m, "BufferRegion", py::buffer_protocol()) + // BufferRegion is not constructible from Python, thus no py::init is added. + .def("set_x", &PyBufferRegion_set_x) + .def("set_y", &PyBufferRegion_set_y) + .def("get_extents", &PyBufferRegion_get_extents) + .def_buffer([](BufferRegion *buffer) -> py::buffer_info { + std::vector shape { + buffer->get_height(), + buffer->get_width(), + 4 + }; + std::vector strides { + buffer->get_width() * 4, + 4, + 1 + }; + return py::buffer_info(buffer->get_data(), shape, strides); + }); } diff --git a/src/meson.build b/src/meson.build index b92b1e407ba3..21ca98a639cf 100644 --- a/src/meson.build +++ b/src/meson.build @@ -74,6 +74,7 @@ extension_data = { 'subdir': 'matplotlib/backends', 'sources': files( 'py_converters.cpp', + 'py_converters_11.cpp', '_backend_agg.cpp', '_backend_agg_wrapper.cpp', ), diff --git a/src/py_converters_11.h b/src/py_converters_11.h index 911d5fe2b924..7f3b7846153e 100644 --- a/src/py_converters_11.h +++ b/src/py_converters_11.h @@ -137,6 +137,8 @@ namespace PYBIND11_NAMESPACE { namespace detail { bool load(handle src, bool) { if (src.is_none()) { value.scale = 0.0; + value.length = 0.0; + value.randomness = 0.0; return true; } From a47e26bd8583f0fcacc1dd83c3f2ac39b1f7a091 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 6 Oct 2023 00:46:47 -0400 Subject: [PATCH 0250/1230] Add a pybind11 type caster for agg::rgba --- src/py_converters_11.h | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/py_converters_11.h b/src/py_converters_11.h index 7f3b7846153e..a13abd2335b5 100644 --- a/src/py_converters_11.h +++ b/src/py_converters_11.h @@ -9,6 +9,7 @@ namespace py = pybind11; #include "agg_basics.h" +#include "agg_color_rgba.h" #include "agg_trans_affine.h" #include "path_converters.h" @@ -58,6 +59,36 @@ namespace PYBIND11_NAMESPACE { namespace detail { } }; + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::rgba, const_name("rgba")); + + bool load(handle src, bool) { + if (src.is_none()) { + value.r = 0.0; + value.g = 0.0; + value.b = 0.0; + value.a = 0.0; + } else { + auto rgbatuple = src.cast(); + value.r = rgbatuple[0].cast(); + value.g = rgbatuple[1].cast(); + value.b = rgbatuple[2].cast(); + switch (rgbatuple.size()) { + case 4: + value.a = rgbatuple[3].cast(); + break; + case 3: + value.a = 1.0; + break; + default: + throw py::value_error("RGBA value must be 3- or 4-tuple"); + } + } + return true; + } + }; + template <> struct type_caster { public: PYBIND11_TYPE_CASTER(agg::trans_affine, const_name("trans_affine")); From 597554db667344c4c4ad026ec1c6dd5f4c688d8f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 6 Oct 2023 02:46:10 -0400 Subject: [PATCH 0251/1230] Add a pybind11 type caster for GCAgg and its requirements --- src/_backend_agg_wrapper.cpp | 51 +++----------- src/py_converters.cpp | 2 - src/py_converters_11.h | 125 +++++++++++++++++++++++++++++++++-- 3 files changed, 130 insertions(+), 48 deletions(-) diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 1486f4772056..22963383335a 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "mplutils.h" #include "numpy_cpp.h" #include "py_converters.h" @@ -41,18 +42,13 @@ PyBufferRegion_get_extents(BufferRegion *self) static void PyRendererAgg_draw_path(RendererAgg *self, - py::object gc_obj, + GCAgg &gc, mpl::PathIterator path, agg::trans_affine trans, py::object face_obj) { - GCAgg gc; agg::rgba face; - if (!convert_gcagg(gc_obj.ptr(), &gc)) { - throw py::error_already_set(); - } - if (!convert_face(face_obj.ptr(), gc, &face)) { throw py::error_already_set(); } @@ -66,37 +62,28 @@ PyRendererAgg_draw_text_image(RendererAgg *self, double x, double y, double angle, - py::object gc_obj) + GCAgg &gc) { numpy::array_view image; - GCAgg gc; if (!image.converter_contiguous(image_obj.ptr(), &image)) { throw py::error_already_set(); } - if (!convert_gcagg(gc_obj.ptr(), &gc)) { - throw py::error_already_set(); - } self->draw_text_image(gc, image, x, y, angle); } static void PyRendererAgg_draw_markers(RendererAgg *self, - py::object gc_obj, + GCAgg &gc, mpl::PathIterator marker_path, agg::trans_affine marker_path_trans, mpl::PathIterator path, agg::trans_affine trans, py::object face_obj) { - GCAgg gc; agg::rgba face; - if (!convert_gcagg(gc_obj.ptr(), &gc)) { - throw py::error_already_set(); - } - if (!convert_face(face_obj.ptr(), gc, &face)) { throw py::error_already_set(); } @@ -106,17 +93,13 @@ PyRendererAgg_draw_markers(RendererAgg *self, static void PyRendererAgg_draw_image(RendererAgg *self, - py::object gc_obj, + GCAgg &gc, double x, double y, py::array_t image_obj) { - GCAgg gc; numpy::array_view image; - if (!convert_gcagg(gc_obj.ptr(), &gc)) { - throw py::error_already_set(); - } if (!image.set(image_obj.ptr())) { throw py::error_already_set(); } @@ -130,7 +113,7 @@ PyRendererAgg_draw_image(RendererAgg *self, static void PyRendererAgg_draw_path_collection(RendererAgg *self, - py::object gc_obj, + GCAgg &gc, agg::trans_affine master_transform, py::object paths_obj, py::object transforms_obj, @@ -139,25 +122,20 @@ PyRendererAgg_draw_path_collection(RendererAgg *self, py::object facecolors_obj, py::object edgecolors_obj, py::object linewidths_obj, - py::object dashes_obj, + DashesVector dashes, py::object antialiaseds_obj, py::object Py_UNUSED(ignored_obj), // offset position is no longer used py::object Py_UNUSED(offset_position_obj)) { - GCAgg gc; mpl::PathGenerator paths; numpy::array_view transforms; numpy::array_view offsets; numpy::array_view facecolors; numpy::array_view edgecolors; numpy::array_view linewidths; - DashesVector dashes; numpy::array_view antialiaseds; - if (!convert_gcagg(gc_obj.ptr(), &gc)) { - throw py::error_already_set(); - } if (!convert_pathgen(paths_obj.ptr(), &paths)) { throw py::error_already_set(); } @@ -176,9 +154,6 @@ PyRendererAgg_draw_path_collection(RendererAgg *self, if (!linewidths.converter(linewidths_obj.ptr(), &linewidths)) { throw py::error_already_set(); } - if (!convert_dashes_vector(dashes_obj.ptr(), &dashes)) { - throw py::error_already_set(); - } if (!antialiaseds.converter(antialiaseds_obj.ptr(), &antialiaseds)) { throw py::error_already_set(); } @@ -198,7 +173,7 @@ PyRendererAgg_draw_path_collection(RendererAgg *self, static void PyRendererAgg_draw_quad_mesh(RendererAgg *self, - py::object gc_obj, + GCAgg &gc, agg::trans_affine master_transform, unsigned int mesh_width, unsigned int mesh_height, @@ -209,15 +184,11 @@ PyRendererAgg_draw_quad_mesh(RendererAgg *self, bool antialiased, py::object edgecolors_obj) { - GCAgg gc; numpy::array_view coordinates; numpy::array_view offsets; numpy::array_view facecolors; numpy::array_view edgecolors; - if (!convert_gcagg(gc_obj.ptr(), &gc)) { - throw py::error_already_set(); - } if (!coordinates.converter(coordinates_obj.ptr(), &coordinates)) { throw py::error_already_set(); } @@ -245,18 +216,14 @@ PyRendererAgg_draw_quad_mesh(RendererAgg *self, static void PyRendererAgg_draw_gouraud_triangles(RendererAgg *self, - py::object gc_obj, + GCAgg &gc, py::object points_obj, py::object colors_obj, agg::trans_affine trans) { - GCAgg gc; numpy::array_view points; numpy::array_view colors; - if (!convert_gcagg(gc_obj.ptr(), &gc)) { - throw py::error_already_set(); - } if (!points.converter(points_obj.ptr(), &points)) { throw py::error_already_set(); } diff --git a/src/py_converters.cpp b/src/py_converters.cpp index e4a04b7bc057..36677da218ca 100644 --- a/src/py_converters.cpp +++ b/src/py_converters.cpp @@ -415,8 +415,6 @@ int convert_pathgen(PyObject *obj, void *pathgenp) int convert_clippath(PyObject *clippath_tuple, void *clippathp) { ClipPath *clippath = (ClipPath *)clippathp; - mpl::PathIterator path; - agg::trans_affine trans; if (clippath_tuple != NULL && clippath_tuple != Py_None) { if (!PyArg_ParseTuple(clippath_tuple, diff --git a/src/py_converters_11.h b/src/py_converters_11.h index a13abd2335b5..ef5d8989c072 100644 --- a/src/py_converters_11.h +++ b/src/py_converters_11.h @@ -8,6 +8,8 @@ namespace py = pybind11; +#include + #include "agg_basics.h" #include "agg_color_rgba.h" #include "agg_trans_affine.h" @@ -135,6 +137,37 @@ namespace PYBIND11_NAMESPACE { namespace detail { /* Remove all this macro magic after dropping NumPy usage and just include `py_adaptors.h`. */ #ifdef MPL_PY_ADAPTORS_H + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::line_cap_e, const_name("line_cap_e")); + + bool load(handle src, bool) { + const std::unordered_map enum_values = { + {"butt", agg::butt_cap}, + {"round", agg::round_cap}, + {"projecting", agg::square_cap}, + }; + value = enum_values.at(src.cast()); + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::line_join_e, const_name("line_join_e")); + + bool load(handle src, bool) { + const std::unordered_map enum_values = { + {"miter", agg::miter_join_revert}, + {"round", agg::round_join}, + {"bevel", agg::bevel_join}, + }; + value = agg::miter_join_revert; + value = enum_values.at(src.cast()); + return true; + } + }; + template <> struct type_caster { public: PYBIND11_TYPE_CASTER(mpl::PathIterator, const_name("PathIterator")); @@ -144,14 +177,14 @@ namespace PYBIND11_NAMESPACE { namespace detail { return true; } - auto vertices = src.attr("vertices"); - auto codes = src.attr("codes"); + py::object vertices = src.attr("vertices"); + py::object codes = src.attr("codes"); auto should_simplify = src.attr("should_simplify").cast(); auto simplify_threshold = src.attr("simplify_threshold").cast(); - if (!value.set(vertices.ptr(), codes.ptr(), + if (!value.set(vertices.inc_ref().ptr(), codes.inc_ref().ptr(), should_simplify, simplify_threshold)) { - return false; + throw py::error_already_set(); } return true; @@ -161,6 +194,64 @@ namespace PYBIND11_NAMESPACE { namespace detail { /* Remove all this macro magic after dropping NumPy usage and just include `_backend_agg_basic_types.h`. */ #ifdef MPL_BACKEND_AGG_BASIC_TYPES_H +# ifndef MPL_PY_ADAPTORS_H +# error "py_adaptors.h must be included to get Agg type casters" +# endif + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(ClipPath, const_name("ClipPath")); + + bool load(handle src, bool) { + if (src.is_none()) { + return true; + } + + auto clippath_tuple = src.cast(); + + auto path = clippath_tuple[0]; + if (!path.is_none()) { + value.path = path.cast(); + } + value.trans = clippath_tuple[1].cast(); + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(Dashes, const_name("Dashes")); + + bool load(handle src, bool) { + auto dash_tuple = src.cast(); + auto dash_offset = dash_tuple[0].cast(); + auto dashes_seq_or_none = dash_tuple[1]; + + if (dashes_seq_or_none.is_none()) { + return true; + } + + auto dashes_seq = dashes_seq_or_none.cast(); + + auto nentries = dashes_seq.size(); + // If the dashpattern has odd length, iterate through it twice (in + // accordance with the pdf/ps/svg specs). + auto dash_pattern_length = (nentries % 2) ? 2 * nentries : nentries; + + for (py::size_t i = 0; i < dash_pattern_length; i += 2) { + auto length = dashes_seq[i % nentries].cast(); + auto skip = dashes_seq[(i + 1) % nentries].cast(); + + value.add_dash_pair(length, skip); + } + + value.set_dash_offset(dash_offset); + + return true; + } + }; + template <> struct type_caster { public: PYBIND11_TYPE_CASTER(SketchParams, const_name("SketchParams")); @@ -179,6 +270,32 @@ namespace PYBIND11_NAMESPACE { namespace detail { return true; } }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(GCAgg, const_name("GCAgg")); + + bool load(handle src, bool) { + value.linewidth = src.attr("_linewidth").cast(); + value.alpha = src.attr("_alpha").cast(); + value.forced_alpha = src.attr("_forced_alpha").cast(); + value.color = src.attr("_rgb").cast(); + value.isaa = src.attr("_antialiased").cast(); + value.cap = src.attr("_capstyle").cast(); + value.join = src.attr("_joinstyle").cast(); + value.dashes = src.attr("get_dashes")().cast(); + value.cliprect = src.attr("_cliprect").cast(); + /* value.clippath = src.attr("get_clip_path")().cast(); */ + convert_clippath(src.attr("get_clip_path")().ptr(), &value.clippath); + value.snap_mode = src.attr("get_snap")().cast(); + value.hatchpath = src.attr("get_hatch_path")().cast(); + value.hatch_color = src.attr("get_hatch_color")().cast(); + value.hatch_linewidth = src.attr("get_hatch_linewidth")().cast(); + value.sketch = src.attr("get_sketch_params")().cast(); + + return true; + } + }; #endif }} // namespace PYBIND11_NAMESPACE::detail From 9cbff610c0b5ab1b375848d49102fad41fdbcaaf Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 11 Sep 2024 06:26:10 -0400 Subject: [PATCH 0252/1230] Inline convert_face type converter --- src/_backend_agg_wrapper.cpp | 22 ++++++++++++---------- src/py_converters.cpp | 15 --------------- src/py_converters.h | 2 -- 3 files changed, 12 insertions(+), 27 deletions(-) diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 22963383335a..5846d45fe1ba 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -45,12 +45,13 @@ PyRendererAgg_draw_path(RendererAgg *self, GCAgg &gc, mpl::PathIterator path, agg::trans_affine trans, - py::object face_obj) + py::object rgbFace) { - agg::rgba face; - - if (!convert_face(face_obj.ptr(), gc, &face)) { - throw py::error_already_set(); + agg::rgba face = rgbFace.cast(); + if (!rgbFace.is_none()) { + if (gc.forced_alpha || rgbFace.cast().size() == 3) { + face.a = gc.alpha; + } } self->draw_path(gc, path, trans, face); @@ -80,12 +81,13 @@ PyRendererAgg_draw_markers(RendererAgg *self, agg::trans_affine marker_path_trans, mpl::PathIterator path, agg::trans_affine trans, - py::object face_obj) + py::object rgbFace) { - agg::rgba face; - - if (!convert_face(face_obj.ptr(), gc, &face)) { - throw py::error_already_set(); + agg::rgba face = rgbFace.cast(); + if (!rgbFace.is_none()) { + if (gc.forced_alpha || rgbFace.cast().size() == 3) { + face.a = gc.alpha; + } } self->draw_markers(gc, marker_path, marker_path_trans, path, trans, face); diff --git a/src/py_converters.cpp b/src/py_converters.cpp index 36677da218ca..dee4b0abfd31 100644 --- a/src/py_converters.cpp +++ b/src/py_converters.cpp @@ -489,21 +489,6 @@ int convert_gcagg(PyObject *pygc, void *gcp) return 1; } -int convert_face(PyObject *color, GCAgg &gc, agg::rgba *rgba) -{ - if (!convert_rgba(color, rgba)) { - return 0; - } - - if (color != NULL && color != Py_None) { - if (gc.forced_alpha || PySequence_Size(color) == 3) { - rgba->a = gc.alpha; - } - } - - return 1; -} - int convert_points(PyObject *obj, void *pointsp) { numpy::array_view *points = (numpy::array_view *)pointsp; diff --git a/src/py_converters.h b/src/py_converters.h index 2c9dc6d1b860..b514efdf5d47 100644 --- a/src/py_converters.h +++ b/src/py_converters.h @@ -41,8 +41,6 @@ int convert_points(PyObject *pygc, void *pointsp); int convert_transforms(PyObject *pygc, void *transp); int convert_bboxes(PyObject *pygc, void *bboxp); int convert_colors(PyObject *pygc, void *colorsp); - -int convert_face(PyObject *color, GCAgg &gc, agg::rgba *rgba); } #endif From 3fde41cddeb031bdfbcc3bb61c81aa70d3d82d75 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 13 Sep 2024 05:46:52 -0400 Subject: [PATCH 0253/1230] Convert some Agg array_view to pybind11 This only does the simple ones that are not shared with the path extension. Those more complex ones will be done separately. --- src/_backend_agg.h | 12 ++++++----- src/_backend_agg_wrapper.cpp | 40 +++++++++++------------------------- 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/src/_backend_agg.h b/src/_backend_agg.h index 3bab3bb785f5..6325df357b1b 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -10,6 +10,7 @@ #include #include +#include #include "agg_alpha_mask_u8.h" #include "agg_conv_curve.h" @@ -732,7 +733,7 @@ inline void RendererAgg::draw_text_image(GCAgg &gc, ImageArray &image, int x, in rendererBase.reset_clipping(true); if (angle != 0.0) { agg::rendering_buffer srcbuf( - image.data(), (unsigned)image.shape(1), + image.mutable_data(0, 0), (unsigned)image.shape(1), (unsigned)image.shape(0), (unsigned)image.shape(1)); agg::pixfmt_gray8 pixf_img(srcbuf); @@ -832,8 +833,9 @@ inline void RendererAgg::draw_image(GCAgg &gc, bool has_clippath = render_clippath(gc.clippath.path, gc.clippath.trans, gc.snap_mode); agg::rendering_buffer buffer; - buffer.attach( - image.data(), (unsigned)image.shape(1), (unsigned)image.shape(0), -(int)image.shape(1) * 4); + buffer.attach(image.mutable_data(0, 0, 0), + (unsigned)image.shape(1), (unsigned)image.shape(0), + -(int)image.shape(1) * 4); pixfmt pixf(buffer); if (has_clippath) { @@ -1249,8 +1251,8 @@ inline void RendererAgg::draw_gouraud_triangles(GCAgg &gc, bool has_clippath = render_clippath(gc.clippath.path, gc.clippath.trans, gc.snap_mode); for (int i = 0; i < points.shape(0); ++i) { - typename PointArray::sub_t point = points.subarray(i); - typename ColorArray::sub_t color = colors.subarray(i); + auto point = std::bind(points, i, std::placeholders::_1, std::placeholders::_2); + auto color = std::bind(colors, i, std::placeholders::_1, std::placeholders::_2); _draw_gouraud_triangle(point, color, trans, has_clippath); } diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 5846d45fe1ba..79cab02e419d 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -59,17 +59,14 @@ PyRendererAgg_draw_path(RendererAgg *self, static void PyRendererAgg_draw_text_image(RendererAgg *self, - py::array_t image_obj, + py::array_t image_obj, double x, double y, double angle, GCAgg &gc) { - numpy::array_view image; - - if (!image.converter_contiguous(image_obj.ptr(), &image)) { - throw py::error_already_set(); - } + // TODO: This really shouldn't be mutable, but Agg's renderer buffers aren't const. + auto image = image_obj.mutable_unchecked<2>(); self->draw_text_image(gc, image, x, y, angle); } @@ -98,13 +95,10 @@ PyRendererAgg_draw_image(RendererAgg *self, GCAgg &gc, double x, double y, - py::array_t image_obj) + py::array_t image_obj) { - numpy::array_view image; - - if (!image.set(image_obj.ptr())) { - throw py::error_already_set(); - } + // TODO: This really shouldn't be mutable, but Agg's renderer buffers aren't const. + auto image = image_obj.mutable_unchecked<3>(); x = mpl_round(x); y = mpl_round(y); @@ -179,21 +173,18 @@ PyRendererAgg_draw_quad_mesh(RendererAgg *self, agg::trans_affine master_transform, unsigned int mesh_width, unsigned int mesh_height, - py::object coordinates_obj, + py::array_t coordinates_obj, py::object offsets_obj, agg::trans_affine offset_trans, py::object facecolors_obj, bool antialiased, py::object edgecolors_obj) { - numpy::array_view coordinates; numpy::array_view offsets; numpy::array_view facecolors; numpy::array_view edgecolors; - if (!coordinates.converter(coordinates_obj.ptr(), &coordinates)) { - throw py::error_already_set(); - } + auto coordinates = coordinates_obj.mutable_unchecked<3>(); if (!convert_points(offsets_obj.ptr(), &offsets)) { throw py::error_already_set(); } @@ -219,19 +210,12 @@ PyRendererAgg_draw_quad_mesh(RendererAgg *self, static void PyRendererAgg_draw_gouraud_triangles(RendererAgg *self, GCAgg &gc, - py::object points_obj, - py::object colors_obj, + py::array_t points_obj, + py::array_t colors_obj, agg::trans_affine trans) { - numpy::array_view points; - numpy::array_view colors; - - if (!points.converter(points_obj.ptr(), &points)) { - throw py::error_already_set(); - } - if (!colors.converter(colors_obj.ptr(), &colors)) { - throw py::error_already_set(); - } + auto points = points_obj.unchecked<3>(); + auto colors = colors_obj.unchecked<3>(); self->draw_gouraud_triangles(gc, points, colors, trans); } From 9ea90b5abc6819f59accab2ba7ab614960b1d57b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 13 Sep 2024 22:22:54 -0400 Subject: [PATCH 0254/1230] DOC: Fix missing cross-reference checks for sphinx-tags In cda437289b0cf41a01571904b6a13bedf8f037a4, I "simplified" from `node['refdomain']` to the existing `domain` argument. With `sphinx-tags`, the `warn-missing-reference` event occurs for a `None` domain, and we crash trying to access `domain.name`. But `node['refdomain']` is still an empty string, so revert back to that. --- doc/sphinxext/missing_references.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/sphinxext/missing_references.py b/doc/sphinxext/missing_references.py index 9c3b8256cd91..87432bc524b4 100644 --- a/doc/sphinxext/missing_references.py +++ b/doc/sphinxext/missing_references.py @@ -100,10 +100,11 @@ def handle_missing_reference(app, domain, node): #. record missing references for saving/comparing with ignored list. #. prevent Sphinx from raising a warning on ignored references. """ - typ = node["reftype"] + refdomain = node["refdomain"] + reftype = node["reftype"] target = node["reftarget"] location = get_location(node, app) - domain_type = f"{domain.name}:{typ}" + domain_type = f"{refdomain}:{reftype}" app.env.missing_references_events[(domain_type, target)].add(location) From f43b7467d4c4f0ac95e6cad88232870fc32adc7c Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 13 Sep 2024 22:49:22 -0400 Subject: [PATCH 0255/1230] CI: Add Sphinx extensions as setting `Documentation: build` label --- .github/labeler.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/labeler.yml b/.github/labeler.yml index 43a1246ba68a..75adfed57f43 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -89,6 +89,7 @@ - 'doc/conf.py' - 'doc/Makefile' - 'doc/make.bat' + - 'doc/sphinxext/**' "Documentation: devdocs": - changed-files: - any-glob-to-any-file: From 957271f83f80aff2878c4ce0785d0fa2eb54b229 Mon Sep 17 00:00:00 2001 From: Michael Hinton Date: Fri, 13 Sep 2024 14:52:37 -0700 Subject: [PATCH 0256/1230] Resolve configdir to handle potential issues with inaccessible symlinks There are some use cases where a user might have a symlinked home directory (e.g. a corporate home directory may be symlinked to a disk with limited space, and is only accessible to other users via a real path to the underlying disk). Resolving configdir before any mkdir or access checks should avoid this problem. Co-authored-by: Greg Lucas --- lib/matplotlib/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index b20af9108bd0..a8d876c30afa 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -519,13 +519,15 @@ def _get_xdg_cache_dir(): def _get_config_or_cache_dir(xdg_base_getter): configdir = os.environ.get('MPLCONFIGDIR') if configdir: - configdir = Path(configdir).resolve() + configdir = Path(configdir) elif sys.platform.startswith(('linux', 'freebsd')): # Only call _xdg_base_getter here so that MPLCONFIGDIR is tried first, # as _xdg_base_getter can throw. configdir = Path(xdg_base_getter(), "matplotlib") else: configdir = Path.home() / ".matplotlib" + # Resolve the path to handle potential issues with inaccessible symlinks. + configdir = configdir.resolve() try: configdir.mkdir(parents=True, exist_ok=True) except OSError: From 8837d9ad97881cb4c4095a04711b32209420cb0b Mon Sep 17 00:00:00 2001 From: Michael Hinton Date: Fri, 13 Sep 2024 23:18:51 -0700 Subject: [PATCH 0257/1230] Print warning when mkdir fails instead of silently ignoring the error For example, a user could have no more disk space, causing mkdir to fail, but not realize it because the error is silently discarded, and the subsequent warning would not indicate the actual issue. --- lib/matplotlib/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index a8d876c30afa..23643a53d811 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -530,26 +530,27 @@ def _get_config_or_cache_dir(xdg_base_getter): configdir = configdir.resolve() try: configdir.mkdir(parents=True, exist_ok=True) - except OSError: - pass + except OSError as exc: + _log.warning("mkdir -p failed for path %s: %s", configdir, exc) else: if os.access(str(configdir), os.W_OK) and configdir.is_dir(): return str(configdir) + _log.warning("%s is not a writable directory", configdir) # If the config or cache directory cannot be created or is not a writable # directory, create a temporary one. try: tmpdir = tempfile.mkdtemp(prefix="matplotlib-") except OSError as exc: raise OSError( - f"Matplotlib requires access to a writable cache directory, but the " - f"default path ({configdir}) is not a writable directory, and a temporary " + f"Matplotlib requires access to a writable cache directory, but there " + f"was an issue with the default path ({configdir}), and a temporary " f"directory could not be created; set the MPLCONFIGDIR environment " f"variable to a writable directory") from exc os.environ["MPLCONFIGDIR"] = tmpdir atexit.register(shutil.rmtree, tmpdir) _log.warning( - "Matplotlib created a temporary cache directory at %s because the default path " - "(%s) is not a writable directory; it is highly recommended to set the " + "Matplotlib created a temporary cache directory at %s because there was " + "an issue with the default path (%s); it is highly recommended to set the " "MPLCONFIGDIR environment variable to a writable directory, in particular to " "speed up the import of Matplotlib and to better support multiprocessing.", tmpdir, configdir) From 5db8071e7647733107274154a730e196c0ab2c3b Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 7 Sep 2024 09:25:04 +0100 Subject: [PATCH 0258/1230] Handle single colors in ContourSet --- doc/users/next_whats_new/contour_color.rst | 21 +++++++++++++++++ lib/matplotlib/contour.py | 27 ++++++++++++++-------- lib/matplotlib/tests/test_contour.py | 14 +++++++++++ 3 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 doc/users/next_whats_new/contour_color.rst diff --git a/doc/users/next_whats_new/contour_color.rst b/doc/users/next_whats_new/contour_color.rst new file mode 100644 index 000000000000..1f7a326ec2b5 --- /dev/null +++ b/doc/users/next_whats_new/contour_color.rst @@ -0,0 +1,21 @@ +Specifying a single color in ``contour`` and ``contourf`` +--------------------------------------------------------- + +`~.Axes.contour` and `~.Axes.contourf` previously accepted a single color +provided it was expressed as a string. This restriction has now been removed +and a single color in any format described in the :ref:`colors_def` tutorial +may be passed. + +.. plot:: + :include-source: true + :alt: Two-panel example contour plots. The left panel has all transparent red contours. The right panel has all dark blue contours. + + import matplotlib.pyplot as plt + + fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(6, 3)) + z = [[0, 1], [1, 2]] + + ax1.contour(z, colors=('r', 0.4)) + ax2.contour(z, colors=(0.1, 0.2, 0.5)) + + plt.show() diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 503ec2747f10..e2b857a57a7f 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -702,6 +702,11 @@ def __init__(self, ax, *args, self._extend_min = self.extend in ['min', 'both'] self._extend_max = self.extend in ['max', 'both'] if self.colors is not None: + if mcolors.is_color_like(self.colors): + color_sequence = [self.colors] + else: + color_sequence = self.colors + ncolors = len(self.levels) if self.filled: ncolors -= 1 @@ -718,19 +723,19 @@ def __init__(self, ax, *args, total_levels = (ncolors + int(self._extend_min) + int(self._extend_max)) - if (len(self.colors) == total_levels and + if (len(color_sequence) == total_levels and (self._extend_min or self._extend_max)): use_set_under_over = True if self._extend_min: i0 = 1 - cmap = mcolors.ListedColormap(self.colors[i0:None], N=ncolors) + cmap = mcolors.ListedColormap(color_sequence[i0:None], N=ncolors) if use_set_under_over: if self._extend_min: - cmap.set_under(self.colors[0]) + cmap.set_under(color_sequence[0]) if self._extend_max: - cmap.set_over(self.colors[-1]) + cmap.set_over(color_sequence[-1]) # label lists must be initialized here self.labelTexts = [] @@ -1498,10 +1503,12 @@ def _initialize_x_y(self, z): The sequence is cycled for the levels in ascending order. If the sequence is shorter than the number of levels, it's repeated. - As a shortcut, single color strings may be used in place of - one-element lists, i.e. ``'red'`` instead of ``['red']`` to color - all levels with the same color. This shortcut does only work for - color strings, not for other ways of specifying colors. + As a shortcut, a single color may be used in place of one-element lists, i.e. + ``'red'`` instead of ``['red']`` to color all levels with the same color. + + .. versionchanged:: 3.10 + Previously a single color had to be expressed as a string, but now any + valid color format may be passed. By default (value *None*), the colormap specified by *cmap* will be used. @@ -1569,10 +1576,10 @@ def _initialize_x_y(self, z): An existing `.QuadContourSet` does not get notified if properties of its colormap are changed. Therefore, an explicit - call `.QuadContourSet.changed()` is needed after modifying the + call ``QuadContourSet.changed()`` is needed after modifying the colormap. The explicit call can be left out, if a colorbar is assigned to the `.QuadContourSet` because it internally calls - `.QuadContourSet.changed()`. + ``QuadContourSet.changed()``. Example:: diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index 6211b2d8418b..8ccff360a51a 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -171,6 +171,20 @@ def test_given_colors_levels_and_extends(): plt.colorbar(c, ax=ax) +@pytest.mark.parametrize('color, extend', [('darkred', 'neither'), + ('darkred', 'both'), + (('r', 0.5), 'neither'), + ((0.1, 0.2, 0.5, 0.3), 'neither')]) +def test_single_color_and_extend(color, extend): + z = [[0, 1], [1, 2]] + + _, ax = plt.subplots() + levels = [0.5, 0.75, 1, 1.25, 1.5] + cs = ax.contour(z, levels=levels, colors=color, extend=extend) + for c in cs.get_edgecolors(): + assert same_color(c, color) + + @image_comparison(['contour_log_locator.svg'], style='mpl20', remove_text=False) def test_log_locator_levels(): From f5abe2c61972eaefa570259ac163052565baf309 Mon Sep 17 00:00:00 2001 From: farquh Date: Sat, 13 Jul 2024 16:24:39 -0400 Subject: [PATCH 0259/1230] added tags to mplot3dexamples --- galleries/examples/mplot3d/2dcollections3d.py | 6 ++++++ galleries/examples/mplot3d/3d_bars.py | 7 +++++++ galleries/examples/mplot3d/bars3d.py | 6 ++++++ galleries/examples/mplot3d/box3d.py | 5 +++++ galleries/examples/mplot3d/contour3d.py | 5 +++++ galleries/examples/mplot3d/contour3d_2.py | 5 +++++ galleries/examples/mplot3d/contour3d_3.py | 6 ++++++ galleries/examples/mplot3d/contourf3d.py | 5 +++++ galleries/examples/mplot3d/contourf3d_2.py | 6 ++++++ galleries/examples/mplot3d/custom_shaded_3d_surface.py | 6 ++++++ galleries/examples/mplot3d/errorbar3d.py | 6 ++++++ galleries/examples/mplot3d/hist3d.py | 6 ++++++ galleries/examples/mplot3d/imshow3d.py | 6 ++++++ galleries/examples/mplot3d/intersecting_planes.py | 6 ++++++ galleries/examples/mplot3d/lines3d.py | 5 +++++ galleries/examples/mplot3d/lorenz_attractor.py | 5 +++++ galleries/examples/mplot3d/mixed_subplots.py | 6 ++++++ galleries/examples/mplot3d/offset.py | 7 +++++++ galleries/examples/mplot3d/pathpatch3d.py | 6 ++++++ galleries/examples/mplot3d/polys3d.py | 6 ++++++ galleries/examples/mplot3d/projections.py | 7 +++++++ galleries/examples/mplot3d/quiver3d.py | 5 +++++ galleries/examples/mplot3d/rotate_axes3d_sgskip.py | 7 +++++++ galleries/examples/mplot3d/scatter3d.py | 5 +++++ galleries/examples/mplot3d/stem3d_demo.py | 5 +++++ galleries/examples/mplot3d/subplot3d.py | 6 ++++++ galleries/examples/mplot3d/surface3d.py | 5 +++++ galleries/examples/mplot3d/surface3d_2.py | 5 +++++ galleries/examples/mplot3d/surface3d_3.py | 6 ++++++ galleries/examples/mplot3d/surface3d_radial.py | 5 +++++ galleries/examples/mplot3d/text3d.py | 6 ++++++ galleries/examples/mplot3d/tricontour3d.py | 5 +++++ galleries/examples/mplot3d/tricontourf3d.py | 5 +++++ galleries/examples/mplot3d/trisurf3d.py | 5 +++++ galleries/examples/mplot3d/trisurf3d_2.py | 5 +++++ galleries/examples/mplot3d/view_planes_3d.py | 6 ++++++ galleries/examples/mplot3d/voxels.py | 5 +++++ galleries/examples/mplot3d/voxels_numpy_logo.py | 6 ++++++ galleries/examples/mplot3d/voxels_rgb.py | 5 +++++ galleries/examples/mplot3d/voxels_torus.py | 6 ++++++ galleries/examples/mplot3d/wire3d.py | 5 +++++ galleries/examples/mplot3d/wire3d_animation_sgskip.py | 6 ++++++ galleries/examples/mplot3d/wire3d_zero_stride.py | 5 +++++ 43 files changed, 242 insertions(+) diff --git a/galleries/examples/mplot3d/2dcollections3d.py b/galleries/examples/mplot3d/2dcollections3d.py index a0155ebb0773..ae88776d133e 100644 --- a/galleries/examples/mplot3d/2dcollections3d.py +++ b/galleries/examples/mplot3d/2dcollections3d.py @@ -46,3 +46,9 @@ ax.view_init(elev=20., azim=-35, roll=0) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: scatter, plot-type: line, +# component: axes, +# level: intermediate diff --git a/galleries/examples/mplot3d/3d_bars.py b/galleries/examples/mplot3d/3d_bars.py index 40a09ae33f68..9d8feeaeb12b 100644 --- a/galleries/examples/mplot3d/3d_bars.py +++ b/galleries/examples/mplot3d/3d_bars.py @@ -31,3 +31,10 @@ ax2.set_title('Not Shaded') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# styling: texture, +# plot-type: bar, +# level: beginner diff --git a/galleries/examples/mplot3d/bars3d.py b/galleries/examples/mplot3d/bars3d.py index 21314057311a..3ea4a100c2f6 100644 --- a/galleries/examples/mplot3d/bars3d.py +++ b/galleries/examples/mplot3d/bars3d.py @@ -40,3 +40,9 @@ ax.set_yticks(yticks) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: bar, +# styling: color, +# level: beginner diff --git a/galleries/examples/mplot3d/box3d.py b/galleries/examples/mplot3d/box3d.py index bbe4accec183..807e3d496ec6 100644 --- a/galleries/examples/mplot3d/box3d.py +++ b/galleries/examples/mplot3d/box3d.py @@ -76,3 +76,8 @@ # Show Figure plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: intermediate diff --git a/galleries/examples/mplot3d/contour3d.py b/galleries/examples/mplot3d/contour3d.py index fb2e5bb5a30d..6ac98bc47ab1 100644 --- a/galleries/examples/mplot3d/contour3d.py +++ b/galleries/examples/mplot3d/contour3d.py @@ -18,3 +18,8 @@ ax.contour(X, Y, Z, cmap=cm.coolwarm) # Plot contour curves plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/contour3d_2.py b/galleries/examples/mplot3d/contour3d_2.py index 1283deb27c81..0f1aac1450a8 100644 --- a/galleries/examples/mplot3d/contour3d_2.py +++ b/galleries/examples/mplot3d/contour3d_2.py @@ -17,3 +17,8 @@ ax.contour(X, Y, Z, extend3d=True, cmap=cm.coolwarm) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/contour3d_3.py b/galleries/examples/mplot3d/contour3d_3.py index 6f73fea85dcb..92adb97fc04e 100644 --- a/galleries/examples/mplot3d/contour3d_3.py +++ b/galleries/examples/mplot3d/contour3d_3.py @@ -29,3 +29,9 @@ xlabel='X', ylabel='Y', zlabel='Z') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: axes, +# level: intermediate diff --git a/galleries/examples/mplot3d/contourf3d.py b/galleries/examples/mplot3d/contourf3d.py index 9f7157eab82a..2512179c3e54 100644 --- a/galleries/examples/mplot3d/contourf3d.py +++ b/galleries/examples/mplot3d/contourf3d.py @@ -20,3 +20,8 @@ ax.contourf(X, Y, Z, cmap=cm.coolwarm) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/contourf3d_2.py b/galleries/examples/mplot3d/contourf3d_2.py index 1530aee5e87f..58fede4e3ab5 100644 --- a/galleries/examples/mplot3d/contourf3d_2.py +++ b/galleries/examples/mplot3d/contourf3d_2.py @@ -29,3 +29,9 @@ xlabel='X', ylabel='Y', zlabel='Z') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: axes, +# level: intermediate diff --git a/galleries/examples/mplot3d/custom_shaded_3d_surface.py b/galleries/examples/mplot3d/custom_shaded_3d_surface.py index 677bfa179a83..1a9fa8d4f7eb 100644 --- a/galleries/examples/mplot3d/custom_shaded_3d_surface.py +++ b/galleries/examples/mplot3d/custom_shaded_3d_surface.py @@ -34,3 +34,9 @@ linewidth=0, antialiased=False, shade=False) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: intermediate, +# domain: cartography diff --git a/galleries/examples/mplot3d/errorbar3d.py b/galleries/examples/mplot3d/errorbar3d.py index e4da658d194b..1ece3ca1e8cf 100644 --- a/galleries/examples/mplot3d/errorbar3d.py +++ b/galleries/examples/mplot3d/errorbar3d.py @@ -27,3 +27,9 @@ ax.set_zlabel("Z label") plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: error, +# level: beginner diff --git a/galleries/examples/mplot3d/hist3d.py b/galleries/examples/mplot3d/hist3d.py index e602f7f1e6c5..65d0d60958d8 100644 --- a/galleries/examples/mplot3d/hist3d.py +++ b/galleries/examples/mplot3d/hist3d.py @@ -31,3 +31,9 @@ ax.bar3d(xpos, ypos, zpos, dx, dy, dz, zsort='average') plt.show() + + +# %% +# .. tags:: +# plot-type: 3D, plot-type: histogram, +# level: beginner diff --git a/galleries/examples/mplot3d/imshow3d.py b/galleries/examples/mplot3d/imshow3d.py index 557d96e1bce5..dba962734bbe 100644 --- a/galleries/examples/mplot3d/imshow3d.py +++ b/galleries/examples/mplot3d/imshow3d.py @@ -86,3 +86,9 @@ def imshow3d(ax, array, value_direction='z', pos=0, norm=None, cmap=None): imshow3d(ax, data_zx, value_direction='y', pos=ny, cmap='plasma') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# styling: colormap, +# level: advanced diff --git a/galleries/examples/mplot3d/intersecting_planes.py b/galleries/examples/mplot3d/intersecting_planes.py index b8aa08fd7e18..a5a92caf5c6b 100644 --- a/galleries/examples/mplot3d/intersecting_planes.py +++ b/galleries/examples/mplot3d/intersecting_planes.py @@ -87,3 +87,9 @@ def figure_3D_array_slices(array, cmap=None): figure_3D_array_slices(r_square, cmap='viridis_r') plt.show() + + +# %% +# .. tags:: +# plot-type: 3D, +# level: advanced diff --git a/galleries/examples/mplot3d/lines3d.py b/galleries/examples/mplot3d/lines3d.py index 2fe3b8f30177..ee38dade6997 100644 --- a/galleries/examples/mplot3d/lines3d.py +++ b/galleries/examples/mplot3d/lines3d.py @@ -22,3 +22,8 @@ ax.legend() plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/lorenz_attractor.py b/galleries/examples/mplot3d/lorenz_attractor.py index 0ac54a7adb9b..72d25ea544cb 100644 --- a/galleries/examples/mplot3d/lorenz_attractor.py +++ b/galleries/examples/mplot3d/lorenz_attractor.py @@ -59,3 +59,8 @@ def lorenz(xyz, *, s=10, r=28, b=2.667): ax.set_title("Lorenz Attractor") plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: intermediate diff --git a/galleries/examples/mplot3d/mixed_subplots.py b/galleries/examples/mplot3d/mixed_subplots.py index dc196f05f90d..a38fd2e10a2b 100644 --- a/galleries/examples/mplot3d/mixed_subplots.py +++ b/galleries/examples/mplot3d/mixed_subplots.py @@ -44,3 +44,9 @@ def f(t): ax.set_zlim(-1, 1) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: subplot, +# level: beginner diff --git a/galleries/examples/mplot3d/offset.py b/galleries/examples/mplot3d/offset.py index 78da5c6b51c3..4c5e4b06b62b 100644 --- a/galleries/examples/mplot3d/offset.py +++ b/galleries/examples/mplot3d/offset.py @@ -29,3 +29,10 @@ ax.set_zlim(0, 2) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: label, +# interactivity: pan, +# level: beginner diff --git a/galleries/examples/mplot3d/pathpatch3d.py b/galleries/examples/mplot3d/pathpatch3d.py index 335b68003d31..8cb7c4951809 100644 --- a/galleries/examples/mplot3d/pathpatch3d.py +++ b/galleries/examples/mplot3d/pathpatch3d.py @@ -69,3 +69,9 @@ def text3d(ax, xyz, s, zdir="z", size=None, angle=0, usetex=False, **kwargs): ax.set_zlim(0, 10) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: label, +# level: advanced diff --git a/galleries/examples/mplot3d/polys3d.py b/galleries/examples/mplot3d/polys3d.py index 635c929908f6..19979ceddaa5 100644 --- a/galleries/examples/mplot3d/polys3d.py +++ b/galleries/examples/mplot3d/polys3d.py @@ -33,3 +33,9 @@ ax.set_aspect('equalxy') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# styling: colormap, +# level: intermediate diff --git a/galleries/examples/mplot3d/projections.py b/galleries/examples/mplot3d/projections.py index 4fdeb6729687..ff9d88ccb5cd 100644 --- a/galleries/examples/mplot3d/projections.py +++ b/galleries/examples/mplot3d/projections.py @@ -53,3 +53,10 @@ axs[2].set_title("'persp'\nfocal_length = 0.2", fontsize=10) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# styling: small-multiples, +# component: subplot, +# level: intermediate diff --git a/galleries/examples/mplot3d/quiver3d.py b/galleries/examples/mplot3d/quiver3d.py index 1eba869c83b8..adc58c2e9d89 100644 --- a/galleries/examples/mplot3d/quiver3d.py +++ b/galleries/examples/mplot3d/quiver3d.py @@ -25,3 +25,8 @@ ax.quiver(x, y, z, u, v, w, length=0.1, normalize=True) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/rotate_axes3d_sgskip.py b/galleries/examples/mplot3d/rotate_axes3d_sgskip.py index 4474fab97460..76a3369a20d6 100644 --- a/galleries/examples/mplot3d/rotate_axes3d_sgskip.py +++ b/galleries/examples/mplot3d/rotate_axes3d_sgskip.py @@ -49,3 +49,10 @@ plt.draw() plt.pause(.001) + +# %% +# .. tags:: +# plot-type: 3D, +# component: animation, +# level: advanced, +# internal: high-bandwidth diff --git a/galleries/examples/mplot3d/scatter3d.py b/galleries/examples/mplot3d/scatter3d.py index 6db0ac9222bc..0fc9bf3fe8da 100644 --- a/galleries/examples/mplot3d/scatter3d.py +++ b/galleries/examples/mplot3d/scatter3d.py @@ -38,3 +38,8 @@ def randrange(n, vmin, vmax): ax.set_zlabel('Z Label') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: scatter, +# level: beginner diff --git a/galleries/examples/mplot3d/stem3d_demo.py b/galleries/examples/mplot3d/stem3d_demo.py index 6f1773c1b505..6e45e7e75c72 100644 --- a/galleries/examples/mplot3d/stem3d_demo.py +++ b/galleries/examples/mplot3d/stem3d_demo.py @@ -49,3 +49,8 @@ ax.set(xlabel='x', ylabel='y', zlabel='z') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: speciality, +# level: beginner diff --git a/galleries/examples/mplot3d/subplot3d.py b/galleries/examples/mplot3d/subplot3d.py index 47e374dc74b9..67d6a81b9e87 100644 --- a/galleries/examples/mplot3d/subplot3d.py +++ b/galleries/examples/mplot3d/subplot3d.py @@ -43,3 +43,9 @@ ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: subplot, +# level: advanced diff --git a/galleries/examples/mplot3d/surface3d.py b/galleries/examples/mplot3d/surface3d.py index e92e6aabd52c..d8a67d40d8b8 100644 --- a/galleries/examples/mplot3d/surface3d.py +++ b/galleries/examples/mplot3d/surface3d.py @@ -53,3 +53,8 @@ # - `matplotlib.axis.Axis.set_major_locator` # - `matplotlib.ticker.LinearLocator` # - `matplotlib.ticker.StrMethodFormatter` +# +# .. tags:: +# plot-type: 3D, +# styling: colormap, +# level: advanced diff --git a/galleries/examples/mplot3d/surface3d_2.py b/galleries/examples/mplot3d/surface3d_2.py index 37ca667d688a..2a4406abc259 100644 --- a/galleries/examples/mplot3d/surface3d_2.py +++ b/galleries/examples/mplot3d/surface3d_2.py @@ -26,3 +26,8 @@ ax.set_aspect('equal') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/surface3d_3.py b/galleries/examples/mplot3d/surface3d_3.py index a2aca4ca3059..c129ef6d3635 100644 --- a/galleries/examples/mplot3d/surface3d_3.py +++ b/galleries/examples/mplot3d/surface3d_3.py @@ -38,3 +38,9 @@ ax.zaxis.set_major_locator(LinearLocator(6)) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# styling: color, styling: texture, +# level: intermediate diff --git a/galleries/examples/mplot3d/surface3d_radial.py b/galleries/examples/mplot3d/surface3d_radial.py index 0d27c9b58cbb..43edd68ee28e 100644 --- a/galleries/examples/mplot3d/surface3d_radial.py +++ b/galleries/examples/mplot3d/surface3d_radial.py @@ -35,3 +35,8 @@ ax.set_zlabel(r'$V(\phi)$') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: polar, +# level: beginner diff --git a/galleries/examples/mplot3d/text3d.py b/galleries/examples/mplot3d/text3d.py index 165ae556c334..881ecfaf406e 100644 --- a/galleries/examples/mplot3d/text3d.py +++ b/galleries/examples/mplot3d/text3d.py @@ -44,3 +44,9 @@ ax.set_zlabel('Z axis') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: annotation, +# level: beginner diff --git a/galleries/examples/mplot3d/tricontour3d.py b/galleries/examples/mplot3d/tricontour3d.py index abf72103d098..fda8de784d71 100644 --- a/galleries/examples/mplot3d/tricontour3d.py +++ b/galleries/examples/mplot3d/tricontour3d.py @@ -43,3 +43,8 @@ ax.view_init(elev=45.) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: specialty, +# level: intermediate diff --git a/galleries/examples/mplot3d/tricontourf3d.py b/galleries/examples/mplot3d/tricontourf3d.py index 94cee6b3aaa9..edf79495e374 100644 --- a/galleries/examples/mplot3d/tricontourf3d.py +++ b/galleries/examples/mplot3d/tricontourf3d.py @@ -44,3 +44,8 @@ ax.view_init(elev=45.) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: specialty, +# level: intermediate diff --git a/galleries/examples/mplot3d/trisurf3d.py b/galleries/examples/mplot3d/trisurf3d.py index 2d288908ab69..f4e7444a4311 100644 --- a/galleries/examples/mplot3d/trisurf3d.py +++ b/galleries/examples/mplot3d/trisurf3d.py @@ -30,3 +30,8 @@ ax.plot_trisurf(x, y, z, linewidth=0.2, antialiased=True) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: intermediate diff --git a/galleries/examples/mplot3d/trisurf3d_2.py b/galleries/examples/mplot3d/trisurf3d_2.py index cb53aabbea1d..b04aa5efb0b1 100644 --- a/galleries/examples/mplot3d/trisurf3d_2.py +++ b/galleries/examples/mplot3d/trisurf3d_2.py @@ -77,3 +77,8 @@ plt.show() + +# %% +# .. tags:: +# plot-type: 3D, plot-type: specialty, +# level: intermediate diff --git a/galleries/examples/mplot3d/view_planes_3d.py b/galleries/examples/mplot3d/view_planes_3d.py index c4322d60fe93..1cac9d61ad1f 100644 --- a/galleries/examples/mplot3d/view_planes_3d.py +++ b/galleries/examples/mplot3d/view_planes_3d.py @@ -55,3 +55,9 @@ def annotate_axes(ax, text, fontsize=18): axd['L'].set_axis_off() plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# component: axes, component: subplot, +# level: beginner diff --git a/galleries/examples/mplot3d/voxels.py b/galleries/examples/mplot3d/voxels.py index 7bd9cf45a2b0..ec9f0f413f3a 100644 --- a/galleries/examples/mplot3d/voxels.py +++ b/galleries/examples/mplot3d/voxels.py @@ -32,3 +32,8 @@ ax.voxels(voxelarray, facecolors=colors, edgecolor='k') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/voxels_numpy_logo.py b/galleries/examples/mplot3d/voxels_numpy_logo.py index 34eb48dcbe8a..c128f055cbe6 100644 --- a/galleries/examples/mplot3d/voxels_numpy_logo.py +++ b/galleries/examples/mplot3d/voxels_numpy_logo.py @@ -45,3 +45,9 @@ def explode(data): ax.set_aspect('equal') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner, +# purpose: fun diff --git a/galleries/examples/mplot3d/voxels_rgb.py b/galleries/examples/mplot3d/voxels_rgb.py index 3ee1e1eab1a6..6f201b08b386 100644 --- a/galleries/examples/mplot3d/voxels_rgb.py +++ b/galleries/examples/mplot3d/voxels_rgb.py @@ -42,3 +42,8 @@ def midpoints(x): ax.set_aspect('equal') plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# styling: color diff --git a/galleries/examples/mplot3d/voxels_torus.py b/galleries/examples/mplot3d/voxels_torus.py index 98621b60976c..db0fdbc6ea4d 100644 --- a/galleries/examples/mplot3d/voxels_torus.py +++ b/galleries/examples/mplot3d/voxels_torus.py @@ -44,3 +44,9 @@ def midpoints(x): linewidth=0.5) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# styling: color, +# level: intermediate diff --git a/galleries/examples/mplot3d/wire3d.py b/galleries/examples/mplot3d/wire3d.py index 9849c8bebf56..357234f51174 100644 --- a/galleries/examples/mplot3d/wire3d.py +++ b/galleries/examples/mplot3d/wire3d.py @@ -20,3 +20,8 @@ ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10) plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: beginner diff --git a/galleries/examples/mplot3d/wire3d_animation_sgskip.py b/galleries/examples/mplot3d/wire3d_animation_sgskip.py index a735f41d94b6..903ff4918586 100644 --- a/galleries/examples/mplot3d/wire3d_animation_sgskip.py +++ b/galleries/examples/mplot3d/wire3d_animation_sgskip.py @@ -39,3 +39,9 @@ plt.pause(.001) print('Average FPS: %f' % (100 / (time.time() - tstart))) + +# %% +# .. tags:: +# plot-type: 3D, +# component: animation, +# level: beginner diff --git a/galleries/examples/mplot3d/wire3d_zero_stride.py b/galleries/examples/mplot3d/wire3d_zero_stride.py index fe45b6c16fcf..ff6a14984b5d 100644 --- a/galleries/examples/mplot3d/wire3d_zero_stride.py +++ b/galleries/examples/mplot3d/wire3d_zero_stride.py @@ -27,3 +27,8 @@ plt.tight_layout() plt.show() + +# %% +# .. tags:: +# plot-type: 3D, +# level: intermediate From 7c074e69afaf7f290acf0a9e9b132aa7e14b4e9d Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:58:36 +0200 Subject: [PATCH 0260/1230] MNT: Replace _docstring.dedent_interpd by its alias _docstring.interpd Historically, they were the different, but are the same for several years. Since the whole `_docstring` module has become private, we can simply remove the alias `dedent_interpd`. Note that `interpd` nowadays also handles indentation smartly, so we don't have to care about "dedenting". --- lib/matplotlib/_docstring.py | 2 +- lib/matplotlib/axes/_axes.py | 84 ++++++++++---------- lib/matplotlib/axes/_base.py | 2 +- lib/matplotlib/contour.py | 4 +- lib/matplotlib/figure.py | 10 +-- lib/matplotlib/legend.py | 4 +- lib/matplotlib/mlab.py | 8 +- lib/matplotlib/offsetbox.py | 2 +- lib/matplotlib/patches.py | 46 +++++------ lib/matplotlib/pyplot.py | 4 +- lib/matplotlib/sankey.py | 2 +- lib/matplotlib/spines.py | 2 +- lib/matplotlib/table.py | 4 +- lib/matplotlib/tri/_tricontour.py | 6 +- lib/mpl_toolkits/axes_grid1/inset_locator.py | 14 ++-- 15 files changed, 97 insertions(+), 97 deletions(-) diff --git a/lib/matplotlib/_docstring.py b/lib/matplotlib/_docstring.py index 6c80b080af4c..7e9448fd63c8 100644 --- a/lib/matplotlib/_docstring.py +++ b/lib/matplotlib/_docstring.py @@ -122,4 +122,4 @@ def do_copy(target): # Create a decorator that will house the various docstring snippets reused # throughout Matplotlib. -dedent_interpd = interpd = _ArtistPropertiesSubstitution() +interpd = _ArtistPropertiesSubstitution() diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index f187d7a0c4f3..33fc42a4b860 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -220,7 +220,7 @@ def get_legend_handles_labels(self, legend_handler_map=None): [self], legend_handler_map) return handles, labels - @_docstring.dedent_interpd + @_docstring.interpd def legend(self, *args, **kwargs): """ Place a legend on the Axes. @@ -418,7 +418,7 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): return inset_ax - @_docstring.dedent_interpd + @_docstring.interpd def indicate_inset(self, bounds, inset_ax=None, *, transform=None, facecolor='none', edgecolor='0.5', alpha=0.5, zorder=4.99, **kwargs): @@ -572,7 +572,7 @@ def indicate_inset_zoom(self, inset_ax, **kwargs): rect = (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]) return self.indicate_inset(rect, inset_ax, **kwargs) - @_docstring.dedent_interpd + @_docstring.interpd def secondary_xaxis(self, location, functions=None, *, transform=None, **kwargs): """ Add a second x-axis to this `~.axes.Axes`. @@ -626,7 +626,7 @@ def invert(x): self.add_child_axes(secondary_ax) return secondary_ax - @_docstring.dedent_interpd + @_docstring.interpd def secondary_yaxis(self, location, functions=None, *, transform=None, **kwargs): """ Add a second y-axis to this `~.axes.Axes`. @@ -670,7 +670,7 @@ def secondary_yaxis(self, location, functions=None, *, transform=None, **kwargs) self.add_child_axes(secondary_ax) return secondary_ax - @_docstring.dedent_interpd + @_docstring.interpd def text(self, x, y, s, fontdict=None, **kwargs): """ Add text to the Axes. @@ -749,7 +749,7 @@ def text(self, x, y, s, fontdict=None, **kwargs): self._add_text(t) return t - @_docstring.dedent_interpd + @_docstring.interpd def annotate(self, text, xy, xytext=None, xycoords='data', textcoords=None, arrowprops=None, annotation_clip=None, **kwargs): # Signature must match Annotation. This is verified in @@ -765,7 +765,7 @@ def annotate(self, text, xy, xytext=None, xycoords='data', textcoords=None, annotate.__doc__ = mtext.Annotation.__init__.__doc__ #### Lines and spans - @_docstring.dedent_interpd + @_docstring.interpd def axhline(self, y=0, xmin=0, xmax=1, **kwargs): """ Add a horizontal line spanning the whole or fraction of the Axes. @@ -839,7 +839,7 @@ def axhline(self, y=0, xmin=0, xmax=1, **kwargs): self._request_autoscale_view("y") return l - @_docstring.dedent_interpd + @_docstring.interpd def axvline(self, x=0, ymin=0, ymax=1, **kwargs): """ Add a vertical line spanning the whole or fraction of the Axes. @@ -921,7 +921,7 @@ def _check_no_units(vals, names): raise ValueError(f"{name} must be a single scalar value, " f"but got {val}") - @_docstring.dedent_interpd + @_docstring.interpd def axline(self, xy1, xy2=None, *, slope=None, **kwargs): """ Add an infinitely long straight line. @@ -995,7 +995,7 @@ def axline(self, xy1, xy2=None, *, slope=None, **kwargs): self._request_autoscale_view() return line - @_docstring.dedent_interpd + @_docstring.interpd def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs): """ Add a horizontal span (rectangle) across the Axes. @@ -1050,7 +1050,7 @@ def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs): self._request_autoscale_view("y") return p - @_docstring.dedent_interpd + @_docstring.interpd def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs): """ Add a vertical span (rectangle) across the Axes. @@ -1301,7 +1301,7 @@ def vlines(self, x, ymin, ymax, colors=None, linestyles='solid', @_preprocess_data(replace_names=["positions", "lineoffsets", "linelengths", "linewidths", "colors", "linestyles"]) - @_docstring.dedent_interpd + @_docstring.interpd def eventplot(self, positions, orientation='horizontal', lineoffsets=1, linelengths=1, linewidths=None, colors=None, alpha=None, linestyles='solid', **kwargs): @@ -1547,7 +1547,7 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, # Uses a custom implementation of data-kwarg handling in # _process_plot_var_args. - @_docstring.dedent_interpd + @_docstring.interpd def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs): """ Plot y versus x as lines and/or markers. @@ -1803,7 +1803,7 @@ def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs): @_api.deprecated("3.9", alternative="plot") @_preprocess_data(replace_names=["x", "y"], label_namer="y") - @_docstring.dedent_interpd + @_docstring.interpd def plot_date(self, x, y, fmt='o', tz=None, xdate=True, ydate=False, **kwargs): """ @@ -1883,7 +1883,7 @@ def plot_date(self, x, y, fmt='o', tz=None, xdate=True, ydate=False, return self.plot(x, y, fmt, **kwargs) # @_preprocess_data() # let 'plot' do the unpacking.. - @_docstring.dedent_interpd + @_docstring.interpd def loglog(self, *args, **kwargs): """ Make a plot with log scaling on both the x- and y-axis. @@ -1937,7 +1937,7 @@ def loglog(self, *args, **kwargs): *args, **{k: v for k, v in kwargs.items() if k not in {*dx, *dy}}) # @_preprocess_data() # let 'plot' do the unpacking.. - @_docstring.dedent_interpd + @_docstring.interpd def semilogx(self, *args, **kwargs): """ Make a plot with log scaling on the x-axis. @@ -1984,7 +1984,7 @@ def semilogx(self, *args, **kwargs): *args, **{k: v for k, v in kwargs.items() if k not in d}) # @_preprocess_data() # let 'plot' do the unpacking.. - @_docstring.dedent_interpd + @_docstring.interpd def semilogy(self, *args, **kwargs): """ Make a plot with log scaling on the y-axis. @@ -2340,7 +2340,7 @@ def _convert_dx(dx, x0, xconv, convert): return dx @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def bar(self, x, height, width=0.8, bottom=None, *, align="center", **kwargs): r""" @@ -2652,7 +2652,7 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", return bar_container # @_preprocess_data() # let 'bar' do the unpacking.. - @_docstring.dedent_interpd + @_docstring.interpd def barh(self, y, width, height=0.8, left=None, *, align="center", data=None, **kwargs): r""" @@ -2946,7 +2946,7 @@ def sign(x): return annotations @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def broken_barh(self, xranges, yrange, **kwargs): """ Plot a horizontal sequence of rectangles. @@ -3455,7 +3455,7 @@ def _errorevery_to_mask(x, errorevery): @_api.make_keyword_only("3.9", "ecolor") @_preprocess_data(replace_names=["x", "y", "xerr", "yerr"], label_namer="y") - @_docstring.dedent_interpd + @_docstring.interpd def errorbar(self, x, y, yerr=None, xerr=None, fmt='', ecolor=None, elinewidth=None, capsize=None, barsabove=False, lolims=False, uplims=False, @@ -4985,7 +4985,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, @_api.make_keyword_only("3.9", "gridsize") @_preprocess_data(replace_names=["x", "y", "C"], label_namer="y") - @_docstring.dedent_interpd + @_docstring.interpd def hexbin(self, x, y, C=None, gridsize=100, bins=None, xscale='linear', yscale='linear', extent=None, cmap=None, norm=None, vmin=None, vmax=None, @@ -5380,7 +5380,7 @@ def on_changed(collection): return collection - @_docstring.dedent_interpd + @_docstring.interpd def arrow(self, x, y, dx, dy, **kwargs): """ Add an arrow to the Axes. @@ -5435,7 +5435,7 @@ def _quiver_units(self, args, kwargs): # args can be a combination of X, Y, U, V, C and all should be replaced @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def quiver(self, *args, **kwargs): """%(quiver_doc)s""" # Make sure units are handled for x and y values @@ -5447,7 +5447,7 @@ def quiver(self, *args, **kwargs): # args can be some combination of X, Y, U, V, C and all should be replaced @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def barbs(self, *args, **kwargs): """%(barbs_doc)s""" # Make sure units are handled for x and y values @@ -5718,7 +5718,7 @@ def fill_between(self, x, y1, y2=0, where=None, interpolate=False, dir="horizontal", ind="x", dep="y" ) fill_between = _preprocess_data( - _docstring.dedent_interpd(fill_between), + _docstring.interpd(fill_between), replace_names=["x", "y1", "y2", "where"]) def fill_betweenx(self, y, x1, x2=0, where=None, @@ -5732,7 +5732,7 @@ def fill_betweenx(self, y, x1, x2=0, where=None, dir="vertical", ind="y", dep="x" ) fill_betweenx = _preprocess_data( - _docstring.dedent_interpd(fill_betweenx), + _docstring.interpd(fill_betweenx), replace_names=["y", "x1", "x2", "where"]) #### plotting z(x, y): imshow, pcolor and relatives, contour @@ -6093,7 +6093,7 @@ def _interp_grid(X): return X, Y, C, shading @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, vmin=None, vmax=None, **kwargs): r""" @@ -6310,7 +6310,7 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, return collection @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, vmax=None, shading=None, antialiased=False, **kwargs): """ @@ -6537,7 +6537,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, return collection @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, vmax=None, **kwargs): """ @@ -6724,7 +6724,7 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, return ret @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def contour(self, *args, **kwargs): """ Plot contour lines. @@ -6742,7 +6742,7 @@ def contour(self, *args, **kwargs): return contours @_preprocess_data() - @_docstring.dedent_interpd + @_docstring.interpd def contourf(self, *args, **kwargs): """ Plot filled contours. @@ -7374,7 +7374,7 @@ def stairs(self, values, edges=None, *, @_api.make_keyword_only("3.9", "range") @_preprocess_data(replace_names=["x", "y", "weights"]) - @_docstring.dedent_interpd + @_docstring.interpd def hist2d(self, x, y, bins=10, range=None, density=False, weights=None, cmin=None, cmax=None, **kwargs): """ @@ -7481,7 +7481,7 @@ def hist2d(self, x, y, bins=10, range=None, density=False, weights=None, return h, xedges, yedges, pc @_preprocess_data(replace_names=["x", "weights"], label_namer="x") - @_docstring.dedent_interpd + @_docstring.interpd def ecdf(self, x, weights=None, *, complementary=False, orientation="vertical", compress=False, **kwargs): """ @@ -7584,7 +7584,7 @@ def ecdf(self, x, weights=None, *, complementary=False, @_api.make_keyword_only("3.9", "NFFT") @_preprocess_data(replace_names=["x"]) - @_docstring.dedent_interpd + @_docstring.interpd def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None, return_line=None, **kwargs): @@ -7696,7 +7696,7 @@ def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, @_api.make_keyword_only("3.9", "NFFT") @_preprocess_data(replace_names=["x", "y"], label_namer="y") - @_docstring.dedent_interpd + @_docstring.interpd def csd(self, x, y, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None, return_line=None, **kwargs): @@ -7799,7 +7799,7 @@ def csd(self, x, y, NFFT=None, Fs=None, Fc=None, detrend=None, @_api.make_keyword_only("3.9", "Fs") @_preprocess_data(replace_names=["x"]) - @_docstring.dedent_interpd + @_docstring.interpd def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, scale=None, **kwargs): @@ -7886,7 +7886,7 @@ def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None, @_api.make_keyword_only("3.9", "Fs") @_preprocess_data(replace_names=["x"]) - @_docstring.dedent_interpd + @_docstring.interpd def angle_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, **kwargs): """ @@ -7956,7 +7956,7 @@ def angle_spectrum(self, x, Fs=None, Fc=None, window=None, @_api.make_keyword_only("3.9", "Fs") @_preprocess_data(replace_names=["x"]) - @_docstring.dedent_interpd + @_docstring.interpd def phase_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, **kwargs): """ @@ -8026,7 +8026,7 @@ def phase_spectrum(self, x, Fs=None, Fc=None, window=None, @_api.make_keyword_only("3.9", "NFFT") @_preprocess_data(replace_names=["x", "y"]) - @_docstring.dedent_interpd + @_docstring.interpd def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, window=mlab.window_hanning, noverlap=0, pad_to=None, sides='default', scale_by_freq=None, **kwargs): @@ -8091,7 +8091,7 @@ def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, @_api.make_keyword_only("3.9", "NFFT") @_preprocess_data(replace_names=["x"]) - @_docstring.dedent_interpd + @_docstring.interpd def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, cmap=None, xextent=None, pad_to=None, sides=None, @@ -8252,7 +8252,7 @@ def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, return spec, freqs, t, im @_api.make_keyword_only("3.9", "precision") - @_docstring.dedent_interpd + @_docstring.interpd def spy(self, Z, precision=0, marker=None, markersize=None, aspect='equal', origin="upper", **kwargs): """ diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 6aa5ef1efb7b..20cea0a917e1 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -3242,7 +3242,7 @@ def set_axisbelow(self, b): axis.set_zorder(zorder) self.stale = True - @_docstring.dedent_interpd + @_docstring.interpd def grid(self, visible=None, which='major', axis='both', **kwargs): """ Configure the grid lines. diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 3ff8cf077d1a..ff115b10e6d8 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -555,7 +555,7 @@ def _find_closest_point_on_path(xys, p): """) -@_docstring.dedent_interpd +@_docstring.interpd class ContourSet(ContourLabeler, mcoll.Collection): """ Store a set of contour lines or filled regions. @@ -1269,7 +1269,7 @@ def draw(self, renderer): super().draw(renderer) -@_docstring.dedent_interpd +@_docstring.interpd class QuadContourSet(ContourSet): """ Create and store a set of contour lines or filled regions. diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 00bfe1459d98..0d5a686de9d8 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -527,7 +527,7 @@ def add_artist(self, artist, clip=False): self.stale = True return artist - @_docstring.dedent_interpd + @_docstring.interpd def add_axes(self, *args, **kwargs): """ Add an `~.axes.Axes` to the figure. @@ -645,7 +645,7 @@ def add_axes(self, *args, **kwargs): addendum="Any additional positional arguments are currently ignored.") return self._add_axes_internal(a, key) - @_docstring.dedent_interpd + @_docstring.interpd def add_subplot(self, *args, **kwargs): """ Add an `~.axes.Axes` to the figure as part of a subplot arrangement. @@ -1024,7 +1024,7 @@ def clf(self, keep_observers=False): # " legend(" -> " figlegend(" for the signatures # "fig.legend(" -> "plt.figlegend" for the code examples # "ax.plot" -> "plt.plot" for consistency in using pyplot when able - @_docstring.dedent_interpd + @_docstring.interpd def legend(self, *args, **kwargs): """ Place a legend on the figure. @@ -1144,7 +1144,7 @@ def legend(self, *args, **kwargs): self.stale = True return l - @_docstring.dedent_interpd + @_docstring.interpd def text(self, x, y, s, fontdict=None, **kwargs): """ Add text to figure. @@ -1194,7 +1194,7 @@ def text(self, x, y, s, fontdict=None, **kwargs): self.stale = True return text - @_docstring.dedent_interpd + @_docstring.interpd def colorbar( self, mappable, cax=None, ax=None, use_gridspec=True, **kwargs): """ diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index b32fe6ea470e..0d487a48bde7 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -351,7 +351,7 @@ class Legend(Artist): def __str__(self): return "Legend" - @_docstring.dedent_interpd + @_docstring.interpd def __init__( self, parent, handles, labels, *, @@ -643,7 +643,7 @@ def _set_artist_props(self, a): a.set_transform(self.get_transform()) - @_docstring.dedent_interpd + @_docstring.interpd def set_loc(self, loc=None): """ Set the location of the legend. diff --git a/lib/matplotlib/mlab.py b/lib/matplotlib/mlab.py index e1f08c0da5ce..fad8d648f6db 100644 --- a/lib/matplotlib/mlab.py +++ b/lib/matplotlib/mlab.py @@ -458,7 +458,7 @@ def _single_spectrum_helper( MATLAB compatibility.""") -@_docstring.dedent_interpd +@_docstring.interpd def psd(x, NFFT=None, Fs=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None): r""" @@ -514,7 +514,7 @@ def psd(x, NFFT=None, Fs=None, detrend=None, window=None, return Pxx.real, freqs -@_docstring.dedent_interpd +@_docstring.interpd def csd(x, y, NFFT=None, Fs=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None): """ @@ -634,7 +634,7 @@ def csd(x, y, NFFT=None, Fs=None, detrend=None, window=None, **_docstring.interpd.params) -@_docstring.dedent_interpd +@_docstring.interpd def specgram(x, NFFT=None, Fs=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None, mode=None): @@ -717,7 +717,7 @@ def specgram(x, NFFT=None, Fs=None, detrend=None, window=None, return spec, freqs, t -@_docstring.dedent_interpd +@_docstring.interpd def cohere(x, y, NFFT=256, Fs=2, detrend=detrend_none, window=window_hanning, noverlap=0, pad_to=None, sides='default', scale_by_freq=None): r""" diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 11244386b5c6..49f0946f1ee9 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -1191,7 +1191,7 @@ class AnnotationBbox(martist.Artist, mtext._AnnotationBase): def __str__(self): return f"AnnotationBbox({self.xy[0]:g},{self.xy[1]:g})" - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, offsetbox, xy, xybox=None, xycoords='data', boxcoords=None, *, frameon=True, pad=0.4, # FancyBboxPatch boxstyle. annotation_clip=None, diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 0d5867e08ae1..1c19d8424db0 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -655,7 +655,7 @@ class Shadow(Patch): def __str__(self): return f"Shadow({self.patch})" - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, patch, ox, oy, *, shade=0.7, **kwargs): """ Create a shadow of the given *patch*. @@ -735,7 +735,7 @@ def __str__(self): fmt = "Rectangle(xy=(%g, %g), width=%g, height=%g, angle=%g)" return fmt % pars - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, width, height, *, angle=0.0, rotation_point='xy', **kwargs): """ @@ -936,7 +936,7 @@ def __str__(self): return s % (self.xy[0], self.xy[1], self.numvertices, self.radius, self.orientation) - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, numVertices, *, radius=5, orientation=0, **kwargs): """ @@ -986,7 +986,7 @@ def __str__(self): s = "PathPatch%d((%g, %g) ...)" return s % (len(self._path.vertices), *tuple(self._path.vertices[0])) - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, path, **kwargs): """ *path* is a `.Path` object. @@ -1015,7 +1015,7 @@ class StepPatch(PathPatch): _edge_default = False - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, values, edges, *, orientation='vertical', baseline=0, **kwargs): """ @@ -1124,7 +1124,7 @@ def __str__(self): else: return "Polygon0()" - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, *, closed=True, **kwargs): """ Parameters @@ -1222,7 +1222,7 @@ def __str__(self): fmt = "Wedge(center=(%g, %g), r=%g, theta1=%g, theta2=%g, width=%s)" return fmt % pars - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, center, r, theta1, theta2, *, width=None, **kwargs): """ A wedge centered at *x*, *y* center with radius *r* that @@ -1310,7 +1310,7 @@ def __str__(self): [0.0, 0.1], [0.0, -0.1], [0.8, -0.1], [0.8, -0.3], [1.0, 0.0], [0.8, 0.3], [0.8, 0.1]]) - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, x, y, dx, dy, *, width=1.0, **kwargs): """ Draws an arrow from (*x*, *y*) to (*x* + *dx*, *y* + *dy*). @@ -1393,7 +1393,7 @@ class FancyArrow(Polygon): def __str__(self): return "FancyArrow()" - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, x, y, dx, dy, *, width=0.001, length_includes_head=False, head_width=None, head_length=None, shape='full', overhang=0, @@ -1564,7 +1564,7 @@ def __str__(self): s = "CirclePolygon((%g, %g), radius=%g, resolution=%d)" return s % (self.xy[0], self.xy[1], self.radius, self.numvertices) - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, radius=5, *, resolution=20, # the number of vertices ** kwargs): @@ -1591,7 +1591,7 @@ def __str__(self): fmt = "Ellipse(xy=(%s, %s), width=%s, height=%s, angle=%s)" return fmt % pars - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, width, height, *, angle=0, **kwargs): """ Parameters @@ -1767,7 +1767,7 @@ class Annulus(Patch): An elliptical annulus. """ - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, r, width, angle=0.0, **kwargs): """ Parameters @@ -1958,7 +1958,7 @@ def __str__(self): fmt = "Circle(xy=(%g, %g), radius=%g)" return fmt % pars - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, radius=5, **kwargs): """ Create a true circle at center *xy* = (*x*, *y*) with given *radius*. @@ -2005,7 +2005,7 @@ def __str__(self): "height=%g, angle=%g, theta1=%g, theta2=%g)") return fmt % pars - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, width, height, *, angle=0.0, theta1=0.0, theta2=360.0, **kwargs): """ @@ -2367,7 +2367,7 @@ def _register_style(style_list, cls=None, *, name=None): return cls -@_docstring.dedent_interpd +@_docstring.interpd class BoxStyle(_Style): """ `BoxStyle` is a container class which defines several @@ -2732,7 +2732,7 @@ def __call__(self, x0, y0, width, height, mutation_size): return Path(saw_vertices, codes) -@_docstring.dedent_interpd +@_docstring.interpd class ConnectionStyle(_Style): """ `ConnectionStyle` is a container class which defines @@ -3154,7 +3154,7 @@ def _point_along_a_line(x0, y0, x1, y1, d): return x2, y2 -@_docstring.dedent_interpd +@_docstring.interpd class ArrowStyle(_Style): """ `ArrowStyle` is a container class which defines several @@ -3891,7 +3891,7 @@ def __str__(self): s = self.__class__.__name__ + "((%g, %g), width=%g, height=%g)" return s % (self._x, self._y, self._width, self._height) - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xy, width, height, boxstyle="round", *, mutation_scale=1, mutation_aspect=1, **kwargs): """ @@ -3943,7 +3943,7 @@ def __init__(self, xy, width, height, boxstyle="round", *, self._mutation_aspect = mutation_aspect self.stale = True - @_docstring.dedent_interpd + @_docstring.interpd def set_boxstyle(self, boxstyle=None, **kwargs): """ Set the box style, possibly with further attributes. @@ -4143,7 +4143,7 @@ def __str__(self): else: return f"{type(self).__name__}({self._path_original})" - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, posA=None, posB=None, *, path=None, arrowstyle="simple", connectionstyle="arc3", patchA=None, patchB=None, shrinkA=2, shrinkB=2, @@ -4282,7 +4282,7 @@ def set_patchB(self, patchB): self.patchB = patchB self.stale = True - @_docstring.dedent_interpd + @_docstring.interpd def set_connectionstyle(self, connectionstyle=None, **kwargs): """ Set the connection style, possibly with further attributes. @@ -4326,7 +4326,7 @@ def get_connectionstyle(self): """Return the `ConnectionStyle` used.""" return self._connector - @_docstring.dedent_interpd + @_docstring.interpd def set_arrowstyle(self, arrowstyle=None, **kwargs): """ Set the arrow style, possibly with further attributes. @@ -4470,7 +4470,7 @@ def __str__(self): return "ConnectionPatch((%g, %g), (%g, %g))" % \ (self.xy1[0], self.xy1[1], self.xy2[0], self.xy2[1]) - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, xyA, xyB, coordsA, coordsB=None, *, axesA=None, axesB=None, arrowstyle="-", diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index b2780f7cf95f..b871bc58a4b4 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -1257,7 +1257,7 @@ def figlegend(*args, **kwargs) -> Legend: ## Axes ## -@_docstring.dedent_interpd +@_docstring.interpd def axes( arg: None | tuple[float, float, float, float] = None, **kwargs @@ -1376,7 +1376,7 @@ def cla() -> None: ## More ways of creating Axes ## -@_docstring.dedent_interpd +@_docstring.interpd def subplot(*args, **kwargs) -> Axes: """ Add an Axes to the current figure or retrieve an existing Axes. diff --git a/lib/matplotlib/sankey.py b/lib/matplotlib/sankey.py index 665b9d6deba2..637cfc849f9d 100644 --- a/lib/matplotlib/sankey.py +++ b/lib/matplotlib/sankey.py @@ -347,7 +347,7 @@ def _revert(self, path, first_action=Path.LINETO): # path[2] = path[2][::-1] # return path - @_docstring.dedent_interpd + @_docstring.interpd def add(self, patchlabel='', flows=None, orientations=None, labels='', trunklength=1.0, pathlengths=0.25, prior=None, connect=(0, 0), rotation=0, **kwargs): diff --git a/lib/matplotlib/spines.py b/lib/matplotlib/spines.py index 1cec93b31db3..7e77a393f2a2 100644 --- a/lib/matplotlib/spines.py +++ b/lib/matplotlib/spines.py @@ -32,7 +32,7 @@ class Spine(mpatches.Patch): def __str__(self): return "Spine" - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, axes, spine_type, path, **kwargs): """ Parameters diff --git a/lib/matplotlib/table.py b/lib/matplotlib/table.py index 51d022907b62..0f75021926fd 100644 --- a/lib/matplotlib/table.py +++ b/lib/matplotlib/table.py @@ -175,7 +175,7 @@ def get_required_width(self, renderer): l, b, w, h = self.get_text_bounds(renderer) return w * (1.0 + (2.0 * self.PAD)) - @_docstring.dedent_interpd + @_docstring.interpd def set_text_props(self, **kwargs): """ Update the text properties. @@ -649,7 +649,7 @@ def get_celld(self): return self._cells -@_docstring.dedent_interpd +@_docstring.interpd def table(ax, cellText=None, cellColours=None, cellLoc='right', colWidths=None, diff --git a/lib/matplotlib/tri/_tricontour.py b/lib/matplotlib/tri/_tricontour.py index 1db3715d01af..c09d04f9e543 100644 --- a/lib/matplotlib/tri/_tricontour.py +++ b/lib/matplotlib/tri/_tricontour.py @@ -5,7 +5,7 @@ from matplotlib.tri._triangulation import Triangulation -@_docstring.dedent_interpd +@_docstring.interpd class TriContourSet(ContourSet): """ Create and store a set of contour lines or filled regions for @@ -218,7 +218,7 @@ def _contour_args(self, args, kwargs): @_docstring.Substitution(func='tricontour', type='lines') -@_docstring.dedent_interpd +@_docstring.interpd def tricontour(ax, *args, **kwargs): """ %(_tricontour_doc)s @@ -247,7 +247,7 @@ def tricontour(ax, *args, **kwargs): @_docstring.Substitution(func='tricontourf', type='regions') -@_docstring.dedent_interpd +@_docstring.interpd def tricontourf(ax, *args, **kwargs): """ %(_tricontour_doc)s diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index c4fbd660fe4c..303dbbb0721e 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -15,7 +15,7 @@ @_api.deprecated("3.8", alternative="Axes.inset_axes") class InsetPosition: - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, parent, lbwh): """ An object for positioning an inset axes. @@ -131,7 +131,7 @@ def get_bbox(self, renderer): class BboxPatch(Patch): - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, bbox, **kwargs): """ Patch showing the shape bounded by a Bbox. @@ -193,7 +193,7 @@ def connect_bbox(bbox1, bbox2, loc1, loc2=None): x2, y2 = BboxConnector.get_bbox_edge_pos(bbox2, loc2) return Path([[x1, y1], [x2, y2]]) - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, bbox1, bbox2, loc1, loc2=None, **kwargs): """ Connect two bboxes with a straight line. @@ -237,7 +237,7 @@ def get_path(self): class BboxConnectorPatch(BboxConnector): - @_docstring.dedent_interpd + @_docstring.interpd def __init__(self, bbox1, bbox2, loc1a, loc2a, loc1b, loc2b, **kwargs): """ Connect two bboxes with a quadrilateral. @@ -295,7 +295,7 @@ def _add_inset_axes(parent_axes, axes_class, axes_kwargs, axes_locator): return fig.add_axes(inset_axes) -@_docstring.dedent_interpd +@_docstring.interpd def inset_axes(parent_axes, width, height, loc='upper right', bbox_to_anchor=None, bbox_transform=None, axes_class=None, axes_kwargs=None, @@ -419,7 +419,7 @@ def inset_axes(parent_axes, width, height, loc='upper right', bbox_transform=bbox_transform, borderpad=borderpad)) -@_docstring.dedent_interpd +@_docstring.interpd def zoomed_inset_axes(parent_axes, zoom, loc='upper right', bbox_to_anchor=None, bbox_transform=None, axes_class=None, axes_kwargs=None, @@ -512,7 +512,7 @@ def get_points(self): return super().get_points() -@_docstring.dedent_interpd +@_docstring.interpd def mark_inset(parent_axes, inset_axes, loc1, loc2, **kwargs): """ Draw a box to mark the location of an area represented by an inset axes. From 791d6c02c6a43b91ea2953910e6d20b9adffa612 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 16 Sep 2024 14:39:29 -0400 Subject: [PATCH 0261/1230] Backport PR #28818: Resolve configdir so that it's not a symlink when is_dir() is called --- lib/matplotlib/__init__.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 9e9325a27d73..ad4676b11ae0 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -518,35 +518,38 @@ def _get_xdg_cache_dir(): def _get_config_or_cache_dir(xdg_base_getter): configdir = os.environ.get('MPLCONFIGDIR') if configdir: - configdir = Path(configdir).resolve() + configdir = Path(configdir) elif sys.platform.startswith(('linux', 'freebsd')): # Only call _xdg_base_getter here so that MPLCONFIGDIR is tried first, # as _xdg_base_getter can throw. configdir = Path(xdg_base_getter(), "matplotlib") else: configdir = Path.home() / ".matplotlib" + # Resolve the path to handle potential issues with inaccessible symlinks. + configdir = configdir.resolve() try: configdir.mkdir(parents=True, exist_ok=True) - except OSError: - pass + except OSError as exc: + _log.warning("mkdir -p failed for path %s: %s", configdir, exc) else: if os.access(str(configdir), os.W_OK) and configdir.is_dir(): return str(configdir) + _log.warning("%s is not a writable directory", configdir) # If the config or cache directory cannot be created or is not a writable # directory, create a temporary one. try: tmpdir = tempfile.mkdtemp(prefix="matplotlib-") except OSError as exc: raise OSError( - f"Matplotlib requires access to a writable cache directory, but the " - f"default path ({configdir}) is not a writable directory, and a temporary " + f"Matplotlib requires access to a writable cache directory, but there " + f"was an issue with the default path ({configdir}), and a temporary " f"directory could not be created; set the MPLCONFIGDIR environment " f"variable to a writable directory") from exc os.environ["MPLCONFIGDIR"] = tmpdir atexit.register(shutil.rmtree, tmpdir) _log.warning( - "Matplotlib created a temporary cache directory at %s because the default path " - "(%s) is not a writable directory; it is highly recommended to set the " + "Matplotlib created a temporary cache directory at %s because there was " + "an issue with the default path (%s); it is highly recommended to set the " "MPLCONFIGDIR environment variable to a writable directory, in particular to " "speed up the import of Matplotlib and to better support multiprocessing.", tmpdir, configdir) From 66dfeaa6d623c1a712ec0e5af5e16c43b4b296ff Mon Sep 17 00:00:00 2001 From: Ayoub Gouasmi <96791563+gougouasmi@users.noreply.github.com> Date: Thu, 9 Nov 2023 15:23:47 -0800 Subject: [PATCH 0262/1230] Added tags for simple_scatter.py demo --- galleries/examples/animation/simple_scatter.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/galleries/examples/animation/simple_scatter.py b/galleries/examples/animation/simple_scatter.py index a006f73bab4c..3f8c285810a3 100644 --- a/galleries/examples/animation/simple_scatter.py +++ b/galleries/examples/animation/simple_scatter.py @@ -19,10 +19,10 @@ def animate(i): scat.set_offsets((x[i], 0)) - return scat, + return (scat,) -ani = animation.FuncAnimation(fig, animate, repeat=True, - frames=len(x) - 1, interval=50) + +ani = animation.FuncAnimation(fig, animate, repeat=True, frames=len(x) - 1, interval=50) # To save the animation using Pillow as a gif # writer = animation.PillowWriter(fps=15, @@ -31,3 +31,11 @@ def animate(i): # ani.save('scatter.gif', writer=writer) plt.show() + +# %% +# +# .. tags:: +# component: animation, +# plot-type: scatter, +# purpose: reference, +# level: intermediate From 831ac5705fdf67a3f4663b21be86d0c8d3961d66 Mon Sep 17 00:00:00 2001 From: Xeniya Shoiko <53381916+kakun45@users.noreply.github.com> Date: Mon, 25 Dec 2023 19:35:10 -0500 Subject: [PATCH 0263/1230] tags for multivariate_marker_plot, geo_demo, and subplots_demo --- .../lines_bars_and_markers/multivariate_marker_plot.py | 3 ++- galleries/examples/subplots_axes_and_figures/geo_demo.py | 1 + .../examples/subplots_axes_and_figures/subplots_demo.py | 6 ++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/galleries/examples/lines_bars_and_markers/multivariate_marker_plot.py b/galleries/examples/lines_bars_and_markers/multivariate_marker_plot.py index 1f149c030abe..d05085a0d9dc 100644 --- a/galleries/examples/lines_bars_and_markers/multivariate_marker_plot.py +++ b/galleries/examples/lines_bars_and_markers/multivariate_marker_plot.py @@ -50,6 +50,7 @@ # .. tags:: # # component: marker -# styling: color +# styling: color, +# styling: shape # level: beginner # purpose: fun diff --git a/galleries/examples/subplots_axes_and_figures/geo_demo.py b/galleries/examples/subplots_axes_and_figures/geo_demo.py index 02680c1fd692..256c440cc4d1 100644 --- a/galleries/examples/subplots_axes_and_figures/geo_demo.py +++ b/galleries/examples/subplots_axes_and_figures/geo_demo.py @@ -45,4 +45,5 @@ # .. tags:: # # plot-type: specialty +# component: projection # domain: cartography diff --git a/galleries/examples/subplots_axes_and_figures/subplots_demo.py b/galleries/examples/subplots_axes_and_figures/subplots_demo.py index acd031b8201d..0e3cb1102230 100644 --- a/galleries/examples/subplots_axes_and_figures/subplots_demo.py +++ b/galleries/examples/subplots_axes_and_figures/subplots_demo.py @@ -213,8 +213,10 @@ # %% # .. tags:: # -# component: subplot -# plot-type: line +# component: subplot, +# component: axes, +# component: axis +# plot-type: line, # plot-type: polar # level: beginner # purpose: showcase From 5b40be444d14895b6c7ae8868e10ed7ea18a6cb0 Mon Sep 17 00:00:00 2001 From: smcgrawDotNet Date: Mon, 25 Dec 2023 19:51:40 -0500 Subject: [PATCH 0264/1230] tags for simple polygon selector widget --- galleries/examples/widgets/polygon_selector_simple.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/galleries/examples/widgets/polygon_selector_simple.py b/galleries/examples/widgets/polygon_selector_simple.py index 8dab957cdca0..e344da7e0645 100644 --- a/galleries/examples/widgets/polygon_selector_simple.py +++ b/galleries/examples/widgets/polygon_selector_simple.py @@ -38,6 +38,15 @@ # %% +# .. tags:: +# +# component: axes, +# styling: position, +# plot-type: line, +# level: intermediate, +# domain: cartography, +# domain: geometry, +# domain: statistics, # # .. admonition:: References # From 30eb07e6934af397092463141cab39a9ecedf979 Mon Sep 17 00:00:00 2001 From: RickyP24 Date: Mon, 25 Dec 2023 20:51:05 -0500 Subject: [PATCH 0265/1230] tags for bayes, dynamic image, random walk, and strip chart --- galleries/examples/animation/bayes_update.py | 3 +++ galleries/examples/animation/dynamic_image.py | 3 +++ galleries/examples/animation/random_walk.py | 3 +++ galleries/examples/animation/strip_chart.py | 2 ++ 4 files changed, 11 insertions(+) diff --git a/galleries/examples/animation/bayes_update.py b/galleries/examples/animation/bayes_update.py index 1081b4704623..6d36bd1e6149 100644 --- a/galleries/examples/animation/bayes_update.py +++ b/galleries/examples/animation/bayes_update.py @@ -69,3 +69,6 @@ def __call__(self, i): ud = UpdateDist(ax, prob=0.7) anim = FuncAnimation(fig, ud, init_func=ud.start, frames=100, interval=100, blit=True) plt.show() + +# %% +# .. tags:: animation, plot-type: line diff --git a/galleries/examples/animation/dynamic_image.py b/galleries/examples/animation/dynamic_image.py index 541edede31e4..221f6f08d0c8 100644 --- a/galleries/examples/animation/dynamic_image.py +++ b/galleries/examples/animation/dynamic_image.py @@ -46,3 +46,6 @@ def f(x, y): # ani.save("movie.mp4", writer=writer) plt.show() + +# %% +# .. tags:: animation diff --git a/galleries/examples/animation/random_walk.py b/galleries/examples/animation/random_walk.py index 4be0b461f933..9dd4383fd548 100644 --- a/galleries/examples/animation/random_walk.py +++ b/galleries/examples/animation/random_walk.py @@ -50,3 +50,6 @@ def update_lines(num, walks, lines): fig, update_lines, num_steps, fargs=(walks, lines), interval=100) plt.show() + +# %% +# .. tags:: animation, plot-type: 3D diff --git a/galleries/examples/animation/strip_chart.py b/galleries/examples/animation/strip_chart.py index 919624c59652..0e533a255f1c 100644 --- a/galleries/examples/animation/strip_chart.py +++ b/galleries/examples/animation/strip_chart.py @@ -67,3 +67,5 @@ def emitter(p=0.1): blit=True, save_count=100) plt.show() + +# ..tags:: animation, plot-type: line From 06d5c27dfa1838474f8ff5a701fa89834a3008ba Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:17:39 +0200 Subject: [PATCH 0266/1230] DOC: Document policy on colormaps and styles Also closes #27673 by removing that page. The relevant content is transferred to the new section. Co-authored-by: hannah --- doc/devel/api_changes.rst | 26 +++++-- doc/devel/color_changes.rst | 136 ------------------------------------ 2 files changed, 19 insertions(+), 143 deletions(-) delete mode 100644 doc/devel/color_changes.rst diff --git a/doc/devel/api_changes.rst b/doc/devel/api_changes.rst index 0e86f11a3694..6e134d6b9509 100644 --- a/doc/devel/api_changes.rst +++ b/doc/devel/api_changes.rst @@ -9,13 +9,8 @@ if the added benefit is worth the effort of adapting existing code. Because we are a visualization library, our primary output is the final visualization the user sees; therefore, the appearance of the figure is part of -the API and any changes, either semantic or :ref:`aesthetic `, -are backwards-incompatible API changes. - -.. toctree:: - :hidden: - - color_changes.rst +the API and any changes, either semantic or aesthetic, are backwards-incompatible +API changes. Add new API and features @@ -37,6 +32,23 @@ take particular care when adding new API: __ https://emptysqua.re/blog/api-evolution-the-right-way/#adding-parameters +Add or change colormaps, color sequences, and styles +---------------------------------------------------- +Visual changes are considered an API break. Therefore, we generally do not modify +existing colormaps, color sequences, or styles. + +We put a high bar on adding new colormaps and styles to prevent excessively growing +them. While the decision is case-by-case, evaluation criteria include: + +- novelty: Does it support a new use case? e.g. slight variations of existing maps, + sequences and styles are likely not accepted. +- usability and accessibility: Are colors of sequences sufficiently distinct? Has + colorblindness been considered? +- evidence of wide spread usage: for example academic papers, industry blogs and + whitepapers, or inclusion in other visualization libraries or domain specific tools +- open license: colormaps, sequences, and styles must have a BSD compatible license + (see :ref:`license-discussion`) + .. _deprecation-guidelines: Deprecate API diff --git a/doc/devel/color_changes.rst b/doc/devel/color_changes.rst deleted file mode 100644 index f7646ded7c14..000000000000 --- a/doc/devel/color_changes.rst +++ /dev/null @@ -1,136 +0,0 @@ -.. _color_changes: - -********************* -Default color changes -********************* - -As discussed at length `elsewhere `__ , -``jet`` is an -empirically bad colormap and should not be the default colormap. -Due to the position that changing the appearance of the plot breaks -backward compatibility, this change has been put off for far longer -than it should have been. In addition to changing the default color -map we plan to take the chance to change the default color-cycle on -plots and to adopt a different colormap for filled plots (``imshow``, -``pcolor``, ``contourf``, etc) and for scatter like plots. - - -Default heat map colormap -------------------------- - -The choice of a new colormap is fertile ground to bike-shedding ("No, -it should be _this_ color") so we have a proposed set criteria (via -Nathaniel Smith) to evaluate proposed colormaps. - -- it should be a sequential colormap, because diverging colormaps are - really misleading unless you know where the "center" of the data is, - and for a default colormap we generally won't. - -- it should be perceptually uniform, i.e., human subjective judgments - of how far apart nearby colors are should correspond as linearly as - possible to the difference between the numerical values they - represent, at least locally. - -- it should have a perceptually uniform luminance ramp, i.e. if you - convert to greyscale it should still be uniform. This is useful both - in practical terms (greyscale printers are still a thing!) and - because luminance is a very strong and natural cue to magnitude. - -- it should also have some kind of variation in hue, because hue - variation is a really helpful additional cue to perception, having - two cues is better than one, and there's no reason not to do it. - -- the hue variation should be chosen to produce reasonable results - even for viewers with the more common types of - colorblindness. (Which rules out things like red-to-green.) - -- For bonus points, it would be nice to choose a hue ramp that still - works if you throw away the luminance variation, because then we - could use the version with varying luminance for 2d plots, and the - version with just hue variation for 3d plots. (In 3d plots you - really want to reserve the luminance channel for lighting/shading, - because your brain is *really* good at extracting 3d shape from - luminance variation. If the 3d surface itself has massively varying - luminance then this screws up the ability to see shape.) - -- Not infringe any existing IP - -Example script -++++++++++++++ - -Proposed colormaps -++++++++++++++++++ - -Default scatter colormap ------------------------- - -For heat-map like applications it can be desirable to cover as much of -the luminance scale as possible, however when colormapping markers, -having markers too close to white can be a problem. For that reason -we propose using a different (but maybe related) colormap to the -heat map for marker-based. The design parameters are the same as -above, only with a more limited luminance variation. - - -Example script -++++++++++++++ -:: - - import numpy as np - import matplotlib.pyplot as plt - - np.random.seed(1234) - - fig, (ax1, ax2) = plt.subplots(1, 2) - - N = 50 - x = np.random.rand(N) - y = np.random.rand(N) - colors = np.random.rand(N) - area = np.pi * (15 * np.random.rand(N))**2 # 0 to 15 point radiuses - - ax1.scatter(x, y, s=area, c=colors, alpha=0.5) - - - X,Y = np.meshgrid(np.arange(0, 2*np.pi, .2), - np.arange(0, 2*np.pi, .2)) - U = np.cos(X) - V = np.sin(Y) - Q = ax2.quiver(X, Y, U, V, units='width') - qd = np.random.rand(np.prod(X.shape)) - Q.set_array(qd) - -Proposed colormaps -++++++++++++++++++ - -Color cycle / qualitative colormap ------------------------------------ - -When plotting lines it is frequently desirable to plot multiple lines -or artists which need to be distinguishable, but there is no inherent -ordering. - - -Example script -++++++++++++++ -:: - - import numpy as np - import matplotlib.pyplot as plt - - fig, (ax1, ax2) = plt.subplots(1, 2) - - x = np.linspace(0, 1, 10) - - for j in range(10): - ax1.plot(x, x * j) - - - th = np.linspace(0, 2*np.pi, 1024) - for j in np.linspace(0, np.pi, 10): - ax2.plot(th, np.sin(th + j)) - - ax2.set_xlim(0, 2*np.pi) - -Proposed color cycle -++++++++++++++++++++ From adaa62f287ade7c1ef228db3ab777fec520f13e6 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 16 Sep 2024 11:21:30 +0200 Subject: [PATCH 0267/1230] DOC: Fix non-working code object references These were not rendered as links in the current docs, because the associated code objects do not exist as targets in the docs. For some reason, sphinx did not complain about it. But it does in some recent PRs (extracted from #28560) - I'm unclear why, but anyway the correct solution is to change to explicitly listing the attributes. --- doc/api/axes_api.rst | 11 +++++++++++ doc/missing-references.json | 4 ---- lib/matplotlib/axes/_axes.py | 7 ------- lib/matplotlib/axes/_base.py | 6 +++++- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/doc/api/axes_api.rst b/doc/api/axes_api.rst index ac4e5bc4f536..6afdad4e768e 100644 --- a/doc/api/axes_api.rst +++ b/doc/api/axes_api.rst @@ -28,6 +28,17 @@ The Axes class Axes +Attributes +---------- + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + Axes.viewLim + Axes.dataLim + Plotting ======== diff --git a/doc/missing-references.json b/doc/missing-references.json index 87c9ce9b716f..8c70fc465ebe 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -338,10 +338,6 @@ "Artist.stale_callback": [ "doc/users/explain/figure/interactive_guide.rst:323" ], - "Axes.dataLim": [ - "doc/api/axes_api.rst:293::1", - "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.update_datalim:2" - ], "AxesBase": [ "doc/api/axes_api.rst:448::1", "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.add_child_axes:2" diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index f187d7a0c4f3..d32fb78b35e5 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -85,13 +85,6 @@ class Axes(_AxesBase): methods instead; e.g. from `.pyplot` or `.Figure`: `~.pyplot.subplots`, `~.pyplot.subplot_mosaic` or `.Figure.add_axes`. - Attributes - ---------- - dataLim : `.Bbox` - The bounding box enclosing all data displayed in the Axes. - viewLim : `.Bbox` - The view limits in data coordinates. - """ ### Labelling, legend and texts diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 6aa5ef1efb7b..c003f51de9a5 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -551,6 +551,9 @@ class _AxesBase(martist.Artist): _subclass_uses_cla = False + dataLim: mtransforms.Bbox + """The bounding `.Bbox` enclosing all data displayed in the Axes.""" + @property def _axis_map(self): """A mapping of axis names, e.g. 'x', to `Axis` instances.""" @@ -849,6 +852,7 @@ def _unstale_viewLim(self): @property def viewLim(self): + """The view limits as `.Bbox` in data coordinates.""" self._unstale_viewLim() return self._viewLim @@ -2265,7 +2269,7 @@ def add_artist(self, a): def add_child_axes(self, ax): """ - Add an `.AxesBase` to the Axes' children; return the child Axes. + Add an `.Axes` to the Axes' children; return the child Axes. This is the lowlevel version. See `.axes.Axes.inset_axes`. """ From b8330557540cf28bcf90ba6e30c2d5b21c846622 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 30 Mar 2024 11:01:36 +0000 Subject: [PATCH 0268/1230] Add InsetIndicator artist --- ci/mypy-stubtest-allowlist.txt | 3 + doc/api/index.rst | 1 + doc/api/inset_api.rst | 8 + .../next_api_changes/behavior/27996-REC.rst | 8 + doc/users/next_whats_new/inset_indicator.rst | 18 ++ lib/matplotlib/axes/_axes.py | 110 +++---- lib/matplotlib/axes/_axes.pyi | 9 +- lib/matplotlib/inset.py | 269 ++++++++++++++++++ lib/matplotlib/inset.pyi | 25 ++ lib/matplotlib/meson.build | 2 + .../zoom_inset_connector_styles.png | Bin 0 -> 18960 bytes lib/matplotlib/tests/test_axes.py | 15 +- lib/matplotlib/tests/test_inset.py | 106 +++++++ 13 files changed, 493 insertions(+), 81 deletions(-) create mode 100644 doc/api/inset_api.rst create mode 100644 doc/api/next_api_changes/behavior/27996-REC.rst create mode 100644 doc/users/next_whats_new/inset_indicator.rst create mode 100644 lib/matplotlib/inset.py create mode 100644 lib/matplotlib/inset.pyi create mode 100644 lib/matplotlib/tests/baseline_images/test_inset/zoom_inset_connector_styles.png create mode 100644 lib/matplotlib/tests/test_inset.py diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 06261a543f99..4b6e487a418d 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -46,3 +46,6 @@ matplotlib.spines.Spine._T # Parameter inconsistency due to 3.10 deprecation matplotlib.figure.FigureBase.get_figure + +# getitem method only exists for 3.10 deprecation backcompatability +matplotlib.inset.InsetIndicator.__getitem__ diff --git a/doc/api/index.rst b/doc/api/index.rst index 53f397a6817a..76b6cd5ffcef 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -104,6 +104,7 @@ Alphabetical list of modules: gridspec_api.rst hatch_api.rst image_api.rst + inset_api.rst layout_engine_api.rst legend_api.rst legend_handler_api.rst diff --git a/doc/api/inset_api.rst b/doc/api/inset_api.rst new file mode 100644 index 000000000000..d8b89a106a7a --- /dev/null +++ b/doc/api/inset_api.rst @@ -0,0 +1,8 @@ +******************** +``matplotlib.inset`` +******************** + +.. automodule:: matplotlib.inset + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/next_api_changes/behavior/27996-REC.rst b/doc/api/next_api_changes/behavior/27996-REC.rst new file mode 100644 index 000000000000..fe81a34073b8 --- /dev/null +++ b/doc/api/next_api_changes/behavior/27996-REC.rst @@ -0,0 +1,8 @@ +``InsetIndicator`` artist +~~~~~~~~~~~~~~~~~~~~~~~~~ + +`~.Axes.indicate_inset` and `~.Axes.indicate_inset_zoom` now return an instance +of `~matplotlib.inset.InsetIndicator`. Use the +`~matplotlib.inset.InsetIndicator.rectangle` and +`~matplotlib.inset.InsetIndicator.connectors` properties of this artist to +access the objects that were previously returned directly. diff --git a/doc/users/next_whats_new/inset_indicator.rst b/doc/users/next_whats_new/inset_indicator.rst new file mode 100644 index 000000000000..614e830e016c --- /dev/null +++ b/doc/users/next_whats_new/inset_indicator.rst @@ -0,0 +1,18 @@ +``InsetIndicator`` artist +~~~~~~~~~~~~~~~~~~~~~~~~~ + +`~.Axes.indicate_inset` and `~.Axes.indicate_inset_zoom` now return an instance +of `~matplotlib.inset.InsetIndicator` which contains the rectangle and +connector patches. These patches now update automatically so that + +.. code-block:: python + + ax.indicate_inset_zoom(ax_inset) + ax_inset.set_xlim(new_lim) + +now gives the same result as + +.. code-block:: python + + ax_inset.set_xlim(new_lim) + ax.indicate_inset_zoom(ax_inset) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 33fc42a4b860..d7b649ae437f 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -16,6 +16,7 @@ import matplotlib.contour as mcontour import matplotlib.dates # noqa: F401, Register date unit converter as side effect. import matplotlib.image as mimage +import matplotlib.inset as minset import matplotlib.legend as mlegend import matplotlib.lines as mlines import matplotlib.markers as mmarkers @@ -419,9 +420,9 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): return inset_ax @_docstring.interpd - def indicate_inset(self, bounds, inset_ax=None, *, transform=None, + def indicate_inset(self, bounds=None, inset_ax=None, *, transform=None, facecolor='none', edgecolor='0.5', alpha=0.5, - zorder=4.99, **kwargs): + zorder=None, **kwargs): """ Add an inset indicator to the Axes. This is a rectangle on the plot at the position indicated by *bounds* that optionally has lines that @@ -433,18 +434,19 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, Parameters ---------- - bounds : [x0, y0, width, height] + bounds : [x0, y0, width, height], optional Lower-left corner of rectangle to be marked, and its width - and height. + and height. If not set, the bounds will be calculated from the + data limits of *inset_ax*, which must be supplied. - inset_ax : `.Axes` + inset_ax : `.Axes`, optional An optional inset Axes to draw connecting lines to. Two lines are drawn connecting the indicator box to the inset Axes on corners chosen so as to not overlap with the indicator box. transform : `.Transform` Transform for the rectangle coordinates. Defaults to - `ax.transAxes`, i.e. the units of *rect* are in Axes-relative + ``ax.transAxes``, i.e. the units of *rect* are in Axes-relative coordinates. facecolor : :mpltype:`color`, default: 'none' @@ -469,15 +471,20 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, Returns ------- - rectangle_patch : `.patches.Rectangle` - The indicator frame. + inset_indicator : `.inset.InsetIndicator` + An artist which contains - connector_lines : 4-tuple of `.patches.ConnectionPatch` - The four connector lines connecting to (lower_left, upper_left, - lower_right upper_right) corners of *inset_ax*. Two lines are - set with visibility to *False*, but the user can set the - visibility to True if the automatic choice is not deemed correct. + inset_indicator.rectangle : `.Rectangle` + The indicator frame. + inset_indicator.connectors : 4-tuple of `.patches.ConnectionPatch` + The four connector lines connecting to (lower_left, upper_left, + lower_right upper_right) corners of *inset_ax*. Two lines are + set with visibility to *False*, but the user can set the + visibility to True if the automatic choice is not deemed correct. + + .. versionchanged:: 3.10 + Previously the rectangle and connectors tuple were returned. """ # to make the Axes connectors work, we need to apply the aspect to # the parent Axes. @@ -487,51 +494,13 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, transform = self.transData kwargs.setdefault('label', '_indicate_inset') - x, y, width, height = bounds - rectangle_patch = mpatches.Rectangle( - (x, y), width, height, + indicator_patch = minset.InsetIndicator( + bounds, inset_ax=inset_ax, facecolor=facecolor, edgecolor=edgecolor, alpha=alpha, zorder=zorder, transform=transform, **kwargs) - self.add_patch(rectangle_patch) - - connects = [] - - if inset_ax is not None: - # connect the inset_axes to the rectangle - for xy_inset_ax in [(0, 0), (0, 1), (1, 0), (1, 1)]: - # inset_ax positions are in axes coordinates - # The 0, 1 values define the four edges if the inset_ax - # lower_left, upper_left, lower_right upper_right. - ex, ey = xy_inset_ax - if self.xaxis.get_inverted(): - ex = 1 - ex - if self.yaxis.get_inverted(): - ey = 1 - ey - xy_data = x + ex * width, y + ey * height - p = mpatches.ConnectionPatch( - xyA=xy_inset_ax, coordsA=inset_ax.transAxes, - xyB=xy_data, coordsB=self.transData, - arrowstyle="-", zorder=zorder, - edgecolor=edgecolor, alpha=alpha) - connects.append(p) - self.add_patch(p) - - # decide which two of the lines to keep visible.... - pos = inset_ax.get_position() - bboxins = pos.transformed(self.get_figure(root=False).transSubfigure) - rectbbox = mtransforms.Bbox.from_bounds( - *bounds - ).transformed(transform) - x0 = rectbbox.x0 < bboxins.x0 - x1 = rectbbox.x1 < bboxins.x1 - y0 = rectbbox.y0 < bboxins.y0 - y1 = rectbbox.y1 < bboxins.y1 - connects[0].set_visible(x0 ^ y0) - connects[1].set_visible(x0 == y1) - connects[2].set_visible(x1 == y0) - connects[3].set_visible(x1 ^ y1) - - return rectangle_patch, tuple(connects) if connects else None + self.add_artist(indicator_patch) + + return indicator_patch def indicate_inset_zoom(self, inset_ax, **kwargs): """ @@ -555,22 +524,23 @@ def indicate_inset_zoom(self, inset_ax, **kwargs): Returns ------- - rectangle_patch : `.patches.Rectangle` - Rectangle artist. - - connector_lines : 4-tuple of `.patches.ConnectionPatch` - Each of four connector lines coming from the rectangle drawn on - this axis, in the order lower left, upper left, lower right, - upper right. - Two are set with visibility to *False*, but the user can - set the visibility to *True* if the automatic choice is not deemed - correct. + inset_indicator : `.inset.InsetIndicator` + An artist which contains + + inset_indicator.rectangle : `.Rectangle` + The indicator frame. + + inset_indicator.connectors : 4-tuple of `.patches.ConnectionPatch` + The four connector lines connecting to (lower_left, upper_left, + lower_right upper_right) corners of *inset_ax*. Two lines are + set with visibility to *False*, but the user can set the + visibility to True if the automatic choice is not deemed correct. + + .. versionchanged:: 3.10 + Previously the rectangle and connectors tuple were returned. """ - xlim = inset_ax.get_xlim() - ylim = inset_ax.get_ylim() - rect = (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]) - return self.indicate_inset(rect, inset_ax, **kwargs) + return self.indicate_inset(None, inset_ax, **kwargs) @_docstring.interpd def secondary_xaxis(self, location, functions=None, *, transform=None, **kwargs): diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 186177576067..de89990d7c13 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -15,6 +15,7 @@ from matplotlib.colors import Colormap, Normalize from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer from matplotlib.contour import ContourSet, QuadContourSet from matplotlib.image import AxesImage, PcolorImage +from matplotlib.inset import InsetIndicator from matplotlib.legend import Legend from matplotlib.legend_handler import HandlerBase from matplotlib.lines import Line2D, AxLine @@ -74,17 +75,17 @@ class Axes(_AxesBase): ) -> Axes: ... def indicate_inset( self, - bounds: tuple[float, float, float, float], + bounds: tuple[float, float, float, float] | None = ..., inset_ax: Axes | None = ..., *, transform: Transform | None = ..., facecolor: ColorType = ..., edgecolor: ColorType = ..., alpha: float = ..., - zorder: float = ..., + zorder: float | None = ..., **kwargs - ) -> Rectangle: ... - def indicate_inset_zoom(self, inset_ax: Axes, **kwargs) -> Rectangle: ... + ) -> InsetIndicator: ... + def indicate_inset_zoom(self, inset_ax: Axes, **kwargs) -> InsetIndicator: ... def secondary_xaxis( self, location: Literal["top", "bottom"] | float, diff --git a/lib/matplotlib/inset.py b/lib/matplotlib/inset.py new file mode 100644 index 000000000000..bab69491303e --- /dev/null +++ b/lib/matplotlib/inset.py @@ -0,0 +1,269 @@ +""" +The inset module defines the InsetIndicator class, which draws the rectangle and +connectors required for `.Axes.indicate_inset` and `.Axes.indicate_inset_zoom`. +""" + +from . import _api, artist, transforms +from matplotlib.patches import ConnectionPatch, PathPatch, Rectangle +from matplotlib.path import Path + + +_shared_properties = ('alpha', 'edgecolor', 'linestyle', 'linewidth') + + +class InsetIndicator(artist.Artist): + """ + An artist to highlight an area of interest. + + An inset indicator is a rectangle on the plot at the position indicated by + *bounds* that optionally has lines that connect the rectangle to an inset + Axes (`.Axes.inset_axes`). + + .. versionadded:: 3.10 + """ + zorder = 4.99 + + def __init__(self, bounds=None, inset_ax=None, zorder=None, **kwargs): + """ + Parameters + ---------- + bounds : [x0, y0, width, height], optional + Lower-left corner of rectangle to be marked, and its width + and height. If not set, the bounds will be calculated from the + data limits of inset_ax, which must be supplied. + + inset_ax : `~.axes.Axes`, optional + An optional inset Axes to draw connecting lines to. Two lines are + drawn connecting the indicator box to the inset Axes on corners + chosen so as to not overlap with the indicator box. + + zorder : float, default: 4.99 + Drawing order of the rectangle and connector lines. The default, + 4.99, is just below the default level of inset Axes. + + **kwargs + Other keyword arguments are passed on to the `.Rectangle` patch. + """ + if bounds is None and inset_ax is None: + raise ValueError("At least one of bounds or inset_ax must be supplied") + + self._inset_ax = inset_ax + + if bounds is None: + # Work out bounds from inset_ax + self._auto_update_bounds = True + bounds = self._bounds_from_inset_ax() + else: + self._auto_update_bounds = False + + x, y, width, height = bounds + + self._rectangle = Rectangle((x, y), width, height, clip_on=False, **kwargs) + + # Connector positions cannot be calculated till the artist has been added + # to an axes, so just make an empty list for now. + self._connectors = [] + + super().__init__() + self.set_zorder(zorder) + + # Initial style properties for the artist should match the rectangle. + for prop in _shared_properties: + setattr(self, f'_{prop}', artist.getp(self._rectangle, prop)) + + def _shared_setter(self, prop, val): + """ + Helper function to set the same style property on the artist and its children. + """ + setattr(self, f'_{prop}', val) + + artist.setp([self._rectangle, *self._connectors], prop, val) + + def set_alpha(self, alpha): + # docstring inherited + self._shared_setter('alpha', alpha) + + def set_edgecolor(self, color): + """ + Set the edge color of the rectangle and the connectors. + + Parameters + ---------- + color : :mpltype:`color` or None + """ + self._shared_setter('edgecolor', color) + + def set_color(self, c): + """ + Set the edgecolor of the rectangle and the connectors, and the + facecolor for the rectangle. + + Parameters + ---------- + c : :mpltype:`color` + """ + self._shared_setter('edgecolor', c) + self._shared_setter('facecolor', c) + + def set_linewidth(self, w): + """ + Set the linewidth in points of the rectangle and the connectors. + + Parameters + ---------- + w : float or None + """ + self._shared_setter('linewidth', w) + + def set_linestyle(self, ls): + """ + Set the linestyle of the rectangle and the connectors. + + ========================================== ================= + linestyle description + ========================================== ================= + ``'-'`` or ``'solid'`` solid line + ``'--'`` or ``'dashed'`` dashed line + ``'-.'`` or ``'dashdot'`` dash-dotted line + ``':'`` or ``'dotted'`` dotted line + ``'none'``, ``'None'``, ``' '``, or ``''`` draw nothing + ========================================== ================= + + Alternatively a dash tuple of the following form can be provided:: + + (offset, onoffseq) + + where ``onoffseq`` is an even length tuple of on and off ink in points. + + Parameters + ---------- + ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...} + The line style. + """ + self._shared_setter('linestyle', ls) + + def _bounds_from_inset_ax(self): + xlim = self._inset_ax.get_xlim() + ylim = self._inset_ax.get_ylim() + return (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]) + + def _update_connectors(self): + (x, y) = self._rectangle.get_xy() + width = self._rectangle.get_width() + height = self._rectangle.get_height() + + existing_connectors = self._connectors or [None] * 4 + + # connect the inset_axes to the rectangle + for xy_inset_ax, existing in zip([(0, 0), (0, 1), (1, 0), (1, 1)], + existing_connectors): + # inset_ax positions are in axes coordinates + # The 0, 1 values define the four edges if the inset_ax + # lower_left, upper_left, lower_right upper_right. + ex, ey = xy_inset_ax + if self.axes.xaxis.get_inverted(): + ex = 1 - ex + if self.axes.yaxis.get_inverted(): + ey = 1 - ey + xy_data = x + ex * width, y + ey * height + if existing is None: + # Create new connection patch with styles inherited from the + # parent artist. + p = ConnectionPatch( + xyA=xy_inset_ax, coordsA=self._inset_ax.transAxes, + xyB=xy_data, coordsB=self.axes.transData, + arrowstyle="-", + edgecolor=self._edgecolor, alpha=self.get_alpha(), + linestyle=self._linestyle, linewidth=self._linewidth) + self._connectors.append(p) + else: + # Only update positioning of existing connection patch. We + # do not want to override any style settings made by the user. + existing.xy1 = xy_inset_ax + existing.xy2 = xy_data + existing.coords1 = self._inset_ax.transAxes + existing.coords2 = self.axes.transData + + if existing is None: + # decide which two of the lines to keep visible.... + pos = self._inset_ax.get_position() + bboxins = pos.transformed(self.get_figure(root=False).transSubfigure) + rectbbox = transforms.Bbox.from_bounds(x, y, width, height).transformed( + self._rectangle.get_transform()) + x0 = rectbbox.x0 < bboxins.x0 + x1 = rectbbox.x1 < bboxins.x1 + y0 = rectbbox.y0 < bboxins.y0 + y1 = rectbbox.y1 < bboxins.y1 + self._connectors[0].set_visible(x0 ^ y0) + self._connectors[1].set_visible(x0 == y1) + self._connectors[2].set_visible(x1 == y0) + self._connectors[3].set_visible(x1 ^ y1) + + @property + def rectangle(self): + """`.Rectangle`: the indicator frame.""" + return self._rectangle + + @property + def connectors(self): + """ + 4-tuple of `.patches.ConnectionPatch` or None + The four connector lines connecting to (lower_left, upper_left, + lower_right upper_right) corners of *inset_ax*. Two lines are + set with visibility to *False*, but the user can set the + visibility to True if the automatic choice is not deemed correct. + """ + if self._inset_ax is None: + return + + if self._auto_update_bounds: + self._rectangle.set_bounds(self._bounds_from_inset_ax()) + self._update_connectors() + return tuple(self._connectors) + + def draw(self, renderer): + # docstring inherited + conn_same_style = [] + + # Figure out which connectors have the same style as the box, so should + # be drawn as a single path. + for conn in self.connectors or []: + if conn.get_visible(): + drawn = False + for s in _shared_properties: + if artist.getp(self._rectangle, s) != artist.getp(conn, s): + # Draw this connector by itself + conn.draw(renderer) + drawn = True + break + + if not drawn: + # Connector has same style as box. + conn_same_style.append(conn) + + if conn_same_style: + # Since at least one connector has the same style as the rectangle, draw + # them as a compound path. + artists = [self._rectangle] + conn_same_style + paths = [a.get_transform().transform_path(a.get_path()) for a in artists] + path = Path.make_compound_path(*paths) + + # Create a temporary patch to draw the path. + p = PathPatch(path) + p.update_from(self._rectangle) + p.set_transform(transforms.IdentityTransform()) + p.draw(renderer) + + return + + # Just draw the rectangle + self._rectangle.draw(renderer) + + @_api.deprecated( + '3.10', + message=('Since Matplotlib 3.10 indicate_inset_[zoom] returns a single ' + 'InsetIndicator artist with a rectangle property and a connectors ' + 'property. From 3.12 it will no longer be possible to unpack the ' + 'return value into two elements.')) + def __getitem__(self, key): + return [self._rectangle, self.connectors][key] diff --git a/lib/matplotlib/inset.pyi b/lib/matplotlib/inset.pyi new file mode 100644 index 000000000000..e895fd7be27c --- /dev/null +++ b/lib/matplotlib/inset.pyi @@ -0,0 +1,25 @@ +from . import artist +from .axes import Axes +from .backend_bases import RendererBase +from .patches import ConnectionPatch, Rectangle + +from .typing import ColorType, LineStyleType + +class InsetIndicator(artist.Artist): + def __init__( + self, + bounds: tuple[float, float, float, float] | None = ..., + inset_ax: Axes | None = ..., + zorder: float | None = ..., + **kwargs + ) -> None: ... + def set_alpha(self, alpha: float | None) -> None: ... + def set_edgecolor(self, color: ColorType | None) -> None: ... + def set_color(self, c: ColorType | None) -> None: ... + def set_linewidth(self, w: float | None) -> None: ... + def set_linestyle(self, ls: LineStyleType | None) -> None: ... + @property + def rectangle(self) -> Rectangle: ... + @property + def connectors(self) -> tuple[ConnectionPatch, ConnectionPatch, ConnectionPatch, ConnectionPatch] | None: ... + def draw(self, renderer: RendererBase) -> None: ... diff --git a/lib/matplotlib/meson.build b/lib/matplotlib/meson.build index 657adfd4e835..e8cf4d129f8d 100644 --- a/lib/matplotlib/meson.build +++ b/lib/matplotlib/meson.build @@ -43,6 +43,7 @@ python_sources = [ 'gridspec.py', 'hatch.py', 'image.py', + 'inset.py', 'layout_engine.py', 'legend_handler.py', 'legend.py', @@ -110,6 +111,7 @@ typing_sources = [ 'gridspec.pyi', 'hatch.pyi', 'image.pyi', + 'inset.pyi', 'layout_engine.pyi', 'legend_handler.pyi', 'legend.pyi', diff --git a/lib/matplotlib/tests/baseline_images/test_inset/zoom_inset_connector_styles.png b/lib/matplotlib/tests/baseline_images/test_inset/zoom_inset_connector_styles.png new file mode 100644 index 0000000000000000000000000000000000000000..df8b768725d7b6a3d2554ac452f617bf34593923 GIT binary patch literal 18960 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 Date: Wed, 18 Sep 2024 15:06:49 -0400 Subject: [PATCH 0269/1230] TYP: Add CoordsType and use in relevant locations(#28532) * TYP: Fix xycoords and friends * Fix organization * Fix linting * Fix import * Fix linting * Fix for earlier Pythons * Fix again for older Pythons * Fix long line * Trailing whitespace * Fix older Pythons again * Fix mypy stub check * Fix mypy stubtests * Fix stubtest for Python 3.9 * Handle suggestions in PR * Fix lint and stubtest --- lib/matplotlib/axes/_axes.pyi | 16 +++------- lib/matplotlib/offsetbox.pyi | 28 ++++-------------- lib/matplotlib/pyplot.py | 24 +++++++-------- lib/matplotlib/text.pyi | 55 +++++++---------------------------- lib/matplotlib/typing.py | 19 +++++++++++- 5 files changed, 49 insertions(+), 93 deletions(-) diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 186177576067..78a1f146e27f 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -22,7 +22,8 @@ from matplotlib.mlab import GaussianKDE from matplotlib.patches import Rectangle, FancyArrow, Polygon, StepPatch, Wedge from matplotlib.quiver import Quiver, QuiverKey, Barbs from matplotlib.text import Annotation, Text -from matplotlib.transforms import Transform, Bbox +from matplotlib.transforms import Transform +from matplotlib.typing import CoordsType import matplotlib.tri as mtri import matplotlib.table as mtable import matplotlib.stackplot as mstack @@ -122,17 +123,8 @@ class Axes(_AxesBase): text: str, xy: tuple[float, float], xytext: tuple[float, float] | None = ..., - xycoords: str - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] - | tuple[float, float] = ..., - textcoords: str - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] - | tuple[float, float] - | None = ..., + xycoords: CoordsType = ..., + textcoords: CoordsType | None = ..., arrowprops: dict[str, Any] | None = ..., annotation_clip: bool | None = ..., **kwargs diff --git a/lib/matplotlib/offsetbox.pyi b/lib/matplotlib/offsetbox.pyi index 05e23df4529d..3b1520e17138 100644 --- a/lib/matplotlib/offsetbox.pyi +++ b/lib/matplotlib/offsetbox.pyi @@ -7,6 +7,7 @@ from matplotlib.font_manager import FontProperties from matplotlib.image import BboxImage from matplotlib.patches import FancyArrowPatch, FancyBboxPatch from matplotlib.transforms import Bbox, BboxBase, Transform +from matplotlib.typing import CoordsType import numpy as np from numpy.typing import ArrayLike @@ -219,9 +220,7 @@ class AnnotationBbox(martist.Artist, mtext._AnnotationBase): offsetbox: OffsetBox arrowprops: dict[str, Any] | None xybox: tuple[float, float] - boxcoords: str | tuple[str, str] | martist.Artist | Transform | Callable[ - [RendererBase], Bbox | Transform - ] + boxcoords: CoordsType arrow_patch: FancyArrowPatch | None patch: FancyBboxPatch prop: FontProperties @@ -230,17 +229,8 @@ class AnnotationBbox(martist.Artist, mtext._AnnotationBase): offsetbox: OffsetBox, xy: tuple[float, float], xybox: tuple[float, float] | None = ..., - xycoords: str - | tuple[str, str] - | martist.Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] = ..., - boxcoords: str - | tuple[str, str] - | martist.Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] - | None = ..., + xycoords: CoordsType = ..., + boxcoords: CoordsType | None = ..., *, frameon: bool = ..., pad: float = ..., @@ -258,17 +248,11 @@ class AnnotationBbox(martist.Artist, mtext._AnnotationBase): @property def anncoords( self, - ) -> str | tuple[str, str] | martist.Artist | Transform | Callable[ - [RendererBase], Bbox | Transform - ]: ... + ) -> CoordsType: ... @anncoords.setter def anncoords( self, - coords: str - | tuple[str, str] - | martist.Artist - | Transform - | Callable[[RendererBase], Bbox | Transform], + coords: CoordsType, ) -> None: ... def get_children(self) -> list[martist.Artist]: ... def set_figure(self, fig: Figure | SubFigure) -> None: ... diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index b871bc58a4b4..744eee0e4b9f 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -96,7 +96,7 @@ import matplotlib.backend_bases from matplotlib.axis import Tick from matplotlib.axes._base import _AxesBase - from matplotlib.backend_bases import RendererBase, Event + from matplotlib.backend_bases import Event from matplotlib.cm import ScalarMappable from matplotlib.contour import ContourSet, QuadContourSet from matplotlib.collections import ( @@ -120,8 +120,13 @@ from matplotlib.patches import FancyArrow, StepPatch, Wedge from matplotlib.quiver import Barbs, Quiver, QuiverKey from matplotlib.scale import ScaleBase - from matplotlib.transforms import Transform, Bbox - from matplotlib.typing import ColorType, LineStyleType, MarkerType, HashableList + from matplotlib.typing import ( + ColorType, + CoordsType, + HashableList, + LineStyleType, + MarkerType, + ) from matplotlib.widgets import SubplotTool _P = ParamSpec('_P') @@ -2860,17 +2865,8 @@ def annotate( text: str, xy: tuple[float, float], xytext: tuple[float, float] | None = None, - xycoords: str - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] - | tuple[float, float] = "data", - textcoords: str - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] - | tuple[float, float] - | None = None, + xycoords: CoordsType = "data", + textcoords: CoordsType | None = None, arrowprops: dict[str, Any] | None = None, annotation_clip: bool | None = None, **kwargs, diff --git a/lib/matplotlib/text.pyi b/lib/matplotlib/text.pyi index 902f0a00dfe8..d65a3dc4c7da 100644 --- a/lib/matplotlib/text.pyi +++ b/lib/matplotlib/text.pyi @@ -16,7 +16,7 @@ from .transforms import ( from collections.abc import Callable, Iterable from typing import Any, Literal -from .typing import ColorType +from .typing import ColorType, CoordsType class Text(Artist): zorder: float @@ -120,17 +120,11 @@ class OffsetFrom: class _AnnotationBase: xy: tuple[float, float] - xycoords: str | tuple[str, str] | Artist | Transform | Callable[ - [RendererBase], Bbox | Transform - ] + xycoords: CoordsType def __init__( self, xy, - xycoords: str - | tuple[str, str] - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] = ..., + xycoords: CoordsType = ..., annotation_clip: bool | None = ..., ) -> None: ... def set_annotation_clip(self, b: bool | None) -> None: ... @@ -147,17 +141,8 @@ class Annotation(Text, _AnnotationBase): text: str, xy: tuple[float, float], xytext: tuple[float, float] | None = ..., - xycoords: str - | tuple[str, str] - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] = ..., - textcoords: str - | tuple[str, str] - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform] - | None = ..., + xycoords: CoordsType = ..., + textcoords: CoordsType | None = ..., arrowprops: dict[str, Any] | None = ..., annotation_clip: bool | None = ..., **kwargs @@ -165,17 +150,11 @@ class Annotation(Text, _AnnotationBase): @property def xycoords( self, - ) -> str | tuple[str, str] | Artist | Transform | Callable[ - [RendererBase], Bbox | Transform - ]: ... + ) -> CoordsType: ... @xycoords.setter def xycoords( self, - xycoords: str - | tuple[str, str] - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform], + xycoords: CoordsType, ) -> None: ... @property def xyann(self) -> tuple[float, float]: ... @@ -183,31 +162,19 @@ class Annotation(Text, _AnnotationBase): def xyann(self, xytext: tuple[float, float]) -> None: ... def get_anncoords( self, - ) -> str | tuple[str, str] | Artist | Transform | Callable[ - [RendererBase], Bbox | Transform - ]: ... + ) -> CoordsType: ... def set_anncoords( self, - coords: str - | tuple[str, str] - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform], + coords: CoordsType, ) -> None: ... @property def anncoords( self, - ) -> str | tuple[str, str] | Artist | Transform | Callable[ - [RendererBase], Bbox | Transform - ]: ... + ) -> CoordsType: ... @anncoords.setter def anncoords( self, - coords: str - | tuple[str, str] - | Artist - | Transform - | Callable[[RendererBase], Bbox | Transform], + coords: CoordsType, ) -> None: ... def update_positions(self, renderer: RendererBase) -> None: ... # Drops `dpi` parameter from superclass diff --git a/lib/matplotlib/typing.py b/lib/matplotlib/typing.py index b70b4cc264dc..20e1022fa0a5 100644 --- a/lib/matplotlib/typing.py +++ b/lib/matplotlib/typing.py @@ -11,11 +11,14 @@ """ from collections.abc import Hashable, Sequence import pathlib -from typing import Any, Literal, TypeAlias, TypeVar +from typing import Any, Callable, Literal, TypeAlias, TypeVar, Union from . import path from ._enums import JoinStyle, CapStyle +from .artist import Artist +from .backend_bases import RendererBase from .markers import MarkerStyle +from .transforms import Bbox, Transform RGBColorType: TypeAlias = tuple[float, float, float] | str RGBAColorType: TypeAlias = ( @@ -49,6 +52,20 @@ JoinStyleType: TypeAlias = JoinStyle | Literal["miter", "round", "bevel"] CapStyleType: TypeAlias = CapStyle | Literal["butt", "projecting", "round"] +CoordsBaseType = Union[ + str, + Artist, + Transform, + Callable[ + [RendererBase], + Union[Bbox, Transform] + ] +] +CoordsType = Union[ + CoordsBaseType, + tuple[CoordsBaseType, CoordsBaseType] +] + RcStyleType: TypeAlias = ( str | dict[str, Any] | From f6a7188b5738ed9bb61f35e68f074273fc687295 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 27 Oct 2023 16:28:55 -0400 Subject: [PATCH 0270/1230] API: finish LocationEvent.lastevent removal --- doc/api/next_api_changes/removals/27218-TAC.rst | 7 +++++++ lib/matplotlib/backend_bases.py | 6 ------ lib/matplotlib/backend_bases.pyi | 1 - 3 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 doc/api/next_api_changes/removals/27218-TAC.rst diff --git a/doc/api/next_api_changes/removals/27218-TAC.rst b/doc/api/next_api_changes/removals/27218-TAC.rst new file mode 100644 index 000000000000..ac69e8a96a26 --- /dev/null +++ b/doc/api/next_api_changes/removals/27218-TAC.rst @@ -0,0 +1,7 @@ +Remove hard reference to ``lastevent`` in ``LocationEvent`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +This was previously used to detect exiting from axes, however the hard +reference would keep closed `.Figure` objects and their children alive longer +than expected. diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index a25b442711a0..a8170ce4f6b0 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1271,10 +1271,6 @@ class LocationEvent(Event): The keyboard modifiers currently being pressed (except for KeyEvent). """ - # Fully delete all occurrences of lastevent after deprecation elapses. - _lastevent = None - lastevent = _api.deprecated("3.8")( - _api.classproperty(lambda cls: cls._lastevent)) _last_axes_ref = None def __init__(self, name, canvas, x, y, guiEvent=None, *, modifiers=None): @@ -1527,8 +1523,6 @@ def _mouse_handler(event): event.canvas.callbacks.process("axes_enter_event", event) LocationEvent._last_axes_ref = ( weakref.ref(event.inaxes) if event.inaxes else None) - LocationEvent._lastevent = ( - None if event.name == "figure_leave_event" else event) def _get_renderer(figure, print_method=None): diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index 075d87a6edd8..c2fc61e386d8 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -220,7 +220,6 @@ class ResizeEvent(Event): class CloseEvent(Event): ... class LocationEvent(Event): - lastevent: Event | None x: int y: int inaxes: Axes | None From aefe7ad635cd27a0fbd08b0961425c56deb4463b Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 18 Sep 2024 22:40:40 +0200 Subject: [PATCH 0271/1230] MNT: Use __init__ parameters of font properties Replace default initialization immediately followed by setting values by initialization with values, i.e. ``` # before: fp = FontProperties() fp.set_[something](val) # after fp = FontProperties([something]=val) ``` This is clearer and additionally helps with the possible transition of making FontProperties immutable, see #22495. --- .../text_labels_and_annotations/fonts_demo.py | 17 +++++------------ galleries/users_explain/text/text_intro.py | 5 +---- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/galleries/examples/text_labels_and_annotations/fonts_demo.py b/galleries/examples/text_labels_and_annotations/fonts_demo.py index bc36c10bce8b..821ee278c4ba 100644 --- a/galleries/examples/text_labels_and_annotations/fonts_demo.py +++ b/galleries/examples/text_labels_and_annotations/fonts_demo.py @@ -21,34 +21,28 @@ fig.text(0.1, 0.9, 'family', fontproperties=heading_font, **alignment) families = ['serif', 'sans-serif', 'cursive', 'fantasy', 'monospace'] for k, family in enumerate(families): - font = FontProperties() - font.set_family(family) + font = FontProperties(family=[family]) fig.text(0.1, yp[k], family, fontproperties=font, **alignment) # Show style options styles = ['normal', 'italic', 'oblique'] fig.text(0.3, 0.9, 'style', fontproperties=heading_font, **alignment) for k, style in enumerate(styles): - font = FontProperties() - font.set_family('sans-serif') - font.set_style(style) + font = FontProperties(family='sans-serif', style=style) fig.text(0.3, yp[k], style, fontproperties=font, **alignment) # Show variant options variants = ['normal', 'small-caps'] fig.text(0.5, 0.9, 'variant', fontproperties=heading_font, **alignment) for k, variant in enumerate(variants): - font = FontProperties() - font.set_family('serif') - font.set_variant(variant) + font = FontProperties(family='serif', variant=variant) fig.text(0.5, yp[k], variant, fontproperties=font, **alignment) # Show weight options weights = ['light', 'normal', 'medium', 'semibold', 'bold', 'heavy', 'black'] fig.text(0.7, 0.9, 'weight', fontproperties=heading_font, **alignment) for k, weight in enumerate(weights): - font = FontProperties() - font.set_weight(weight) + font = FontProperties(weight=weight) fig.text(0.7, yp[k], weight, fontproperties=font, **alignment) # Show size options @@ -56,8 +50,7 @@ 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'] fig.text(0.9, 0.9, 'size', fontproperties=heading_font, **alignment) for k, size in enumerate(sizes): - font = FontProperties() - font.set_size(size) + font = FontProperties(size=size) fig.text(0.9, yp[k], size, fontproperties=font, **alignment) # Show bold italic diff --git a/galleries/users_explain/text/text_intro.py b/galleries/users_explain/text/text_intro.py index 948545667fa9..3b8a66f1c98e 100644 --- a/galleries/users_explain/text/text_intro.py +++ b/galleries/users_explain/text/text_intro.py @@ -177,10 +177,7 @@ from matplotlib.font_manager import FontProperties -font = FontProperties() -font.set_family('serif') -font.set_name('Times New Roman') -font.set_style('italic') +font = FontProperties(family='Times New Roman', style='italic') fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(bottom=0.15, left=0.2) From ae85b62e623baffa6679f9f3a58384ba0e1e5947 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 18 Sep 2024 22:36:21 -0400 Subject: [PATCH 0272/1230] Backport PR #28836: MNT: Use __init__ parameters of font properties --- .../text_labels_and_annotations/fonts_demo.py | 17 +++++------------ galleries/users_explain/text/text_intro.py | 5 +---- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/galleries/examples/text_labels_and_annotations/fonts_demo.py b/galleries/examples/text_labels_and_annotations/fonts_demo.py index bc36c10bce8b..821ee278c4ba 100644 --- a/galleries/examples/text_labels_and_annotations/fonts_demo.py +++ b/galleries/examples/text_labels_and_annotations/fonts_demo.py @@ -21,34 +21,28 @@ fig.text(0.1, 0.9, 'family', fontproperties=heading_font, **alignment) families = ['serif', 'sans-serif', 'cursive', 'fantasy', 'monospace'] for k, family in enumerate(families): - font = FontProperties() - font.set_family(family) + font = FontProperties(family=[family]) fig.text(0.1, yp[k], family, fontproperties=font, **alignment) # Show style options styles = ['normal', 'italic', 'oblique'] fig.text(0.3, 0.9, 'style', fontproperties=heading_font, **alignment) for k, style in enumerate(styles): - font = FontProperties() - font.set_family('sans-serif') - font.set_style(style) + font = FontProperties(family='sans-serif', style=style) fig.text(0.3, yp[k], style, fontproperties=font, **alignment) # Show variant options variants = ['normal', 'small-caps'] fig.text(0.5, 0.9, 'variant', fontproperties=heading_font, **alignment) for k, variant in enumerate(variants): - font = FontProperties() - font.set_family('serif') - font.set_variant(variant) + font = FontProperties(family='serif', variant=variant) fig.text(0.5, yp[k], variant, fontproperties=font, **alignment) # Show weight options weights = ['light', 'normal', 'medium', 'semibold', 'bold', 'heavy', 'black'] fig.text(0.7, 0.9, 'weight', fontproperties=heading_font, **alignment) for k, weight in enumerate(weights): - font = FontProperties() - font.set_weight(weight) + font = FontProperties(weight=weight) fig.text(0.7, yp[k], weight, fontproperties=font, **alignment) # Show size options @@ -56,8 +50,7 @@ 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'] fig.text(0.9, 0.9, 'size', fontproperties=heading_font, **alignment) for k, size in enumerate(sizes): - font = FontProperties() - font.set_size(size) + font = FontProperties(size=size) fig.text(0.9, yp[k], size, fontproperties=font, **alignment) # Show bold italic diff --git a/galleries/users_explain/text/text_intro.py b/galleries/users_explain/text/text_intro.py index 948545667fa9..3b8a66f1c98e 100644 --- a/galleries/users_explain/text/text_intro.py +++ b/galleries/users_explain/text/text_intro.py @@ -177,10 +177,7 @@ from matplotlib.font_manager import FontProperties -font = FontProperties() -font.set_family('serif') -font.set_name('Times New Roman') -font.set_style('italic') +font = FontProperties(family='Times New Roman', style='italic') fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(bottom=0.15, left=0.2) From 077ba10c5427354c6cbe456dd92617323f2c0d31 Mon Sep 17 00:00:00 2001 From: MischaMegens2 <122418839+MischaMegens2@users.noreply.github.com> Date: Wed, 10 Jul 2024 10:04:12 -0700 Subject: [PATCH 0273/1230] Make mplot3d mouse rotation style adjustable Addresses Issue #28408 - matplotlibrc: add axes3d.mouserotationstyle and axes3d.trackballsize - lib/matplotlib/rcsetup.py: add validation for axes3d.mouserotationstyle and axes3d.trackballsize - axes3d.py: implement various mouse rotation styles - update test_axes3d.py::test_rotate() - view_angles.rst: add documentation for the mouse rotation styles - update next_whats_new/mouse_rotation.rst --- doc/api/toolkits/mplot3d/view_angles.rst | 116 ++++++++++++++++++ doc/users/next_whats_new/mouse_rotation.rst | 11 +- lib/matplotlib/mpl-data/matplotlibrc | 4 + lib/matplotlib/rcsetup.py | 4 + lib/mpl_toolkits/mplot3d/axes3d.py | 84 +++++++++---- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 104 +++++++++++----- 6 files changed, 266 insertions(+), 57 deletions(-) diff --git a/doc/api/toolkits/mplot3d/view_angles.rst b/doc/api/toolkits/mplot3d/view_angles.rst index ce2c5f5698a5..377e1452911a 100644 --- a/doc/api/toolkits/mplot3d/view_angles.rst +++ b/doc/api/toolkits/mplot3d/view_angles.rst @@ -38,3 +38,119 @@ further documented in the `.mplot3d.axes3d.Axes3D.view_init` API. .. plot:: gallery/mplot3d/view_planes_3d.py :align: center + + +Rotation with mouse +=================== + +3D plots can be reoriented by dragging the mouse. +There are various ways to accomplish this; the style of mouse rotation +can be specified by setting ``rcParams.axes3d.mouserotationstyle``, see +:doc:`/users/explain/customizing`. + +Originally (with ``mouserotationstyle: azel``), the 2D mouse position +corresponded directly to azimuth and elevation; this is also how it is done +in `MATLAB `_. +This approach works fine for polar plots, where the *z* axis is special; +however, it leads to a kind of 'gimbal lock' when looking down the *z* axis: +the plot reacts differently to mouse movement, dependent on the particular +orientation at hand. Also, 'roll' cannot be controlled. + +As an alternative, there are various mouse rotation styles where the mouse +manipulates a 'trackball'. In its simplest form (``mouserotationstyle: trackball``), +the trackball rotates around an in-plane axis perpendicular to the mouse motion +(it is as if there is a plate laying on the trackball; the plate itself is fixed +in orientation, but you can drag the plate with the mouse, thus rotating the ball). +This is more natural to work with than the ``azel`` style; however, +the plot cannot be easily rotated around the viewing direction - one has to +drag the mouse in circles with a handedness opposite to the desired rotation. + +A different variety of trackball rotates along the shortest arc on the virtual +sphere (``mouserotationstyle: arcball``); it is a variation on Ken Shoemake's +ARCBALL [Shoemake1992]_. Rotating around the viewing direction is straightforward +with it. Shoemake's original arcball is also available +(``mouserotationstyle: Shoemake``); it is free of hysteresis, i.e., +returning mouse to the original position returns the figure to its original +orientation, the rotation is independent of the details of the path the mouse +took. However, Shoemake's arcball rotates at twice the angular rate of the +mouse movement (it is quite noticeable, especially when adjusting roll). +So it is a trade-off. + +Shoemake's arcball has an abrupt edge; this is remedied in Holroyd's arcball +(``mouserotationstyle: Holroyd``). + +Henriksen et al. [Henriksen2002]_ provide an overview. + +In summary: + +.. list-table:: + :width: 100% + :widths: 30 20 20 20 35 + + * - Style + - traditional [1]_ + - incl. roll [2]_ + - uniform [3]_ + - path independent [4]_ + * - azel + - ✔️ + - ❌ + - ❌ + - ✔️ + * - trackball + - ❌ + - ~ + - ✔️ + - ❌ + * - arcball + - ❌ + - ✔️ + - ✔️ + - ❌ + * - Shoemake + - ❌ + - ✔️ + - ✔️ + - ✔️ + * - Holroyd + - ❌ + - ✔️ + - ✔️ + - ✔️ + + +.. [1] The way it was historically; this is also MATLAB's style +.. [2] Mouse controls roll too (not only azimuth and elevation) +.. [3] Figure reacts the same way to mouse movements, regardless of orientation (no difference between 'poles' and 'equator') +.. [4] Returning mouse to original position returns figure to original orientation (no hysteresis: rotation is independent of the details of the path the mouse took) + +Try it out by adding a file ``matplotlibrc`` to folder ``matplotlib\galleries\examples\mplot3d``, +with contents:: + + axes3d.mouserotationstyle: arcball + +(or any of the other styles), and run a suitable example, e.g.:: + + python surfaced3d.py + +(If eternal compatibility with the horrors of the past is less of a consideration +for you, then it is likely that you would want to go with ``arcball``, ``Shoemake``, +or ``Holroyd``.) + +The size of the trackball or arcball can be adjusted by setting +``rcParams.axes3d.trackballsize``, in units of the Axes bounding box; +i.e., to make the trackball span the whole bounding box, set it to 1. +A size of ca. 2/3 appears to work reasonably well. + +---- + +.. [Shoemake1992] Ken Shoemake, "ARCBALL: A user interface for specifying + three-dimensional rotation using a mouse", in Proceedings of Graphics + Interface '92, 1992, pp. 151-156, https://doi.org/10.20380/GI1992.18 + +.. [Henriksen2002] Knud Henriksen, Jon Sporring, Kasper Hornbæk, + "Virtual Trackballs Revisited", in Proceedings of DSAGM'2002: + http://www.diku.dk/~kash/papers/DSAGM2002_henriksen.pdf; + and in IEEE Transactions on Visualization + and Computer Graphics, Volume 10, Issue 2, March-April 2004, pp. 206-216, + https://doi.org/10.1109/TVCG.2004.1260772 diff --git a/doc/users/next_whats_new/mouse_rotation.rst b/doc/users/next_whats_new/mouse_rotation.rst index 64fca63ec472..00198565c54e 100644 --- a/doc/users/next_whats_new/mouse_rotation.rst +++ b/doc/users/next_whats_new/mouse_rotation.rst @@ -4,9 +4,12 @@ Rotating 3d plots with the mouse Rotating three-dimensional plots with the mouse has been made more intuitive. The plot now reacts the same way to mouse movement, independent of the particular orientation at hand; and it is possible to control all 3 rotational -degrees of freedom (azimuth, elevation, and roll). It uses a variation on -Ken Shoemake's ARCBALL [Shoemake1992]_. +degrees of freedom (azimuth, elevation, and roll). By default, +it uses a variation on Ken Shoemake's ARCBALL [1]_. +The particular style of mouse rotation can be set via +``rcParams.axes3d.mouserotationstyle``. +See also :doc:`/api/toolkits/mplot3d/view_angles`. -.. [Shoemake1992] Ken Shoemake, "ARCBALL: A user interface for specifying - three-dimensional rotation using a mouse." in Proceedings of Graphics +.. [1] Ken Shoemake, "ARCBALL: A user interface for specifying + three-dimensional rotation using a mouse", in Proceedings of Graphics Interface '92, 1992, pp. 151-156, https://doi.org/10.20380/GI1992.18 diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index d419ed6e5af7..d56043d5581c 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -433,6 +433,10 @@ #axes3d.yaxis.panecolor: (0.90, 0.90, 0.90, 0.5) # background pane on 3D axes #axes3d.zaxis.panecolor: (0.925, 0.925, 0.925, 0.5) # background pane on 3D axes +#axes3d.mouserotationstyle: arcball # {azel, trackball, arcball, Shoemake, Holroyd} + # See also https://matplotlib.org/stable/api/toolkits/mplot3d/view_angles.html#rotation-with-mouse +#axes3d.trackballsize: 0.667 # trackball diameter, in units of the Axes bbox + ## *************************************************************************** ## * AXIS * ## *************************************************************************** diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index e84b0539385b..e9395207fb99 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -1132,6 +1132,10 @@ def _convert_validator_spec(key, conv): "axes3d.yaxis.panecolor": validate_color, # 3d background pane "axes3d.zaxis.panecolor": validate_color, # 3d background pane + "axes3d.mouserotationstyle": ["azel", "trackball", "arcball", + "Shoemake", "Holroyd"], + "axes3d.trackballsize": validate_float, + # scatter props "scatter.marker": _validate_marker, "scatter.edgecolors": validate_string, diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 5d522cd0988a..3d108420422e 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -1508,7 +1508,7 @@ def _calc_coord(self, xv, yv, renderer=None): p2 = p1 - scale*vec return p2, pane_idx - def _arcball(self, x: float, y: float) -> np.ndarray: + def _arcball(self, x: float, y: float, Holroyd: bool) -> np.ndarray: """ Convert a point (x, y) to a point on a virtual trackball This is Ken Shoemake's arcball @@ -1517,13 +1517,20 @@ def _arcball(self, x: float, y: float) -> np.ndarray: Proceedings of Graphics Interface '92, 1992, pp. 151-156, https://doi.org/10.20380/GI1992.18 """ - x *= 2 - y *= 2 + s = mpl.rcParams['axes3d.trackballsize'] / 2 + x /= s + y /= s r2 = x*x + y*y - if r2 > 1: - p = np.array([0, x/math.sqrt(r2), y/math.sqrt(r2)]) - else: - p = np.array([math.sqrt(1-r2), x, y]) + if Holroyd: + if r2 > 0.5: + p = np.array([1/(2*math.sqrt(r2)), x, y])/math.sqrt(1/(4*r2)+r2) + else: + p = np.array([math.sqrt(1-r2), x, y]) + else: # Shoemake + if r2 > 1: + p = np.array([0, x/math.sqrt(r2), y/math.sqrt(r2)]) + else: + p = np.array([math.sqrt(1-r2), x, y]) return p def _on_move(self, event): @@ -1561,23 +1568,49 @@ def _on_move(self, event): if dx == 0 and dy == 0: return - # Convert to quaternion - elev = np.deg2rad(self.elev) - azim = np.deg2rad(self.azim) - roll = np.deg2rad(self.roll) - q = _Quaternion.from_cardan_angles(elev, azim, roll) - - # Update quaternion - a variation on Ken Shoemake's ARCBALL - current_vec = self._arcball(self._sx/w, self._sy/h) - new_vec = self._arcball(x/w, y/h) - dq = _Quaternion.rotate_from_to(current_vec, new_vec) - q = dq * q - - # Convert to elev, azim, roll - elev, azim, roll = q.as_cardan_angles() - azim = np.rad2deg(azim) - elev = np.rad2deg(elev) - roll = np.rad2deg(roll) + style = mpl.rcParams['axes3d.mouserotationstyle'] + if style == 'azel': + roll = np.deg2rad(self.roll) + delev = -(dy/h)*180*np.cos(roll) + (dx/w)*180*np.sin(roll) + dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll) + elev = self.elev + delev + azim = self.azim + dazim + roll = self.roll + else: + # Convert to quaternion + elev = np.deg2rad(self.elev) + azim = np.deg2rad(self.azim) + roll = np.deg2rad(self.roll) + q = _Quaternion.from_cardan_angles(elev, azim, roll) + + if style in ['arcball', 'Shoemake', 'Holroyd']: + # Update quaternion + is_Holroyd = (style == 'Holroyd') + current_vec = self._arcball(self._sx/w, self._sy/h, is_Holroyd) + new_vec = self._arcball(x/w, y/h, is_Holroyd) + if style == 'arcball': + dq = _Quaternion.rotate_from_to(current_vec, new_vec) + else: # 'Shoemake', 'Holroyd' + dq = _Quaternion(0, new_vec) * _Quaternion(0, -current_vec) + q = dq * q + elif style == 'trackball': + s = mpl.rcParams['axes3d.trackballsize'] / 2 + k = np.array([0, -(y-self._sy)/h, (x-self._sx)/w]) / s + nk = np.linalg.norm(k) + th = nk / 2 + dq = _Quaternion(math.cos(th), k*math.sin(th)/nk) + q = dq * q + else: + warnings.warn("Mouse rotation style (axes3d.mouserotationstyle: " + + style + ") not recognized.") + + # Convert to elev, azim, roll + elev, azim, roll = q.as_cardan_angles() + elev = np.rad2deg(elev) + azim = np.rad2deg(azim) + roll = np.rad2deg(roll) + + # update view vertical_axis = self._axis_names[self._vertical_axis] self.view_init( elev=elev, @@ -3984,7 +4017,7 @@ def rotate_from_to(cls, r1, r2): k = np.cross(r1, r2) nk = np.linalg.norm(k) th = np.arctan2(nk, np.dot(r1, r2)) - th = th/2 + th /= 2 if nk == 0: # r1 and r2 are parallel or anti-parallel if np.dot(r1, r2) < 0: warnings.warn("Rotation defined by anti-parallel vectors is ambiguous") @@ -4021,6 +4054,7 @@ def as_cardan_angles(self): """ The inverse of `from_cardan_angles()`. Note that the angles returned are in radians, not degrees. + The angles are not sensitive to the quaternion's norm(). """ qw = self.scalar qx, qy, qz = self.vector[..., :] diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 0afcae99c980..02a58eadff1a 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -1939,37 +1939,85 @@ def test_quaternion(): np.deg2rad(elev), np.deg2rad(azim), np.deg2rad(roll)) assert np.isclose(q.norm, 1) q = Quaternion(mag * q.scalar, mag * q.vector) - e, a, r = np.rad2deg(Quaternion.as_cardan_angles(q)) - assert np.isclose(e, elev) - assert np.isclose(a, azim) - assert np.isclose(r, roll) + np.testing.assert_allclose(np.rad2deg(Quaternion.as_cardan_angles(q)), + (elev, azim, roll), atol=1e-6) -def test_rotate(): +@pytest.mark.parametrize('style', + ('azel', 'trackball', 'arcball', 'Shoemake', 'Holroyd')) +def test_rotate(style): """Test rotating using the left mouse button.""" - for roll, dx, dy, new_elev, new_azim, new_roll in [ - [0, 0.5, 0, 0, -90, 0], - [30, 0.5, 0, 30, -90, 0], - [0, 0, 0.5, -90, 0, 0], - [30, 0, 0.5, -60, -90, 90], - [0, 0.5, 0.5, -45, -90, 45], - [30, 0.5, 0.5, -15, -90, 45]]: - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1, projection='3d') - ax.view_init(0, 0, roll) - fig.canvas.draw() - - # drag mouse to change orientation - ax._button_press( - mock_event(ax, button=MouseButton.LEFT, xdata=0, ydata=0)) - ax._on_move( - mock_event(ax, button=MouseButton.LEFT, - xdata=dx*ax._pseudo_w, ydata=dy*ax._pseudo_h)) - fig.canvas.draw() - - assert np.isclose(ax.elev, new_elev) - assert np.isclose(ax.azim, new_azim) - assert np.isclose(ax.roll, new_roll) + if style == 'azel': + s = 0.5 + else: + s = mpl.rcParams['axes3d.trackballsize'] / 2 + s *= 0.5 + with mpl.rc_context({'axes3d.mouserotationstyle': style}): + for roll, dx, dy in [ + [0, 1, 0], + [30, 1, 0], + [0, 0, 1], + [30, 0, 1], + [0, 0.5, np.sqrt(3)/2], + [30, 0.5, np.sqrt(3)/2], + [0, 2, 0]]: + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1, projection='3d') + ax.view_init(0, 0, roll) + ax.figure.canvas.draw() + + # drag mouse to change orientation + ax._button_press( + mock_event(ax, button=MouseButton.LEFT, xdata=0, ydata=0)) + ax._on_move( + mock_event(ax, button=MouseButton.LEFT, + xdata=s*dx*ax._pseudo_w, ydata=s*dy*ax._pseudo_h)) + ax.figure.canvas.draw() + + c = np.sqrt(3)/2 + expectations = { + ('azel', 0, 1, 0): (0, -45, 0), + ('azel', 0, 0, 1): (-45, 0, 0), + ('azel', 0, 0.5, c): (-38.971143, -22.5, 0), + ('azel', 0, 2, 0): (0, -90, 0), + ('azel', 30, 1, 0): (22.5, -38.971143, 30), + ('azel', 30, 0, 1): (-38.971143, -22.5, 30), + ('azel', 30, 0.5, c): (-22.5, -38.971143, 30), + + ('trackball', 0, 1, 0): (0, -28.64789, 0), + ('trackball', 0, 0, 1): (-28.64789, 0, 0), + ('trackball', 0, 0.5, c): (-24.531578, -15.277726, 3.340403), + ('trackball', 0, 2, 0): (0, -180/np.pi, 0), + ('trackball', 30, 1, 0): (13.869588, -25.319385, 26.87008), + ('trackball', 30, 0, 1): (-24.531578, -15.277726, 33.340403), + ('trackball', 30, 0.5, c): (-13.869588, -25.319385, 33.129920), + + ('arcball', 0, 1, 0): (0, -30, 0), + ('arcball', 0, 0, 1): (-30, 0, 0), + ('arcball', 0, 0.5, c): (-25.658906, -16.102114, 3.690068), + ('arcball', 0, 2, 0): (0, -90, 0), + ('arcball', 30, 1, 0): (14.477512, -26.565051, 26.565051), + ('arcball', 30, 0, 1): (-25.658906, -16.102114, 33.690068), + ('arcball', 30, 0.5, c): (-14.477512, -26.565051, 33.434949), + + ('Shoemake', 0, 1, 0): (0, -60, 0), + ('Shoemake', 0, 0, 1): (-60, 0, 0), + ('Shoemake', 0, 0.5, c): (-48.590378, -40.893395, 19.106605), + ('Shoemake', 0, 2, 0): (0, 180, 0), + ('Shoemake', 30, 1, 0): (25.658906, -56.309932, 16.102114), + ('Shoemake', 30, 0, 1): (-48.590378, -40.893395, 49.106605), + ('Shoemake', 30, 0.5, c): (-25.658906, -56.309932, 43.897886), + + ('Holroyd', 0, 1, 0): (0, -60, 0), + ('Holroyd', 0, 0, 1): (-60, 0, 0), + ('Holroyd', 0, 0.5, c): (-48.590378, -40.893395, 19.106605), + ('Holroyd', 0, 2, 0): (0, -126.869898, 0), + ('Holroyd', 30, 1, 0): (25.658906, -56.309932, 16.102114), + ('Holroyd', 30, 0, 1): (-48.590378, -40.893395, 49.106605), + ('Holroyd', 30, 0.5, c): (-25.658906, -56.309932, 43.897886)} + new_elev, new_azim, new_roll = expectations[(style, roll, dx, dy)] + np.testing.assert_allclose((ax.elev, ax.azim, ax.roll), + (new_elev, new_azim, new_roll), atol=1e-6) def test_pan(): From b845b6d2374a55954aa90fe049aff9d758234d50 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 19 Sep 2024 17:20:05 +0200 Subject: [PATCH 0274/1230] In examples, prefer named locations rather than location numbers. --- .../examples/axes_grid1/inset_locator_demo.py | 28 +++++++++---------- .../axes_grid1/inset_locator_demo2.py | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/galleries/examples/axes_grid1/inset_locator_demo.py b/galleries/examples/axes_grid1/inset_locator_demo.py index fa9c4593d932..e4b310ac6c73 100644 --- a/galleries/examples/axes_grid1/inset_locator_demo.py +++ b/galleries/examples/axes_grid1/inset_locator_demo.py @@ -20,21 +20,21 @@ fig, (ax, ax2) = plt.subplots(1, 2, figsize=[5.5, 2.8]) # Create inset of width 1.3 inches and height 0.9 inches -# at the default upper right location +# at the default upper right location. axins = inset_axes(ax, width=1.3, height=0.9) # Create inset of width 30% and height 40% of the parent Axes' bounding box -# at the lower left corner (loc=3) -axins2 = inset_axes(ax, width="30%", height="40%", loc=3) +# at the lower left corner. +axins2 = inset_axes(ax, width="30%", height="40%", loc="lower left") # Create inset of mixed specifications in the second subplot; # width is 30% of parent Axes' bounding box and -# height is 1 inch at the upper left corner (loc=2) -axins3 = inset_axes(ax2, width="30%", height=1., loc=2) +# height is 1 inch at the upper left corner. +axins3 = inset_axes(ax2, width="30%", height=1., loc="upper left") -# Create an inset in the lower right corner (loc=4) with borderpad=1, i.e. -# 10 points padding (as 10pt is the default fontsize) to the parent Axes -axins4 = inset_axes(ax2, width="20%", height="20%", loc=4, borderpad=1) +# Create an inset in the lower right corner with borderpad=1, i.e. +# 10 points padding (as 10pt is the default fontsize) to the parent Axes. +axins4 = inset_axes(ax2, width="20%", height="20%", loc="lower right", borderpad=1) # Turn ticklabels of insets off for axi in [axins, axins2, axins3, axins4]: @@ -61,12 +61,12 @@ # in those coordinates. # Inside this bounding box an inset of half the bounding box' width and # three quarters of the bounding box' height is created. The lower left corner -# of the inset is aligned to the lower left corner of the bounding box (loc=3). +# of the inset is aligned to the lower left corner of the bounding box. # The inset is then offset by the default 0.5 in units of the font size. axins = inset_axes(ax, width="50%", height="75%", bbox_to_anchor=(.2, .4, .6, .5), - bbox_transform=ax.transAxes, loc=3) + bbox_transform=ax.transAxes, loc="lower left") # For visualization purposes we mark the bounding box by a rectangle ax.add_patch(plt.Rectangle((.2, .4), .6, .5, ls="--", ec="c", fc="none", @@ -113,7 +113,7 @@ # Create an inset outside the Axes axins = inset_axes(ax, width="100%", height="100%", bbox_to_anchor=(1.05, .6, .5, .4), - bbox_transform=ax.transAxes, loc=2, borderpad=0) + bbox_transform=ax.transAxes, loc="upper left", borderpad=0) axins.tick_params(left=False, right=True, labelleft=False, labelright=True) # Create an inset with a 2-tuple bounding box. Note that this creates a @@ -121,7 +121,7 @@ # width and height in absolute units (inches). axins2 = inset_axes(ax, width=0.5, height=0.4, bbox_to_anchor=(0.33, 0.25), - bbox_transform=ax.transAxes, loc=3, borderpad=0) + bbox_transform=ax.transAxes, loc="lower left", borderpad=0) ax2 = fig.add_subplot(133) @@ -131,7 +131,7 @@ # Create inset in data coordinates using ax.transData as transform axins3 = inset_axes(ax2, width="100%", height="100%", bbox_to_anchor=(1e-2, 2, 1e3, 3), - bbox_transform=ax2.transData, loc=2, borderpad=0) + bbox_transform=ax2.transData, loc="upper left", borderpad=0) # Create an inset horizontally centered in figure coordinates and vertically # bound to line up with the Axes. @@ -140,6 +140,6 @@ transform = blended_transform_factory(fig.transFigure, ax2.transAxes) axins4 = inset_axes(ax2, width="16%", height="34%", bbox_to_anchor=(0, 0, 1, 1), - bbox_transform=transform, loc=8, borderpad=0) + bbox_transform=transform, loc="lower center", borderpad=0) plt.show() diff --git a/galleries/examples/axes_grid1/inset_locator_demo2.py b/galleries/examples/axes_grid1/inset_locator_demo2.py index f648c38e8d55..1bbbdd39b886 100644 --- a/galleries/examples/axes_grid1/inset_locator_demo2.py +++ b/galleries/examples/axes_grid1/inset_locator_demo2.py @@ -36,7 +36,7 @@ def add_sizebar(ax, size): asb = AnchoredSizeBar(ax.transData, size, str(size), - loc=8, + loc="lower center", pad=0.1, borderpad=0.5, sep=5, frameon=False) ax.add_artist(asb) @@ -54,7 +54,7 @@ def add_sizebar(ax, size): ax2.imshow(Z2, extent=extent, origin="lower") -axins2 = zoomed_inset_axes(ax2, zoom=6, loc=1) +axins2 = zoomed_inset_axes(ax2, zoom=6, loc="upper right") axins2.imshow(Z2, extent=extent, origin="lower") # subregion of the original image From 96751227d9e3f8f8440b425d0916e593c17667c3 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 19 Sep 2024 21:57:19 +0200 Subject: [PATCH 0275/1230] Support unhashable callbacks in CallbackRegistry (#26013) * Record the connected signal in CallbackRegistry weakref cleanup function. ... to remove the need to loop over all signals in _remove_proxy. * Flatten CallbackRegistry._func_cid_map. It is easier to manipulate a flat (signal, proxy) -> cid map rather than a nested signal -> (proxy -> cid) map. * Support unhashable callbacks in CallbackRegistry. ... by replacing _func_cid_map by a dict-like structure (_UnhashDict) that also supports unhashable entries. Note that _func_cid_map (and thus _UnhashDict) can be dropped if we get rid of proxy deduplication in CallbackRegistry. --- lib/matplotlib/cbook.py | 124 +++++++++++++++++++---------- lib/matplotlib/tests/test_cbook.py | 44 ++++++---- 2 files changed, 109 insertions(+), 59 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 2411784af3ec..cff8f02fd349 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -117,6 +117,61 @@ def _weak_or_strong_ref(func, callback): return _StrongRef(func) +class _UnhashDict: + """ + A minimal dict-like class that also supports unhashable keys, storing them + in a list of key-value pairs. + + This class only implements the interface needed for `CallbackRegistry`, and + tries to minimize the overhead for the hashable case. + """ + + def __init__(self, pairs): + self._dict = {} + self._pairs = [] + for k, v in pairs: + self[k] = v + + def __setitem__(self, key, value): + try: + self._dict[key] = value + except TypeError: + for i, (k, v) in enumerate(self._pairs): + if k == key: + self._pairs[i] = (key, value) + break + else: + self._pairs.append((key, value)) + + def __getitem__(self, key): + try: + return self._dict[key] + except TypeError: + pass + for k, v in self._pairs: + if k == key: + return v + raise KeyError(key) + + def pop(self, key, *args): + try: + if key in self._dict: + return self._dict.pop(key) + except TypeError: + for i, (k, v) in enumerate(self._pairs): + if k == key: + del self._pairs[i] + return v + if args: + return args[0] + raise KeyError(key) + + def __iter__(self): + yield from self._dict + for k, v in self._pairs: + yield k + + class CallbackRegistry: """ Handle registering, processing, blocking, and disconnecting @@ -176,14 +231,14 @@ class CallbackRegistry: # We maintain two mappings: # callbacks: signal -> {cid -> weakref-to-callback} - # _func_cid_map: signal -> {weakref-to-callback -> cid} + # _func_cid_map: {(signal, weakref-to-callback) -> cid} def __init__(self, exception_handler=_exception_printer, *, signals=None): self._signals = None if signals is None else list(signals) # Copy it. self.exception_handler = exception_handler self.callbacks = {} self._cid_gen = itertools.count() - self._func_cid_map = {} + self._func_cid_map = _UnhashDict([]) # A hidden variable that marks cids that need to be pickled. self._pickled_cids = set() @@ -204,27 +259,25 @@ def __setstate__(self, state): cid_count = state.pop('_cid_gen') vars(self).update(state) self.callbacks = { - s: {cid: _weak_or_strong_ref(func, self._remove_proxy) + s: {cid: _weak_or_strong_ref(func, functools.partial(self._remove_proxy, s)) for cid, func in d.items()} for s, d in self.callbacks.items()} - self._func_cid_map = { - s: {proxy: cid for cid, proxy in d.items()} - for s, d in self.callbacks.items()} + self._func_cid_map = _UnhashDict( + ((s, proxy), cid) + for s, d in self.callbacks.items() for cid, proxy in d.items()) self._cid_gen = itertools.count(cid_count) def connect(self, signal, func): """Register *func* to be called when signal *signal* is generated.""" if self._signals is not None: _api.check_in_list(self._signals, signal=signal) - self._func_cid_map.setdefault(signal, {}) - proxy = _weak_or_strong_ref(func, self._remove_proxy) - if proxy in self._func_cid_map[signal]: - return self._func_cid_map[signal][proxy] - cid = next(self._cid_gen) - self._func_cid_map[signal][proxy] = cid - self.callbacks.setdefault(signal, {}) - self.callbacks[signal][cid] = proxy - return cid + proxy = _weak_or_strong_ref(func, functools.partial(self._remove_proxy, signal)) + try: + return self._func_cid_map[signal, proxy] + except KeyError: + cid = self._func_cid_map[signal, proxy] = next(self._cid_gen) + self.callbacks.setdefault(signal, {})[cid] = proxy + return cid def _connect_picklable(self, signal, func): """ @@ -238,23 +291,18 @@ def _connect_picklable(self, signal, func): # Keep a reference to sys.is_finalizing, as sys may have been cleared out # at that point. - def _remove_proxy(self, proxy, *, _is_finalizing=sys.is_finalizing): + def _remove_proxy(self, signal, proxy, *, _is_finalizing=sys.is_finalizing): if _is_finalizing(): # Weakrefs can't be properly torn down at that point anymore. return - for signal, proxy_to_cid in list(self._func_cid_map.items()): - cid = proxy_to_cid.pop(proxy, None) - if cid is not None: - del self.callbacks[signal][cid] - self._pickled_cids.discard(cid) - break - else: - # Not found + cid = self._func_cid_map.pop((signal, proxy), None) + if cid is not None: + del self.callbacks[signal][cid] + self._pickled_cids.discard(cid) + else: # Not found return - # Clean up empty dicts - if len(self.callbacks[signal]) == 0: + if len(self.callbacks[signal]) == 0: # Clean up empty dicts del self.callbacks[signal] - del self._func_cid_map[signal] def disconnect(self, cid): """ @@ -263,24 +311,16 @@ def disconnect(self, cid): No error is raised if such a callback does not exist. """ self._pickled_cids.discard(cid) - # Clean up callbacks - for signal, cid_to_proxy in list(self.callbacks.items()): - proxy = cid_to_proxy.pop(cid, None) - if proxy is not None: + for signal, proxy in self._func_cid_map: + if self._func_cid_map[signal, proxy] == cid: break - else: - # Not found + else: # Not found return - - proxy_to_cid = self._func_cid_map[signal] - for current_proxy, current_cid in list(proxy_to_cid.items()): - if current_cid == cid: - assert proxy is current_proxy - del proxy_to_cid[current_proxy] - # Clean up empty dicts - if len(self.callbacks[signal]) == 0: + assert self.callbacks[signal][cid] == proxy + del self.callbacks[signal][cid] + self._func_cid_map.pop((signal, proxy)) + if len(self.callbacks[signal]) == 0: # Clean up empty dicts del self.callbacks[signal] - del self._func_cid_map[signal] def process(self, s, *args, **kwargs): """ diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 222cc23b7e4d..435745e03e16 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -181,6 +181,15 @@ def test_boxplot_stats_autorange_false(self): assert_array_almost_equal(bstats_true[0]['fliers'], []) +class Hashable: + def dummy(self): pass + + +class Unhashable: + __hash__ = None # type: ignore + def dummy(self): pass + + class Test_callback_registry: def setup_method(self): self.signal = 'test' @@ -196,20 +205,20 @@ def disconnect(self, cid): return self.callbacks.disconnect(cid) def count(self): - count1 = len(self.callbacks._func_cid_map.get(self.signal, [])) + count1 = sum(s == self.signal for s, p in self.callbacks._func_cid_map) count2 = len(self.callbacks.callbacks.get(self.signal)) assert count1 == count2 return count1 def is_empty(self): np.testing.break_cycles() - assert self.callbacks._func_cid_map == {} + assert [*self.callbacks._func_cid_map] == [] assert self.callbacks.callbacks == {} assert self.callbacks._pickled_cids == set() def is_not_empty(self): np.testing.break_cycles() - assert self.callbacks._func_cid_map != {} + assert [*self.callbacks._func_cid_map] != [] assert self.callbacks.callbacks != {} def test_cid_restore(self): @@ -220,12 +229,13 @@ def test_cid_restore(self): assert cid == 1 @pytest.mark.parametrize('pickle', [True, False]) - def test_callback_complete(self, pickle): + @pytest.mark.parametrize('cls', [Hashable, Unhashable]) + def test_callback_complete(self, pickle, cls): # ensure we start with an empty registry self.is_empty() # create a class for testing - mini_me = Test_callback_registry() + mini_me = cls() # test that we can add a callback cid1 = self.connect(self.signal, mini_me.dummy, pickle) @@ -236,7 +246,7 @@ def test_callback_complete(self, pickle): cid2 = self.connect(self.signal, mini_me.dummy, pickle) assert cid1 == cid2 self.is_not_empty() - assert len(self.callbacks._func_cid_map) == 1 + assert len([*self.callbacks._func_cid_map]) == 1 assert len(self.callbacks.callbacks) == 1 del mini_me @@ -245,12 +255,13 @@ def test_callback_complete(self, pickle): self.is_empty() @pytest.mark.parametrize('pickle', [True, False]) - def test_callback_disconnect(self, pickle): + @pytest.mark.parametrize('cls', [Hashable, Unhashable]) + def test_callback_disconnect(self, pickle, cls): # ensure we start with an empty registry self.is_empty() # create a class for testing - mini_me = Test_callback_registry() + mini_me = cls() # test that we can add a callback cid1 = self.connect(self.signal, mini_me.dummy, pickle) @@ -263,12 +274,13 @@ def test_callback_disconnect(self, pickle): self.is_empty() @pytest.mark.parametrize('pickle', [True, False]) - def test_callback_wrong_disconnect(self, pickle): + @pytest.mark.parametrize('cls', [Hashable, Unhashable]) + def test_callback_wrong_disconnect(self, pickle, cls): # ensure we start with an empty registry self.is_empty() # create a class for testing - mini_me = Test_callback_registry() + mini_me = cls() # test that we can add a callback cid1 = self.connect(self.signal, mini_me.dummy, pickle) @@ -281,20 +293,21 @@ def test_callback_wrong_disconnect(self, pickle): self.is_not_empty() @pytest.mark.parametrize('pickle', [True, False]) - def test_registration_on_non_empty_registry(self, pickle): + @pytest.mark.parametrize('cls', [Hashable, Unhashable]) + def test_registration_on_non_empty_registry(self, pickle, cls): # ensure we start with an empty registry self.is_empty() # setup the registry with a callback - mini_me = Test_callback_registry() + mini_me = cls() self.connect(self.signal, mini_me.dummy, pickle) # Add another callback - mini_me2 = Test_callback_registry() + mini_me2 = cls() self.connect(self.signal, mini_me2.dummy, pickle) # Remove and add the second callback - mini_me2 = Test_callback_registry() + mini_me2 = cls() self.connect(self.signal, mini_me2.dummy, pickle) # We still have 2 references @@ -306,9 +319,6 @@ def test_registration_on_non_empty_registry(self, pickle): mini_me2 = None self.is_empty() - def dummy(self): - pass - def test_pickling(self): assert hasattr(pickle.loads(pickle.dumps(cbook.CallbackRegistry())), "callbacks") From 04c8ca2bb97863efb0cd7bd5c0d304eceea0c28f Mon Sep 17 00:00:00 2001 From: thiagoluisbecker Date: Mon, 29 May 2023 16:26:37 -0300 Subject: [PATCH 0276/1230] Make onselect argument to selector widget optional --- doc/api/next_api_changes/behavior/26000-t.rst | 5 ++ lib/matplotlib/tests/test_widgets.py | 65 +++++++++---------- lib/matplotlib/widgets.py | 21 +++--- lib/matplotlib/widgets.pyi | 8 +-- 4 files changed, 51 insertions(+), 48 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/26000-t.rst diff --git a/doc/api/next_api_changes/behavior/26000-t.rst b/doc/api/next_api_changes/behavior/26000-t.rst new file mode 100644 index 000000000000..054feb0887e6 --- /dev/null +++ b/doc/api/next_api_changes/behavior/26000-t.rst @@ -0,0 +1,5 @@ +onselect argument to selector widgets made optional +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *onselect* argument to `.EllipseSelector`, `.LassoSelector`, `.PolygonSelector`, and +`.RectangleSelector` is no longer required. diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 2ac8716a2b4d..58238cd08af2 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -71,7 +71,7 @@ def test_save_blitted_widget_as_pdf(): def test_rectangle_selector(ax, kwargs): onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.RectangleSelector(ax, onselect, **kwargs) + tool = widgets.RectangleSelector(ax, onselect=onselect, **kwargs) do_event(tool, 'press', xdata=100, ydata=100, button=1) do_event(tool, 'onmove', xdata=199, ydata=199, button=1) @@ -105,7 +105,7 @@ def test_rectangle_minspan(ax, spancoords, minspanx, x1, minspany, y1): minspanx, minspany = (ax.transData.transform((x1, y1)) - ax.transData.transform((x0, y0))) - tool = widgets.RectangleSelector(ax, onselect, interactive=True, + tool = widgets.RectangleSelector(ax, onselect=onselect, interactive=True, spancoords=spancoords, minspanx=minspanx, minspany=minspany) # Too small to create a selector @@ -132,7 +132,7 @@ def test_rectangle_minspan(ax, spancoords, minspanx, x1, minspany, y1): def test_deprecation_selector_visible_attribute(ax): - tool = widgets.RectangleSelector(ax, lambda *args: None) + tool = widgets.RectangleSelector(ax) assert tool.get_visible() @@ -145,7 +145,7 @@ def test_deprecation_selector_visible_attribute(ax): [[True, (60, 75)], [False, (30, 20)]]) def test_rectangle_drag(ax, drag_from_anywhere, new_center): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True, + tool = widgets.RectangleSelector(ax, interactive=True, drag_from_anywhere=drag_from_anywhere) # Create rectangle click_and_drag(tool, start=(0, 10), end=(100, 120)) @@ -166,7 +166,7 @@ def test_rectangle_drag(ax, drag_from_anywhere, new_center): def test_rectangle_selector_set_props_handle_props(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True, + tool = widgets.RectangleSelector(ax, interactive=True, props=dict(facecolor='b', alpha=0.2), handle_props=dict(alpha=0.5)) # Create rectangle @@ -187,7 +187,7 @@ def test_rectangle_selector_set_props_handle_props(ax): def test_rectangle_resize(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(0, 10), end=(100, 120)) assert tool.extents == (0.0, 100.0, 10.0, 120.0) @@ -222,7 +222,7 @@ def test_rectangle_resize(ax): def test_rectangle_add_state(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(70, 65), end=(125, 130)) @@ -238,7 +238,7 @@ def test_rectangle_add_state(ax): @pytest.mark.parametrize('add_state', [True, False]) def test_rectangle_resize_center(ax, add_state): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(70, 65), end=(125, 130)) assert tool.extents == (70.0, 125.0, 65.0, 130.0) @@ -312,7 +312,7 @@ def test_rectangle_resize_center(ax, add_state): @pytest.mark.parametrize('add_state', [True, False]) def test_rectangle_resize_square(ax, add_state): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(70, 65), end=(120, 115)) assert tool.extents == (70.0, 120.0, 65.0, 115.0) @@ -385,7 +385,7 @@ def test_rectangle_resize_square(ax, add_state): def test_rectangle_resize_square_center(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(70, 65), end=(120, 115)) tool.add_state('square') @@ -450,7 +450,7 @@ def test_rectangle_resize_square_center(ax): @pytest.mark.parametrize('selector_class', [widgets.RectangleSelector, widgets.EllipseSelector]) def test_rectangle_rotate(ax, selector_class): - tool = selector_class(ax, onselect=noop, interactive=True) + tool = selector_class(ax, interactive=True) # Draw rectangle click_and_drag(tool, start=(100, 100), end=(130, 140)) assert tool.extents == (100, 130, 100, 140) @@ -483,7 +483,7 @@ def test_rectangle_rotate(ax, selector_class): def test_rectangle_add_remove_set(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Draw rectangle click_and_drag(tool, start=(100, 100), end=(130, 140)) assert tool.extents == (100, 130, 100, 140) @@ -499,7 +499,7 @@ def test_rectangle_add_remove_set(ax): def test_rectangle_resize_square_center_aspect(ax, use_data_coordinates): ax.set_aspect(0.8) - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True, + tool = widgets.RectangleSelector(ax, interactive=True, use_data_coordinates=use_data_coordinates) # Create rectangle click_and_drag(tool, start=(70, 65), end=(120, 115)) @@ -531,8 +531,7 @@ def test_rectangle_resize_square_center_aspect(ax, use_data_coordinates): def test_ellipse(ax): """For ellipse, test out the key modifiers""" - tool = widgets.EllipseSelector(ax, onselect=noop, - grab_range=10, interactive=True) + tool = widgets.EllipseSelector(ax, grab_range=10, interactive=True) tool.extents = (100, 150, 100, 150) # drag the rectangle @@ -558,9 +557,7 @@ def test_ellipse(ax): def test_rectangle_handles(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, - grab_range=10, - interactive=True, + tool = widgets.RectangleSelector(ax, grab_range=10, interactive=True, handle_props={'markerfacecolor': 'r', 'markeredgecolor': 'b'}) tool.extents = (100, 150, 100, 150) @@ -595,7 +592,7 @@ def test_rectangle_selector_onselect(ax, interactive): # check when press and release events take place at the same position onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.RectangleSelector(ax, onselect, interactive=interactive) + tool = widgets.RectangleSelector(ax, onselect=onselect, interactive=interactive) # move outside of axis click_and_drag(tool, start=(100, 110), end=(150, 120)) @@ -611,7 +608,7 @@ def test_rectangle_selector_onselect(ax, interactive): def test_rectangle_selector_ignore_outside(ax, ignore_event_outside): onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.RectangleSelector(ax, onselect, + tool = widgets.RectangleSelector(ax, onselect=onselect, ignore_event_outside=ignore_event_outside) click_and_drag(tool, start=(100, 110), end=(150, 120)) onselect.assert_called_once() @@ -773,10 +770,11 @@ def test_span_selector_set_props_handle_props(ax): @pytest.mark.parametrize('selector', ['span', 'rectangle']) def test_selector_clear(ax, selector): - kwargs = dict(ax=ax, onselect=noop, interactive=True) + kwargs = dict(ax=ax, interactive=True) if selector == 'span': Selector = widgets.SpanSelector kwargs['direction'] = 'horizontal' + kwargs['onselect'] = noop else: Selector = widgets.RectangleSelector @@ -807,7 +805,7 @@ def test_selector_clear_method(ax, selector): interactive=True, ignore_event_outside=True) else: - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) click_and_drag(tool, start=(10, 10), end=(100, 120)) assert tool._selection_completed assert tool.get_visible() @@ -1000,7 +998,7 @@ def test_span_selector_extents(ax): def test_lasso_selector(ax, kwargs): onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.LassoSelector(ax, onselect, **kwargs) + tool = widgets.LassoSelector(ax, onselect=onselect, **kwargs) do_event(tool, 'press', xdata=100, ydata=100, button=1) do_event(tool, 'onmove', xdata=125, ydata=125, button=1) do_event(tool, 'release', xdata=150, ydata=150, button=1) @@ -1011,7 +1009,8 @@ def test_lasso_selector(ax, kwargs): def test_lasso_selector_set_props(ax): onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.LassoSelector(ax, onselect, props=dict(color='b', alpha=0.2)) + tool = widgets.LassoSelector(ax, onselect=onselect, + props=dict(color='b', alpha=0.2)) artist = tool._selection_artist assert mcolors.same_color(artist.get_color(), 'b') @@ -1380,7 +1379,7 @@ def check_polygon_selector(event_sequence, expected_result, selections_count, onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.PolygonSelector(ax, onselect, **kwargs) + tool = widgets.PolygonSelector(ax, onselect=onselect, **kwargs) for (etype, event_args) in event_sequence: do_event(tool, etype, **event_args) @@ -1517,7 +1516,7 @@ def test_polygon_selector(draw_bounding_box): @pytest.mark.parametrize('draw_bounding_box', [False, True]) def test_polygon_selector_set_props_handle_props(ax, draw_bounding_box): - tool = widgets.PolygonSelector(ax, onselect=noop, + tool = widgets.PolygonSelector(ax, props=dict(color='b', alpha=0.2), handle_props=dict(alpha=0.5), draw_bounding_box=draw_bounding_box) @@ -1554,8 +1553,7 @@ def test_rect_visibility(fig_test, fig_ref): ax_test = fig_test.subplots() _ = fig_ref.subplots() - tool = widgets.RectangleSelector(ax_test, onselect=noop, - props={'visible': False}) + tool = widgets.RectangleSelector(ax_test, props={'visible': False}) tool.extents = (0.2, 0.8, 0.3, 0.7) @@ -1608,8 +1606,7 @@ def test_polygon_selector_redraw(ax, draw_bounding_box): *polygon_place_vertex(*verts[1]), ] - tool = widgets.PolygonSelector(ax, onselect=noop, - draw_bounding_box=draw_bounding_box) + tool = widgets.PolygonSelector(ax, draw_bounding_box=draw_bounding_box) for (etype, event_args) in event_sequence: do_event(tool, etype, **event_args) # After removing two verts, only one remains, and the @@ -1623,14 +1620,12 @@ def test_polygon_selector_verts_setter(fig_test, fig_ref, draw_bounding_box): verts = [(0.1, 0.4), (0.5, 0.9), (0.3, 0.2)] ax_test = fig_test.add_subplot() - tool_test = widgets.PolygonSelector( - ax_test, onselect=noop, draw_bounding_box=draw_bounding_box) + tool_test = widgets.PolygonSelector(ax_test, draw_bounding_box=draw_bounding_box) tool_test.verts = verts assert tool_test.verts == verts ax_ref = fig_ref.add_subplot() - tool_ref = widgets.PolygonSelector( - ax_ref, onselect=noop, draw_bounding_box=draw_bounding_box) + tool_ref = widgets.PolygonSelector(ax_ref, draw_bounding_box=draw_bounding_box) event_sequence = [ *polygon_place_vertex(*verts[0]), *polygon_place_vertex(*verts[1]), @@ -1654,7 +1649,7 @@ def test_polygon_selector_box(ax): ] # Create selector - tool = widgets.PolygonSelector(ax, onselect=noop, draw_bounding_box=True) + tool = widgets.PolygonSelector(ax, draw_bounding_box=True) for (etype, event_args) in event_sequence: do_event(tool, etype, **event_args) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 4245ce665b00..0e8eaa68b6b4 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2091,12 +2091,15 @@ def onmove(self, event): class _SelectorWidget(AxesWidget): - def __init__(self, ax, onselect, useblit=False, button=None, + def __init__(self, ax, onselect=None, useblit=False, button=None, state_modifier_keys=None, use_data_coordinates=False): super().__init__(ax) self._visible = True - self.onselect = onselect + if onselect is None: + self.onselect = lambda *args: None + else: + self.onselect = onselect self.useblit = useblit and self.canvas.supports_blit self.connect_default_events() @@ -3041,7 +3044,7 @@ def closest(self, x, y): ax : `~matplotlib.axes.Axes` The parent Axes for the widget. - onselect : function + onselect : function, optional A callback function that is called after a release event and the selection is created, changed or removed. It must have the signature:: @@ -3154,7 +3157,8 @@ class RectangleSelector(_SelectorWidget): See also: :doc:`/gallery/widgets/rectangle_selector` """ - def __init__(self, ax, onselect, *, minspanx=0, minspany=0, useblit=False, + def __init__(self, ax, onselect=None, *, minspanx=0, + minspany=0, useblit=False, props=None, spancoords='data', button=None, grab_range=10, handle_props=None, interactive=False, state_modifier_keys=None, drag_from_anywhere=False, @@ -3676,7 +3680,7 @@ def onselect(verts): ---------- ax : `~matplotlib.axes.Axes` The parent Axes for the widget. - onselect : function + onselect : function, optional Whenever the lasso is released, the *onselect* function is called and passed the vertices of the selected path. useblit : bool, default: True @@ -3691,7 +3695,7 @@ def onselect(verts): which corresponds to all buttons. """ - def __init__(self, ax, onselect, *, useblit=True, props=None, button=None): + def __init__(self, ax, onselect=None, *, useblit=True, props=None, button=None): super().__init__(ax, onselect, useblit=useblit, button=button) self.verts = None props = { @@ -3749,7 +3753,7 @@ class PolygonSelector(_SelectorWidget): ax : `~matplotlib.axes.Axes` The parent Axes for the widget. - onselect : function + onselect : function, optional When a polygon is completed or modified after completion, the *onselect* function is called and passed a list of the vertices as ``(xdata, ydata)`` tuples. @@ -3801,7 +3805,7 @@ class PolygonSelector(_SelectorWidget): point. """ - def __init__(self, ax, onselect, *, useblit=False, + def __init__(self, ax, onselect=None, *, useblit=False, props=None, handle_props=None, grab_range=10, draw_bounding_box=False, box_handle_props=None, box_props=None): @@ -3851,7 +3855,6 @@ def _get_bbox(self): def _add_box(self): self._box = RectangleSelector(self.ax, - onselect=lambda *args, **kwargs: None, useblit=self.useblit, grab_range=self.grab_range, handle_props=self._box_handle_props, diff --git a/lib/matplotlib/widgets.pyi b/lib/matplotlib/widgets.pyi index f5de6cb62414..96bc0c431ac3 100644 --- a/lib/matplotlib/widgets.pyi +++ b/lib/matplotlib/widgets.pyi @@ -276,7 +276,7 @@ class _SelectorWidget(AxesWidget): def __init__( self, ax: Axes, - onselect: Callable[[float, float], Any], + onselect: Callable[[float, float], Any] | None = ..., useblit: bool = ..., button: MouseButton | Collection[MouseButton] | None = ..., state_modifier_keys: dict[str, str] | None = ..., @@ -403,7 +403,7 @@ class RectangleSelector(_SelectorWidget): def __init__( self, ax: Axes, - onselect: Callable[[MouseEvent, MouseEvent], Any], + onselect: Callable[[MouseEvent, MouseEvent], Any] | None = ..., *, minspanx: float = ..., minspany: float = ..., @@ -443,7 +443,7 @@ class LassoSelector(_SelectorWidget): def __init__( self, ax: Axes, - onselect: Callable[[list[tuple[float, float]]], Any], + onselect: Callable[[list[tuple[float, float]]], Any] | None = ..., *, useblit: bool = ..., props: dict[str, Any] | None = ..., @@ -455,7 +455,7 @@ class PolygonSelector(_SelectorWidget): def __init__( self, ax: Axes, - onselect: Callable[[ArrayLike, ArrayLike], Any], + onselect: Callable[[ArrayLike, ArrayLike], Any] | None = ..., *, useblit: bool = ..., props: dict[str, Any] | None = ..., From 33fd5b13e68ca766474c4f9aa04477c8b55ec9b9 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 20 Sep 2024 02:52:25 -0400 Subject: [PATCH 0277/1230] DOC: Mark subfigures as no longer provisional Fixes #25947 --- doc/users/next_whats_new/subfigures_change_order.rst | 6 ++++++ .../examples/subplots_axes_and_figures/subfigures.py | 3 --- lib/matplotlib/figure.py | 11 ++--------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/doc/users/next_whats_new/subfigures_change_order.rst b/doc/users/next_whats_new/subfigures_change_order.rst index 49a018a3fd96..e059d71755bc 100644 --- a/doc/users/next_whats_new/subfigures_change_order.rst +++ b/doc/users/next_whats_new/subfigures_change_order.rst @@ -1,3 +1,9 @@ +Subfigures no longer provisional +-------------------------------- + +The API on `.Figure.subfigures` and `.SubFigure` are now considered stable. + + Subfigures are now added in row-major order ------------------------------------------- diff --git a/galleries/examples/subplots_axes_and_figures/subfigures.py b/galleries/examples/subplots_axes_and_figures/subfigures.py index 5060946b59b2..cbe62f57d6b1 100644 --- a/galleries/examples/subplots_axes_and_figures/subfigures.py +++ b/galleries/examples/subplots_axes_and_figures/subfigures.py @@ -13,9 +13,6 @@ `matplotlib.figure.Figure.subfigures` to make an array of subfigures. Note that subfigures can also have their own child subfigures. -.. note:: - The *subfigure* concept is new in v3.4, and the API is still provisional. - """ import matplotlib.pyplot as plt import numpy as np diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 0d5a686de9d8..7664ee6ded86 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -6,9 +6,8 @@ Many methods are implemented in `FigureBase`. `SubFigure` - A logical figure inside a figure, usually added to a figure (or parent - `SubFigure`) with `Figure.add_subfigure` or `Figure.subfigures` methods - (provisional API v3.4). + A logical figure inside a figure, usually added to a figure (or parent `SubFigure`) + with `Figure.add_subfigure` or `Figure.subfigures` methods. Figures are typically created using pyplot methods `~.pyplot.figure`, `~.pyplot.subplots`, and `~.pyplot.subplot_mosaic`. @@ -1608,9 +1607,6 @@ def subfigures(self, nrows=1, ncols=1, squeeze=True, the same as a figure, but cannot print itself. See :doc:`/gallery/subplots_axes_and_figures/subfigures`. - .. note:: - The *subfigure* concept is new in v3.4, and the API is still provisional. - .. versionchanged:: 3.10 subfigures are now added in row-major order. @@ -2229,9 +2225,6 @@ class SubFigure(FigureBase): axsR = sfigs[1].subplots(2, 1) See :doc:`/gallery/subplots_axes_and_figures/subfigures` - - .. note:: - The *subfigure* concept is new in v3.4, and the API is still provisional. """ def __init__(self, parent, subplotspec, *, From 6ffb4804e54f4101dbfba18188d2642fbecbce42 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 21 Sep 2024 02:04:55 -0400 Subject: [PATCH 0278/1230] Fix flaky labelcolor tests For labelcolor={linecolor,markeredgecolor,markerfacecolor}, text will match the specified attribute if consistent, but fall back to black if they differ within a single labeled artist. These tests use 10 random colours out of the ['r', 'g', 'b'] set, so 3 (all red, all green, all blue) out of 3**10 will result in the text _not_ being black. This is rare (0.0051%), but does happen once in a while. Instead, just hard-code some different colours in the test. --- lib/matplotlib/tests/test_legend.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index f083c8374619..62b40ddb2d7a 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -868,8 +868,8 @@ def test_legend_pathcollection_labelcolor_linecolor_iterable(): # test the labelcolor for labelcolor='linecolor' on PathCollection # with iterable colors fig, ax = plt.subplots() - colors = np.random.default_rng().choice(['r', 'g', 'b'], 10) - ax.scatter(np.arange(10), np.arange(10)*1, label='#1', c=colors) + colors = np.array(['r', 'g', 'b', 'c', 'm'] * 2) + ax.scatter(np.arange(10), np.arange(10), label='#1', c=colors) leg = ax.legend(labelcolor='linecolor') text, = leg.get_texts() @@ -915,8 +915,8 @@ def test_legend_pathcollection_labelcolor_markeredgecolor_iterable(): # test the labelcolor for labelcolor='markeredgecolor' on PathCollection # with iterable colors fig, ax = plt.subplots() - colors = np.random.default_rng().choice(['r', 'g', 'b'], 10) - ax.scatter(np.arange(10), np.arange(10)*1, label='#1', edgecolor=colors) + colors = np.array(['r', 'g', 'b', 'c', 'm'] * 2) + ax.scatter(np.arange(10), np.arange(10), label='#1', edgecolor=colors) leg = ax.legend(labelcolor='markeredgecolor') for text, color in zip(leg.get_texts(), ['k']): @@ -970,8 +970,8 @@ def test_legend_pathcollection_labelcolor_markerfacecolor_iterable(): # test the labelcolor for labelcolor='markerfacecolor' on PathCollection # with iterable colors fig, ax = plt.subplots() - colors = np.random.default_rng().choice(['r', 'g', 'b'], 10) - ax.scatter(np.arange(10), np.arange(10)*1, label='#1', facecolor=colors) + colors = np.array(['r', 'g', 'b', 'c', 'm'] * 2) + ax.scatter(np.arange(10), np.arange(10), label='#1', facecolor=colors) leg = ax.legend(labelcolor='markerfacecolor') for text, color in zip(leg.get_texts(), ['k']): From 172624ebcbdccb08b27fb137910a2253871801e1 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 21 Sep 2024 08:31:41 +0100 Subject: [PATCH 0279/1230] Backport PR #28858: Fix flaky labelcolor tests --- lib/matplotlib/tests/test_legend.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 0353f1408b73..3c2af275649f 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -868,8 +868,8 @@ def test_legend_pathcollection_labelcolor_linecolor_iterable(): # test the labelcolor for labelcolor='linecolor' on PathCollection # with iterable colors fig, ax = plt.subplots() - colors = np.random.default_rng().choice(['r', 'g', 'b'], 10) - ax.scatter(np.arange(10), np.arange(10)*1, label='#1', c=colors) + colors = np.array(['r', 'g', 'b', 'c', 'm'] * 2) + ax.scatter(np.arange(10), np.arange(10), label='#1', c=colors) leg = ax.legend(labelcolor='linecolor') text, = leg.get_texts() @@ -915,8 +915,8 @@ def test_legend_pathcollection_labelcolor_markeredgecolor_iterable(): # test the labelcolor for labelcolor='markeredgecolor' on PathCollection # with iterable colors fig, ax = plt.subplots() - colors = np.random.default_rng().choice(['r', 'g', 'b'], 10) - ax.scatter(np.arange(10), np.arange(10)*1, label='#1', edgecolor=colors) + colors = np.array(['r', 'g', 'b', 'c', 'm'] * 2) + ax.scatter(np.arange(10), np.arange(10), label='#1', edgecolor=colors) leg = ax.legend(labelcolor='markeredgecolor') for text, color in zip(leg.get_texts(), ['k']): @@ -970,8 +970,8 @@ def test_legend_pathcollection_labelcolor_markerfacecolor_iterable(): # test the labelcolor for labelcolor='markerfacecolor' on PathCollection # with iterable colors fig, ax = plt.subplots() - colors = np.random.default_rng().choice(['r', 'g', 'b'], 10) - ax.scatter(np.arange(10), np.arange(10)*1, label='#1', facecolor=colors) + colors = np.array(['r', 'g', 'b', 'c', 'm'] * 2) + ax.scatter(np.arange(10), np.arange(10), label='#1', facecolor=colors) leg = ax.legend(labelcolor='markerfacecolor') for text, color in zip(leg.get_texts(), ['k']): From a0d5f89ad10417d2ca0d45446980f13ebb7025c0 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:02:40 +0200 Subject: [PATCH 0280/1230] DOC: Add illustration to Figure.subplots_adjust Closes #23005. --- doc/_embedded_plots/figure_subplots_adjust.py | 28 +++++++++++++++++++ lib/matplotlib/figure.py | 2 ++ 2 files changed, 30 insertions(+) create mode 100644 doc/_embedded_plots/figure_subplots_adjust.py diff --git a/doc/_embedded_plots/figure_subplots_adjust.py b/doc/_embedded_plots/figure_subplots_adjust.py new file mode 100644 index 000000000000..b4b8d7d32a3d --- /dev/null +++ b/doc/_embedded_plots/figure_subplots_adjust.py @@ -0,0 +1,28 @@ +import matplotlib.pyplot as plt + + +def arrow(p1, p2, **props): + axs[0, 0].annotate( + "", p1, p2, xycoords='figure fraction', + arrowprops=dict(arrowstyle="<->", shrinkA=0, shrinkB=0, **props)) + + +fig, axs = plt.subplots(2, 2, figsize=(6.5, 4)) +fig.set_facecolor('lightblue') +fig.subplots_adjust(0.1, 0.1, 0.9, 0.9, 0.4, 0.4) +for ax in axs.flat: + ax.set(xticks=[], yticks=[]) + +arrow((0, 0.75), (0.1, 0.75)) # left +arrow((0.435, 0.75), (0.565, 0.75)) # wspace +arrow((0.9, 0.75), (1, 0.75)) # right +fig.text(0.05, 0.7, "left", ha="center") +fig.text(0.5, 0.7, "wspace", ha="center") +fig.text(0.95, 0.7, "right", ha="center") + +arrow((0.25, 0), (0.25, 0.1)) # bottom +arrow((0.25, 0.435), (0.25, 0.565)) # hspace +arrow((0.25, 0.9), (0.25, 1)) # top +fig.text(0.28, 0.05, "bottom", va="center") +fig.text(0.28, 0.5, "hspace", va="center") +fig.text(0.28, 0.95, "top", va="center") diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 0d5a686de9d8..c82c9582dbaf 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1322,6 +1322,8 @@ def subplots_adjust(self, left=None, bottom=None, right=None, top=None, Unset parameters are left unmodified; initial values are given by :rc:`figure.subplot.[name]`. + .. plot:: _embedded_plots/figure_subplots_adjust.py + Parameters ---------- left : float, optional From ec6014fe97c6f70310b3259727879595e5dc0f61 Mon Sep 17 00:00:00 2001 From: Costa Paraskevopoulos Date: Sun, 22 Sep 2024 10:18:24 +1000 Subject: [PATCH 0281/1230] Improve pie chart error messages Fix typo in error message, add more detail and make formatting consistent --- lib/matplotlib/axes/_axes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index d7b649ae437f..8d38746f3773 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3283,9 +3283,9 @@ def pie(self, x, explode=None, labels=None, colors=None, if explode is None: explode = [0] * len(x) if len(x) != len(labels): - raise ValueError("'label' must be of length 'x'") + raise ValueError(f"'labels' must be of length 'x', not {len(labels)}") if len(x) != len(explode): - raise ValueError("'explode' must be of length 'x'") + raise ValueError(f"'explode' must be of length 'x', not {len(explode)}") if colors is None: get_next_color = self._get_patches_for_fill.get_next_color else: @@ -3298,7 +3298,7 @@ def get_next_color(): _api.check_isinstance(Real, radius=radius, startangle=startangle) if radius <= 0: - raise ValueError(f'radius must be a positive number, not {radius}') + raise ValueError(f"'radius' must be a positive number, not {radius}") # Starting theta1 is the start fraction of the circle theta1 = startangle / 360 From 5c48037b338170ed222275a6485cf0d37fb78ec6 Mon Sep 17 00:00:00 2001 From: Costa Paraskevopoulos Date: Mon, 23 Sep 2024 20:29:06 +1000 Subject: [PATCH 0282/1230] Add unit tests --- lib/matplotlib/tests/test_axes.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 33c81c44abaf..e3877dbad7af 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6149,6 +6149,27 @@ def test_pie_get_negative_values(): ax.pie([5, 5, -3], explode=[0, .1, .2]) +def test_pie_invalid_explode(): + # Test ValueError raised when feeding short explode list to axes.pie + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.pie([1, 2, 3], explode=[0.1, 0.1]) + + +def test_pie_invalid_labels(): + # Test ValueError raised when feeding short labels list to axes.pie + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.pie([1, 2, 3], labels=["One", "Two"]) + + +def test_pie_invalid_radius(): + # Test ValueError raised when feeding negative radius to axes.pie + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.pie([1, 2, 3], radius=-5) + + def test_normalize_kwarg_pie(): fig, ax = plt.subplots() x = [0.3, 0.3, 0.1] From 2755d6f93af1d2626bbb601483bf88c28fdb3938 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:34:17 +0200 Subject: [PATCH 0283/1230] DOC: Fix documentation of hist() kwarg lists Minimal fix for #28873. One can still further improve, but this fixes the release-critical part of #28873. --- lib/matplotlib/axes/_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 61b41443c66d..415a88b28435 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6925,7 +6925,7 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, `~matplotlib.patches.Patch` properties. The following properties additionally accept a sequence of values corresponding to the datasets in *x*: - *edgecolors*, *facecolors*, *lines*, *linestyles*, *hatches*. + *edgecolor*, *facecolor*, *linewidth*, *linestyle*, *hatch*. .. versionadded:: 3.10 Allowing sequences of values in above listed Patch properties. From f4963234b2f6293d22f7a9310c4ca0f6e4e7e15e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 25 Sep 2024 03:23:55 -0400 Subject: [PATCH 0284/1230] Only check X11 when running Tkinter tests Tkinter only supports X11, not Wayland, so if running in an environment with only the latter, the tests should not run. --- lib/matplotlib/_c_internal_utils.pyi | 1 + lib/matplotlib/cbook.py | 2 +- lib/matplotlib/tests/test_backend_tk.py | 4 +-- .../tests/test_backends_interactive.py | 10 +++++--- lib/matplotlib/tests/test_rcparams.py | 2 +- src/_c_internal_utils.cpp | 25 ++++++++++++++++++- 6 files changed, 36 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/_c_internal_utils.pyi b/lib/matplotlib/_c_internal_utils.pyi index 3efc81bc8332..ccc172cde27a 100644 --- a/lib/matplotlib/_c_internal_utils.pyi +++ b/lib/matplotlib/_c_internal_utils.pyi @@ -1,4 +1,5 @@ def display_is_valid() -> bool: ... +def xdisplay_is_valid() -> bool: ... def Win32_GetForegroundWindow() -> int | None: ... def Win32_SetForegroundWindow(hwnd: int) -> None: ... diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index cff8f02fd349..7cf32c4d5f6a 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -72,7 +72,7 @@ def _get_running_interactive_framework(): if frame.f_code in codes: return "tk" frame = frame.f_back - # premetively break reference cycle between locals and the frame + # Preemptively break reference cycle between locals and the frame. del frame macosx = sys.modules.get("matplotlib.backends._macosx") if macosx and macosx.event_loop_is_running(): diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index ee20a94042f7..89782e8a66f3 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -35,8 +35,8 @@ def _isolated_tk_test(success_count, func=None): reason="missing tkinter" ) @pytest.mark.skipif( - sys.platform == "linux" and not _c_internal_utils.display_is_valid(), - reason="$DISPLAY and $WAYLAND_DISPLAY are unset" + sys.platform == "linux" and not _c_internal_utils.xdisplay_is_valid(), + reason="$DISPLAY is unset" ) @pytest.mark.xfail( # https://github.com/actions/setup-python/issues/649 ('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 2c6b61a48438..ca702bc1d99c 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -57,6 +57,8 @@ def wait_for(self, terminator): def _get_available_interactive_backends(): _is_linux_and_display_invalid = (sys.platform == "linux" and not _c_internal_utils.display_is_valid()) + _is_linux_and_xdisplay_invalid = (sys.platform == "linux" and + not _c_internal_utils.xdisplay_is_valid()) envs = [] for deps, env in [ *[([qt_api], @@ -74,10 +76,12 @@ def _get_available_interactive_backends(): ]: reason = None missing = [dep for dep in deps if not importlib.util.find_spec(dep)] - if _is_linux_and_display_invalid: - reason = "$DISPLAY and $WAYLAND_DISPLAY are unset" - elif missing: + if missing: reason = "{} cannot be imported".format(", ".join(missing)) + elif env["MPLBACKEND"] == "tkagg" and _is_linux_and_xdisplay_invalid: + reason = "$DISPLAY is unset" + elif _is_linux_and_display_invalid: + reason = "$DISPLAY and $WAYLAND_DISPLAY are unset" elif env["MPLBACKEND"] == 'macosx' and os.environ.get('TF_BUILD'): reason = "macosx backend fails on Azure" elif env["MPLBACKEND"].startswith('gtk'): diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 4823df0ce250..25ae258ffcbb 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -536,7 +536,7 @@ def test_backend_fallback_headless(tmp_path): @pytest.mark.skipif( - sys.platform == "linux" and not _c_internal_utils.display_is_valid(), + sys.platform == "linux" and not _c_internal_utils.xdisplay_is_valid(), reason="headless") def test_backend_fallback_headful(tmp_path): pytest.importorskip("tkinter") diff --git a/src/_c_internal_utils.cpp b/src/_c_internal_utils.cpp index 74bb97904f89..561cb303639c 100644 --- a/src/_c_internal_utils.cpp +++ b/src/_c_internal_utils.cpp @@ -33,7 +33,7 @@ namespace py = pybind11; using namespace pybind11::literals; static bool -mpl_display_is_valid(void) +mpl_xdisplay_is_valid(void) { #ifdef __linux__ void* libX11; @@ -57,6 +57,19 @@ mpl_display_is_valid(void) return true; } } + return false; +#else + return true; +#endif +} + +static bool +mpl_display_is_valid(void) +{ +#ifdef __linux__ + if (mpl_xdisplay_is_valid()) { + return true; + } void* libwayland_client; if (getenv("WAYLAND_DISPLAY") && (libwayland_client = dlopen("libwayland-client.so.0", RTLD_LAZY))) { @@ -194,6 +207,16 @@ PYBIND11_MODULE(_c_internal_utils, m) succeeds, or $WAYLAND_DISPLAY is set and wl_display_connect(NULL) succeeds. + On other platforms, always returns True.)"""); + m.def( + "xdisplay_is_valid", &mpl_xdisplay_is_valid, + R"""( -- + Check whether the current X11 display is valid. + + On Linux, returns True if either $DISPLAY is set and XOpenDisplay(NULL) + succeeds. Use this function if you need to specifically check for X11 + only (e.g., for Tkinter). + On other platforms, always returns True.)"""); m.def( "Win32_GetCurrentProcessExplicitAppUserModelID", From e43e0bdacb5f29c4bcf8d9cfc7c7c189b359cdde Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 25 Sep 2024 14:52:52 -0400 Subject: [PATCH 0285/1230] Remove 'in' from removal substitution for deprecation messages (#28880) Ever since #27702, the removal will always be a version, and not "two minor/meso releases later". This tends to get messed up by people writing custom `message` arguments, and since there's no reason to add the "to" there instead of in the message any more, move it to the message. Also, fix the autogenerated message to follow what the docstring says (Falsy `removal` leaves out the removal date.) --- lib/matplotlib/_api/deprecation.py | 37 +++++++++++++---------------- lib/matplotlib/_api/deprecation.pyi | 4 ++-- lib/matplotlib/backend_bases.py | 2 +- lib/matplotlib/hatch.py | 2 +- lib/matplotlib/legend.py | 2 +- lib/matplotlib/projections/polar.py | 2 +- lib/matplotlib/tests/test_api.py | 35 +++++++++++++++++++++++++++ 7 files changed, 57 insertions(+), 27 deletions(-) diff --git a/lib/matplotlib/_api/deprecation.py b/lib/matplotlib/_api/deprecation.py index e9722f5d26c4..65a754bbb43d 100644 --- a/lib/matplotlib/_api/deprecation.py +++ b/lib/matplotlib/_api/deprecation.py @@ -26,25 +26,20 @@ def _generate_deprecation_warning( addendum='', *, removal=''): if pending: if removal: - raise ValueError( - "A pending deprecation cannot have a scheduled removal") - else: - if not removal: - macro, meso, *_ = since.split('.') - removal = f'{macro}.{int(meso) + 2}' - removal = f"in {removal}" + raise ValueError("A pending deprecation cannot have a scheduled removal") + elif removal == '': + macro, meso, *_ = since.split('.') + removal = f'{macro}.{int(meso) + 2}' if not message: message = ( - ("The %(name)s %(obj_type)s" if obj_type else "%(name)s") - + (" will be deprecated in a future version" - if pending else - " was deprecated in Matplotlib %(since)s and will be removed %(removal)s" - ) - + "." - + (" Use %(alternative)s instead." if alternative else "") - + (" %(addendum)s" if addendum else "")) - warning_cls = (PendingDeprecationWarning if pending - else MatplotlibDeprecationWarning) + ("The %(name)s %(obj_type)s" if obj_type else "%(name)s") + + (" will be deprecated in a future version" if pending else + (" was deprecated in Matplotlib %(since)s" + + (" and will be removed in %(removal)s" if removal else ""))) + + "." + + (" Use %(alternative)s instead." if alternative else "") + + (" %(addendum)s" if addendum else "")) + warning_cls = PendingDeprecationWarning if pending else MatplotlibDeprecationWarning return warning_cls(message % dict( func=name, name=name, obj_type=obj_type, since=since, removal=removal, alternative=alternative, addendum=addendum)) @@ -295,7 +290,7 @@ def wrapper(*args, **kwargs): warn_deprecated( since, message=f"The {old!r} parameter of {func.__name__}() " f"has been renamed {new!r} since Matplotlib {since}; support " - f"for the old name will be dropped %(removal)s.") + f"for the old name will be dropped in %(removal)s.") kwargs[new] = kwargs.pop(old) return func(*args, **kwargs) @@ -390,12 +385,12 @@ def wrapper(*inner_args, **inner_kwargs): warn_deprecated( since, message=f"Additional positional arguments to " f"{func.__name__}() are deprecated since %(since)s and " - f"support for them will be removed %(removal)s.") + f"support for them will be removed in %(removal)s.") elif is_varkwargs and arguments.get(name): warn_deprecated( since, message=f"Additional keyword arguments to " f"{func.__name__}() are deprecated since %(since)s and " - f"support for them will be removed %(removal)s.") + f"support for them will be removed in %(removal)s.") # We cannot just check `name not in arguments` because the pyplot # wrappers always pass all arguments explicitly. elif any(name in d and d[name] != _deprecated_parameter @@ -453,7 +448,7 @@ def wrapper(*args, **kwargs): warn_deprecated( since, message="Passing the %(name)s %(obj_type)s " "positionally is deprecated since Matplotlib %(since)s; the " - "parameter will become keyword-only %(removal)s.", + "parameter will become keyword-only in %(removal)s.", name=name, obj_type=f"parameter of {func.__name__}()") return func(*args, **kwargs) diff --git a/lib/matplotlib/_api/deprecation.pyi b/lib/matplotlib/_api/deprecation.pyi index d0d04d987410..e050290662d9 100644 --- a/lib/matplotlib/_api/deprecation.pyi +++ b/lib/matplotlib/_api/deprecation.pyi @@ -1,6 +1,6 @@ from collections.abc import Callable import contextlib -from typing import Any, ParamSpec, TypedDict, TypeVar, overload +from typing import Any, Literal, ParamSpec, TypedDict, TypeVar, overload from typing_extensions import ( Unpack, # < Py 3.11 ) @@ -17,7 +17,7 @@ class DeprecationKwargs(TypedDict, total=False): pending: bool obj_type: str addendum: str - removal: str + removal: str | Literal[False] class NamedDeprecationKwargs(DeprecationKwargs, total=False): name: str diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index a8170ce4f6b0..817eb51705fe 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3333,7 +3333,7 @@ def _get_image_filename(self, tool): _api.warn_deprecated( "3.9", message=f"Loading icon {tool.image!r} from the current " "directory or from Matplotlib's image directory. This behavior " - "is deprecated since %(since)s and will be removed %(removal)s; " + "is deprecated since %(since)s and will be removed in %(removal)s; " "Tool.image should be set to a path relative to the Tool's source " "file, or to an absolute path.") return os.path.abspath(fname) diff --git a/lib/matplotlib/hatch.py b/lib/matplotlib/hatch.py index 7a4b283c1dbe..0cbd042e1628 100644 --- a/lib/matplotlib/hatch.py +++ b/lib/matplotlib/hatch.py @@ -192,7 +192,7 @@ def _validate_hatch_pattern(hatch): message=f'hatch must consist of a string of "{valid}" or ' 'None, but found the following invalid values ' f'"{invalids}". Passing invalid values is deprecated ' - 'since %(since)s and will become an error %(removal)s.' + 'since %(since)s and will become an error in %(removal)s.' ) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 0d487a48bde7..270757fc298e 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -1337,7 +1337,7 @@ def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs): _api.warn_deprecated("3.9", message=( "You have mixed positional and keyword arguments, some input may " "be discarded. This is deprecated since %(since)s and will " - "become an error %(removal)s.")) + "become an error in %(removal)s.")) if (hasattr(handles, "__len__") and hasattr(labels, "__len__") and diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 325da95105ab..7fe6045039b1 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -21,7 +21,7 @@ def _apply_theta_transforms_warn(): message=( "Passing `apply_theta_transforms=True` (the default) " "is deprecated since Matplotlib %(since)s. " - "Support for this will be removed in Matplotlib %(removal)s. " + "Support for this will be removed in Matplotlib in %(removal)s. " "To prevent this warning, set `apply_theta_transforms=False`, " "and make sure to shift theta values before being passed to " "this transform." diff --git a/lib/matplotlib/tests/test_api.py b/lib/matplotlib/tests/test_api.py index 23d3ec48f31f..f04604c14cce 100644 --- a/lib/matplotlib/tests/test_api.py +++ b/lib/matplotlib/tests/test_api.py @@ -49,6 +49,41 @@ def f(cls: Self) -> None: a.f +def test_warn_deprecated(): + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10 and will be ' + r'removed in 3\.12\.'): + _api.warn_deprecated('3.10', name='foo') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'The foo class was deprecated in Matplotlib 3\.10 and ' + r'will be removed in 3\.12\.'): + _api.warn_deprecated('3.10', name='foo', obj_type='class') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10 and will be ' + r'removed in 3\.12\. Use bar instead\.'): + _api.warn_deprecated('3.10', name='foo', alternative='bar') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10 and will be ' + r'removed in 3\.12\. More information\.'): + _api.warn_deprecated('3.10', name='foo', addendum='More information.') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10 and will be ' + r'removed in 4\.0\.'): + _api.warn_deprecated('3.10', name='foo', removal='4.0') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10\.'): + _api.warn_deprecated('3.10', name='foo', removal=False) + with pytest.warns(PendingDeprecationWarning, + match=r'foo will be deprecated in a future version'): + _api.warn_deprecated('3.10', name='foo', pending=True) + with pytest.raises(ValueError, match=r'cannot have a scheduled removal'): + _api.warn_deprecated('3.10', name='foo', pending=True, removal='3.12') + with pytest.warns(mpl.MatplotlibDeprecationWarning, match=r'Complete replacement'): + _api.warn_deprecated('3.10', message='Complete replacement', name='foo', + alternative='bar', addendum='More information.', + obj_type='class', removal='4.0') + + def test_deprecate_privatize_attribute() -> None: class C: def __init__(self) -> None: self._attr = 1 From ff3e448f0d7183d04c4e353fcb19948e88e5d31f Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:44:15 +0200 Subject: [PATCH 0286/1230] Backport PR #28883: Only check X11 when running Tkinter tests --- lib/matplotlib/_c_internal_utils.pyi | 1 + lib/matplotlib/cbook.py | 2 +- lib/matplotlib/tests/test_backend_tk.py | 4 +-- .../tests/test_backends_interactive.py | 10 +++++--- lib/matplotlib/tests/test_rcparams.py | 2 +- src/_c_internal_utils.cpp | 25 ++++++++++++++++++- 6 files changed, 36 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/_c_internal_utils.pyi b/lib/matplotlib/_c_internal_utils.pyi index 3efc81bc8332..ccc172cde27a 100644 --- a/lib/matplotlib/_c_internal_utils.pyi +++ b/lib/matplotlib/_c_internal_utils.pyi @@ -1,4 +1,5 @@ def display_is_valid() -> bool: ... +def xdisplay_is_valid() -> bool: ... def Win32_GetForegroundWindow() -> int | None: ... def Win32_SetForegroundWindow(hwnd: int) -> None: ... diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index f5a4199cf9ad..c5b851ff6c9b 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -72,7 +72,7 @@ def _get_running_interactive_framework(): if frame.f_code in codes: return "tk" frame = frame.f_back - # premetively break reference cycle between locals and the frame + # Preemptively break reference cycle between locals and the frame. del frame macosx = sys.modules.get("matplotlib.backends._macosx") if macosx and macosx.event_loop_is_running(): diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index ee20a94042f7..89782e8a66f3 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -35,8 +35,8 @@ def _isolated_tk_test(success_count, func=None): reason="missing tkinter" ) @pytest.mark.skipif( - sys.platform == "linux" and not _c_internal_utils.display_is_valid(), - reason="$DISPLAY and $WAYLAND_DISPLAY are unset" + sys.platform == "linux" and not _c_internal_utils.xdisplay_is_valid(), + reason="$DISPLAY is unset" ) @pytest.mark.xfail( # https://github.com/actions/setup-python/issues/649 ('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 2c6b61a48438..ca702bc1d99c 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -57,6 +57,8 @@ def wait_for(self, terminator): def _get_available_interactive_backends(): _is_linux_and_display_invalid = (sys.platform == "linux" and not _c_internal_utils.display_is_valid()) + _is_linux_and_xdisplay_invalid = (sys.platform == "linux" and + not _c_internal_utils.xdisplay_is_valid()) envs = [] for deps, env in [ *[([qt_api], @@ -74,10 +76,12 @@ def _get_available_interactive_backends(): ]: reason = None missing = [dep for dep in deps if not importlib.util.find_spec(dep)] - if _is_linux_and_display_invalid: - reason = "$DISPLAY and $WAYLAND_DISPLAY are unset" - elif missing: + if missing: reason = "{} cannot be imported".format(", ".join(missing)) + elif env["MPLBACKEND"] == "tkagg" and _is_linux_and_xdisplay_invalid: + reason = "$DISPLAY is unset" + elif _is_linux_and_display_invalid: + reason = "$DISPLAY and $WAYLAND_DISPLAY are unset" elif env["MPLBACKEND"] == 'macosx' and os.environ.get('TF_BUILD'): reason = "macosx backend fails on Azure" elif env["MPLBACKEND"].startswith('gtk'): diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 4823df0ce250..25ae258ffcbb 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -536,7 +536,7 @@ def test_backend_fallback_headless(tmp_path): @pytest.mark.skipif( - sys.platform == "linux" and not _c_internal_utils.display_is_valid(), + sys.platform == "linux" and not _c_internal_utils.xdisplay_is_valid(), reason="headless") def test_backend_fallback_headful(tmp_path): pytest.importorskip("tkinter") diff --git a/src/_c_internal_utils.cpp b/src/_c_internal_utils.cpp index 74bb97904f89..561cb303639c 100644 --- a/src/_c_internal_utils.cpp +++ b/src/_c_internal_utils.cpp @@ -33,7 +33,7 @@ namespace py = pybind11; using namespace pybind11::literals; static bool -mpl_display_is_valid(void) +mpl_xdisplay_is_valid(void) { #ifdef __linux__ void* libX11; @@ -57,6 +57,19 @@ mpl_display_is_valid(void) return true; } } + return false; +#else + return true; +#endif +} + +static bool +mpl_display_is_valid(void) +{ +#ifdef __linux__ + if (mpl_xdisplay_is_valid()) { + return true; + } void* libwayland_client; if (getenv("WAYLAND_DISPLAY") && (libwayland_client = dlopen("libwayland-client.so.0", RTLD_LAZY))) { @@ -194,6 +207,16 @@ PYBIND11_MODULE(_c_internal_utils, m) succeeds, or $WAYLAND_DISPLAY is set and wl_display_connect(NULL) succeeds. + On other platforms, always returns True.)"""); + m.def( + "xdisplay_is_valid", &mpl_xdisplay_is_valid, + R"""( -- + Check whether the current X11 display is valid. + + On Linux, returns True if either $DISPLAY is set and XOpenDisplay(NULL) + succeeds. Use this function if you need to specifically check for X11 + only (e.g., for Tkinter). + On other platforms, always returns True.)"""); m.def( "Win32_GetCurrentProcessExplicitAppUserModelID", From 5c8dc985a4b198d76f7229ada1d5d4aa3591f46c Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:40:24 +0200 Subject: [PATCH 0287/1230] DOC: Cross-link Axes attributes Follow up to #28825. --- lib/matplotlib/axes/_base.py | 6 +++--- lib/matplotlib/image.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index b9b2da1cb9fb..d49e6f10a54c 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2251,8 +2251,8 @@ def add_artist(self, a): Use `add_artist` only for artists for which there is no dedicated "add" method; and if necessary, use a method such as `update_datalim` - to manually update the dataLim if the artist is to be included in - autoscaling. + to manually update the `~.Axes.dataLim` if the artist is to be included + in autoscaling. If no ``transform`` has been specified when creating the artist (e.g. ``artist.get_transform() == None``) then the transform is set to @@ -2365,7 +2365,7 @@ def _add_text(self, txt): def _update_line_limits(self, line): """ - Figures out the data limit of the given line, updating self.dataLim. + Figures out the data limit of the given line, updating `.Axes.dataLim`. """ path = line.get_path() if path.vertices.size == 0: diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 2a7afbbe450c..03e1ed43e43a 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -919,10 +919,10 @@ def set_extent(self, extent, **kwargs): Notes ----- - This updates ``ax.dataLim``, and, if autoscaling, sets ``ax.viewLim`` - to tightly fit the image, regardless of ``dataLim``. Autoscaling - state is not changed, so following this with ``ax.autoscale_view()`` - will redo the autoscaling in accord with ``dataLim``. + This updates `.Axes.dataLim`, and, if autoscaling, sets `.Axes.viewLim` + to tightly fit the image, regardless of `~.Axes.dataLim`. Autoscaling + state is not changed, so a subsequent call to `.Axes.autoscale_view` + will redo the autoscaling in accord with `~.Axes.dataLim`. """ (xmin, xmax), (ymin, ymax) = self.axes._process_unit_info( [("x", [extent[0], extent[1]]), From 582459f41ffa1b44810164f3ff2bc346df1fc51c Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 26 Sep 2024 00:02:04 +0200 Subject: [PATCH 0288/1230] DOC: Better visualization for the default color cycle example The current visualization is quite messy ( https://matplotlib.org/stable/gallery/color/color_cycle_default.html). Let's focus on: - giving a one clear sequence - giving the color names as 'CN' notation and named colors - showing lines and patches (colors appear substantially different in thin lines and filled areas) And don't bother with: - multiple line widths - they are only a slight visual variation (compared to patches) and multiple widths clutter the example - black background: It's enough to show the default color cycle on the default background. --- .../examples/color/color_cycle_default.py | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/galleries/examples/color/color_cycle_default.py b/galleries/examples/color/color_cycle_default.py index 16f6634937c0..af35f6d00f9e 100644 --- a/galleries/examples/color/color_cycle_default.py +++ b/galleries/examples/color/color_cycle_default.py @@ -9,33 +9,28 @@ import matplotlib.pyplot as plt import numpy as np -prop_cycle = plt.rcParams['axes.prop_cycle'] -colors = prop_cycle.by_key()['color'] +from matplotlib.colors import TABLEAU_COLORS, same_color + -lwbase = plt.rcParams['lines.linewidth'] -thin = lwbase / 2 -thick = lwbase * 3 +def f(x, a): + """A nice sigmoid-like parametrized curve, ending approximately at *a*.""" + return 0.85 * a * (1 / (1 + np.exp(-x)) + 0.2) -fig, axs = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True) -for icol in range(2): - if icol == 0: - lwx, lwy = thin, lwbase - else: - lwx, lwy = lwbase, thick - for irow in range(2): - for i, color in enumerate(colors): - axs[irow, icol].axhline(i, color=color, lw=lwx) - axs[irow, icol].axvline(i, color=color, lw=lwy) - axs[1, icol].set_facecolor('k') - axs[1, icol].xaxis.set_ticks(np.arange(0, 10, 2)) - axs[0, icol].set_title(f'line widths (pts): {lwx:g}, {lwy:g}', - fontsize='medium') +fig, ax = plt.subplots() +ax.axis('off') +ax.set_title("Colors in the default property cycle") -for irow in range(2): - axs[irow, 0].yaxis.set_ticks(np.arange(0, 10, 2)) +prop_cycle = plt.rcParams['axes.prop_cycle'] +colors = prop_cycle.by_key()['color'] +x = np.linspace(-4, 4, 200) -fig.suptitle('Colors in the default prop_cycle', fontsize='large') +for i, (color, color_name) in enumerate(zip(colors, TABLEAU_COLORS)): + assert same_color(color, color_name) + pos = 4.5 - i + ax.plot(x, f(x, pos)) + ax.text(4.2, pos, f"'C{i}': '{color_name}'", color=color, va="center") + ax.bar(9, 1, width=1.5, bottom=pos-0.5) plt.show() @@ -46,14 +41,14 @@ # The use of the following functions, methods, classes and modules is shown # in this example: # -# - `matplotlib.axes.Axes.axhline` / `matplotlib.pyplot.axhline` -# - `matplotlib.axes.Axes.axvline` / `matplotlib.pyplot.axvline` -# - `matplotlib.axes.Axes.set_facecolor` -# - `matplotlib.figure.Figure.suptitle` +# - `matplotlib.axes.Axes.axis` +# - `matplotlib.axes.Axes.text` +# - `matplotlib.colors.same_color` +# - `cycler.Cycler` # # .. tags:: # # styling: color -# styling: colormap +# purpose: reference # plot-type: line # level: beginner From 1cd9255f38389c42f13b3ca4f44677f52ff38d48 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Fri, 7 Apr 2023 14:25:02 +0200 Subject: [PATCH 0289/1230] Fix issue with sketch not working on PathCollection in Agg --- src/_backend_agg.h | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/_backend_agg.h b/src/_backend_agg.h index 6325df357b1b..5549978cfb80 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -919,6 +919,10 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, typedef PathSnapper snapped_t; typedef agg::conv_curve snapped_curve_t; typedef agg::conv_curve curve_t; + typedef Sketch sketch_clipped_t; + typedef Sketch sketch_curve_t; + typedef Sketch sketch_snapped_t; + typedef Sketch sketch_snapped_curve_t; size_t Npaths = path_generator.num_paths(); size_t Noffsets = safe_first_shape(offsets); @@ -994,31 +998,29 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, } } + gc.isaa = antialiaseds(i % Naa); + transformed_path_t tpath(path, trans); + nan_removed_t nan_removed(tpath, true, has_codes); + clipped_t clipped(nan_removed, do_clip, width, height); if (check_snap) { - gc.isaa = antialiaseds(i % Naa); - - transformed_path_t tpath(path, trans); - nan_removed_t nan_removed(tpath, true, has_codes); - clipped_t clipped(nan_removed, do_clip, width, height); snapped_t snapped( clipped, gc.snap_mode, path.total_vertices(), points_to_pixels(gc.linewidth)); if (has_codes) { snapped_curve_t curve(snapped); - _draw_path(curve, has_clippath, face, gc); + sketch_snapped_curve_t sketch(curve, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + _draw_path(sketch, has_clippath, face, gc); } else { - _draw_path(snapped, has_clippath, face, gc); + sketch_snapped_t sketch(snapped, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + _draw_path(sketch, has_clippath, face, gc); } } else { - gc.isaa = antialiaseds(i % Naa); - - transformed_path_t tpath(path, trans); - nan_removed_t nan_removed(tpath, true, has_codes); - clipped_t clipped(nan_removed, do_clip, width, height); if (has_codes) { curve_t curve(clipped); - _draw_path(curve, has_clippath, face, gc); + sketch_curve_t sketch(curve, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + _draw_path(sketch, has_clippath, face, gc); } else { - _draw_path(clipped, has_clippath, face, gc); + sketch_clipped_t sketch(clipped, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + _draw_path(sketch, has_clippath, face, gc); } } } From ba05b4484aac61daebd6573a44a4c66c6f1512dd Mon Sep 17 00:00:00 2001 From: hannah Date: Thu, 26 Sep 2024 11:14:09 -0400 Subject: [PATCH 0290/1230] doc: add pandas and xarray fixtures to testing API docs (#28879) Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- doc/api/testing_api.rst | 8 ++++++++ doc/conf.py | 1 + lib/matplotlib/testing/conftest.py | 31 ++++++++++++++++++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/doc/api/testing_api.rst b/doc/api/testing_api.rst index 7731d4510b27..ae81d2f89ca7 100644 --- a/doc/api/testing_api.rst +++ b/doc/api/testing_api.rst @@ -37,3 +37,11 @@ :members: :undoc-members: :show-inheritance: + + +Testing with optional dependencies +================================== +For more information on fixtures, see :external+pytest:ref:`pytest fixtures `. + +.. autofunction:: matplotlib.testing.conftest.pd +.. autofunction:: matplotlib.testing.conftest.xr diff --git a/doc/conf.py b/doc/conf.py index ea6b1a3fa444..c9d498e939f7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -220,6 +220,7 @@ def tutorials_download_error(record): autosummary_generate = True autodoc_typehints = "none" +autodoc_mock_imports = ["pytest"] # we should ignore warnings coming from importing deprecated modules for # autodoc purposes, as this will disappear automatically when they are removed diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index c285c247e7b4..3f96de611195 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -82,7 +82,20 @@ def mpl_test_settings(request): @pytest.fixture def pd(): - """Fixture to import and configure pandas.""" + """ + Fixture to import and configure pandas. Using this fixture, the test is skipped when + pandas is not installed. Use this fixture instead of importing pandas in test files. + + Examples + -------- + Request the pandas fixture by passing in ``pd`` as an argument to the test :: + + def test_matshow_pandas(pd): + + df = pd.DataFrame({'x':[1,2,3], 'y':[4,5,6]}) + im = plt.figure().subplots().matshow(df) + np.testing.assert_array_equal(im.get_array(), df) + """ pd = pytest.importorskip('pandas') try: from pandas.plotting import ( @@ -95,6 +108,20 @@ def pd(): @pytest.fixture def xr(): - """Fixture to import xarray.""" + """ + Fixture to import xarray so that the test is skipped when xarray is not installed. + Use this fixture instead of importing xrray in test files. + + Examples + -------- + Request the xarray fixture by passing in ``xr`` as an argument to the test :: + + def test_imshow_xarray(xr): + + ds = xr.DataArray(np.random.randn(2, 3)) + im = plt.figure().subplots().imshow(ds) + np.testing.assert_array_equal(im.get_array(), ds) + """ + xr = pytest.importorskip('xarray') return xr From e8e12df059fdbbeb03164ec6900d4922e54bdb67 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 26 Sep 2024 22:14:19 +0200 Subject: [PATCH 0291/1230] MNT: Warn if fixed aspect overwrites explicitly set data limits (#28683) Closes #28673. --- lib/matplotlib/axes/_base.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index d49e6f10a54c..92c5354435e7 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1713,7 +1713,8 @@ def set_adjustable(self, adjustable, share=False): ---------- adjustable : {'box', 'datalim'} If 'box', change the physical dimensions of the Axes. - If 'datalim', change the ``x`` or ``y`` data limits. + If 'datalim', change the ``x`` or ``y`` data limits. This + may ignore explicitly defined axis limits. share : bool, default: False If ``True``, apply the settings to all shared Axes. @@ -2030,11 +2031,17 @@ def apply_aspect(self, position=None): yc = 0.5 * (ymin + ymax) y0 = yc - Ysize / 2.0 y1 = yc + Ysize / 2.0 + if not self.get_autoscaley_on(): + _log.warning("Ignoring fixed y limits to fulfill fixed data aspect " + "with adjustable data limits.") self.set_ybound(y_trf.inverted().transform([y0, y1])) else: xc = 0.5 * (xmin + xmax) x0 = xc - Xsize / 2.0 x1 = xc + Xsize / 2.0 + if not self.get_autoscalex_on(): + _log.warning("Ignoring fixed x limits to fulfill fixed data aspect " + "with adjustable data limits.") self.set_xbound(x_trf.inverted().transform([x0, x1])) def axis(self, arg=None, /, *, emit=True, **kwargs): From 13380e100200db3a04cb43be968d6962d51d0a28 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 26 Sep 2024 22:27:06 +0200 Subject: [PATCH 0292/1230] MNT: Cleanup FontProperties __init__ API (#28843) The cleanup approach is to not modify code logic during a deprecation period, but only detect deprecated call patterns through a decorator and warn on them. --- .../deprecations/28843-TH.rst | 9 +++ lib/matplotlib/font_manager.py | 64 ++++++++++++++++++- lib/matplotlib/tests/test_font_manager.py | 40 ++++++++++++ lib/matplotlib/ticker.py | 2 +- 4 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/28843-TH.rst diff --git a/doc/api/next_api_changes/deprecations/28843-TH.rst b/doc/api/next_api_changes/deprecations/28843-TH.rst new file mode 100644 index 000000000000..25dc91be3ccc --- /dev/null +++ b/doc/api/next_api_changes/deprecations/28843-TH.rst @@ -0,0 +1,9 @@ +FontProperties initialization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`.FontProperties` initialization is limited to the two call patterns: + +- single positional parameter, interpreted as fontconfig pattern +- only keyword parameters for setting individual properties + +All other previously supported call patterns are deprecated. diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index d9560ec0cc0f..890663381b3d 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -32,6 +32,7 @@ import copy import dataclasses from functools import lru_cache +import functools from io import BytesIO import json import logging @@ -536,6 +537,57 @@ def afmFontProperty(fontpath, font): return FontEntry(fontpath, name, style, variant, weight, stretch, size) +def _cleanup_fontproperties_init(init_method): + """ + A decorator to limit the call signature to single a positional argument + or alternatively only keyword arguments. + + We still accept but deprecate all other call signatures. + + When the deprecation expires we can switch the signature to:: + + __init__(self, pattern=None, /, *, family=None, style=None, ...) + + plus a runtime check that pattern is not used alongside with the + keyword arguments. This results eventually in the two possible + call signatures:: + + FontProperties(pattern) + FontProperties(family=..., size=..., ...) + + """ + @functools.wraps(init_method) + def wrapper(self, *args, **kwargs): + # multiple args with at least some positional ones + if len(args) > 1 or len(args) == 1 and kwargs: + # Note: Both cases were previously handled as individual properties. + # Therefore, we do not mention the case of font properties here. + _api.warn_deprecated( + "3.10", + message="Passing individual properties to FontProperties() " + "positionally was deprecated in Matplotlib %(since)s and " + "will be removed in %(removal)s. Please pass all properties " + "via keyword arguments." + ) + # single non-string arg -> clearly a family not a pattern + if len(args) == 1 and not kwargs and not cbook.is_scalar_or_string(args[0]): + # Case font-family list passed as single argument + _api.warn_deprecated( + "3.10", + message="Passing family as positional argument to FontProperties() " + "was deprecated in Matplotlib %(since)s and will be removed " + "in %(removal)s. Please pass family names as keyword" + "argument." + ) + # Note on single string arg: + # This has been interpreted as pattern so far. We are already raising if a + # non-pattern compatible family string was given. Therefore, we do not need + # to warn for this case. + return init_method(self, *args, **kwargs) + + return wrapper + + class FontProperties: """ A class for storing and manipulating font properties. @@ -585,9 +637,14 @@ class FontProperties: approach allows all text sizes to be made larger or smaller based on the font manager's default font size. - This class will also accept a fontconfig_ pattern_, if it is the only - argument provided. This support does not depend on fontconfig; we are - merely borrowing its pattern syntax for use here. + This class accepts a single positional string as fontconfig_ pattern_, + or alternatively individual properties as keyword arguments:: + + FontProperties(pattern) + FontProperties(*, family=None, style=None, variant=None, ...) + + This support does not depend on fontconfig; we are merely borrowing its + pattern syntax for use here. .. _fontconfig: https://www.freedesktop.org/wiki/Software/fontconfig/ .. _pattern: @@ -599,6 +656,7 @@ class FontProperties: fontconfig. """ + @_cleanup_fontproperties_init def __init__(self, family=None, style=None, variant=None, weight=None, stretch=None, size=None, fname=None, # if set, it's a hardcoded filename to use diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index ab8c6c70d1bf..25b6a122c7ce 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -11,6 +11,7 @@ import numpy as np import pytest +import matplotlib as mpl from matplotlib.font_manager import ( findfont, findSystemFonts, FontEntry, FontProperties, fontManager, json_dump, json_load, get_font, is_opentype_cff_font, @@ -367,3 +368,42 @@ def inner(): for obj in gc.get_objects(): if isinstance(obj, SomeObject): pytest.fail("object from inner stack still alive") + + +def test_fontproperties_init_deprecation(): + """ + Test the deprecated API of FontProperties.__init__. + + The deprecation does not change behavior, it only adds a deprecation warning + via a decorator. Therefore, the purpose of this test is limited to check + which calls do and do not issue deprecation warnings. Behavior is still + tested via the existing regular tests. + """ + with pytest.warns(mpl.MatplotlibDeprecationWarning): + # multiple positional arguments + FontProperties("Times", "italic") + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + # Mixed positional and keyword arguments + FontProperties("Times", size=10) + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + # passing a family list positionally + FontProperties(["Times"]) + + # still accepted: + FontProperties(family="Times", style="italic") + FontProperties(family="Times") + FontProperties("Times") # works as pattern and family + FontProperties("serif-24:style=oblique:weight=bold") # pattern + + # also still accepted: + # passing as pattern via family kwarg was not covered by the docs but + # historically worked. This is left unchanged for now. + # AFAICT, we cannot detect this: We can determine whether a string + # works as pattern, but that doesn't help, because there are strings + # that are both pattern and family. We would need to identify, whether + # a string is *not* a valid family. + # Since this case is not covered by docs, I've refrained from jumping + # extra hoops to detect this possible API misuse. + FontProperties(family="serif-24:style=oblique:weight=bold") diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 23abb6aef6b9..0053031ece3e 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -574,7 +574,7 @@ def set_useMathText(self, val): from matplotlib import font_manager ufont = font_manager.findfont( font_manager.FontProperties( - mpl.rcParams["font.family"] + family=mpl.rcParams["font.family"] ), fallback_to_default=False, ) From 674751af1c7f3d6cd82847e6c0fe6f2afdc97c98 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 26 Sep 2024 21:07:05 -0400 Subject: [PATCH 0293/1230] ci: Correct stubtest allow list format The list is full of regexs, not globs, so correctly escape the periods to be more specific. Note that to catch `module.__init__`, you need to specific `module`, so there is some slight modification for `matplotlib.tests` / `matplotlib.sphinxext`. --- ci/mypy-stubtest-allowlist.txt | 56 +++++++++++++++++----------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 4b6e487a418d..1d08a690d8f2 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -1,51 +1,51 @@ # Non-typed (and private) modules/functions -matplotlib.backends.* -matplotlib.tests.* -matplotlib.pylab.* -matplotlib._.* -matplotlib.rcsetup._listify_validator -matplotlib.rcsetup._validate_linestyle -matplotlib.ft2font.Glyph -matplotlib.testing.jpl_units.* -matplotlib.sphinxext.* +matplotlib\.backends\..* +matplotlib\.tests(\..*)? +matplotlib\.pylab\..* +matplotlib\._.* +matplotlib\.rcsetup\._listify_validator +matplotlib\.rcsetup\._validate_linestyle +matplotlib\.ft2font\.Glyph +matplotlib\.testing\.jpl_units\..* +matplotlib\.sphinxext(\..*)? # set methods have heavy dynamic usage of **kwargs, with differences for subclasses # which results in technically inconsistent signatures, but not actually a problem -matplotlib.*\.set$ +matplotlib\..*\.set$ # Typed inline, inconsistencies largely due to imports -matplotlib.pyplot.* -matplotlib.typing.* +matplotlib\.pyplot\..* +matplotlib\.typing\..* # Other decorator modifying signature # Backcompat decorator which does not modify runtime reported signature -matplotlib.offsetbox.*Offset[Bb]ox.get_offset +matplotlib\.offsetbox\..*Offset[Bb]ox\.get_offset # Inconsistent super/sub class parameter name (maybe rename for consistency) -matplotlib.projections.polar.RadialLocator.nonsingular -matplotlib.ticker.LogLocator.nonsingular -matplotlib.ticker.LogitLocator.nonsingular +matplotlib\.projections\.polar\.RadialLocator\.nonsingular +matplotlib\.ticker\.LogLocator\.nonsingular +matplotlib\.ticker\.LogitLocator\.nonsingular # Stdlib/Enum considered inconsistent (no fault of ours, I don't think) -matplotlib.backend_bases._Mode.__new__ -matplotlib.units.Number.__hash__ +matplotlib\.backend_bases\._Mode\.__new__ +matplotlib\.units\.Number\.__hash__ # 3.6 Pending deprecations -matplotlib.figure.Figure.set_constrained_layout -matplotlib.figure.Figure.set_constrained_layout_pads -matplotlib.figure.Figure.set_tight_layout +matplotlib\.figure\.Figure\.set_constrained_layout +matplotlib\.figure\.Figure\.set_constrained_layout_pads +matplotlib\.figure\.Figure\.set_tight_layout # Maybe should be abstractmethods, required for subclasses, stubs define once -matplotlib.tri.*TriInterpolator.__call__ -matplotlib.tri.*TriInterpolator.gradient +matplotlib\.tri\..*TriInterpolator\.__call__ +matplotlib\.tri\..*TriInterpolator\.gradient # TypeVar used only in type hints -matplotlib.backend_bases.FigureCanvasBase._T -matplotlib.backend_managers.ToolManager._T -matplotlib.spines.Spine._T +matplotlib\.backend_bases\.FigureCanvasBase\._T +matplotlib\.backend_managers\.ToolManager\._T +matplotlib\.spines\.Spine\._T # Parameter inconsistency due to 3.10 deprecation -matplotlib.figure.FigureBase.get_figure +matplotlib\.figure\.FigureBase\.get_figure # getitem method only exists for 3.10 deprecation backcompatability -matplotlib.inset.InsetIndicator.__getitem__ +matplotlib\.inset\.InsetIndicator\.__getitem__ From 1f6bfca248f6b6482c4a67b4f5a6f7c3e19b2436 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 26 Sep 2024 23:24:24 -0400 Subject: [PATCH 0294/1230] ci: Skip adding a stubtest exception when already allowed This fixes the "unused allowlist entry" error we see in #28874. --- tools/stubtest.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/tools/stubtest.py b/tools/stubtest.py index 77676595cbf8..b79ab2f40dd0 100644 --- a/tools/stubtest.py +++ b/tools/stubtest.py @@ -1,6 +1,7 @@ import ast import os import pathlib +import re import subprocess import sys import tempfile @@ -12,10 +13,19 @@ class Visitor(ast.NodeVisitor): - def __init__(self, filepath, output): + def __init__(self, filepath, output, existing_allowed): self.filepath = filepath self.context = list(filepath.with_suffix("").relative_to(lib).parts) self.output = output + self.existing_allowed = existing_allowed + + def _is_already_allowed(self, parts): + # Skip outputting a path if it's already allowed before. + candidates = ['.'.join(parts[:s]) for s in range(1, len(parts))] + for allow in self.existing_allowed: + if any(allow.fullmatch(path) for path in candidates): + return True + return False def visit_FunctionDef(self, node): # delete_parameter adds a private sentinel value that leaks @@ -43,7 +53,9 @@ def visit_FunctionDef(self, node): ): parents.insert(0, parent.name) parent = parent.parent - self.output.write(f"{'.'.join(self.context + parents)}.{node.name}\n") + parts = [*self.context, *parents, node.name] + if not self._is_already_allowed(parts): + self.output.write("\\.".join(parts) + "\n") break def visit_ClassDef(self, node): @@ -62,20 +74,28 @@ def visit_ClassDef(self, node): # for setters on items with only a getter for substitutions in aliases.values(): parts = self.context + parents + [node.name] - self.output.write( - "\n".join( - f"{'.'.join(parts)}.[gs]et_{a}\n" for a in substitutions - ) - ) + for a in substitutions: + if not (self._is_already_allowed([*parts, f"get_{a}"]) and + self._is_already_allowed([*parts, f"set_{a}"])): + self.output.write("\\.".join([*parts, f"[gs]et_{a}\n"])) for child in ast.iter_child_nodes(node): self.visit(child) +existing_allowed = [] +with (root / 'ci/mypy-stubtest-allowlist.txt').open() as f: + for line in f: + line, _, _ = line.partition('#') + line = line.strip() + if line: + existing_allowed.append(re.compile(line)) + + with tempfile.TemporaryDirectory() as d: p = pathlib.Path(d) / "allowlist.txt" with p.open("wt") as f: for path in mpl.glob("**/*.py"): - v = Visitor(path, f) + v = Visitor(path, f, existing_allowed) tree = ast.parse(path.read_text()) # Assign parents to tree so they can be backtraced From 133c916b83a0354c5a2279c698110c981217295e Mon Sep 17 00:00:00 2001 From: MischaMegens2 <122418839+MischaMegens2@users.noreply.github.com> Date: Fri, 27 Sep 2024 01:25:11 -0700 Subject: [PATCH 0295/1230] Implement review suggestions --- doc/api/toolkits/mplot3d/view_angles.rst | 99 ++++++++++++++------- doc/users/next_whats_new/mouse_rotation.rst | 32 ++++++- lib/mpl_toolkits/mplot3d/axes3d.py | 49 ++++------ 3 files changed, 117 insertions(+), 63 deletions(-) diff --git a/doc/api/toolkits/mplot3d/view_angles.rst b/doc/api/toolkits/mplot3d/view_angles.rst index 377e1452911a..6ddb757f44d3 100644 --- a/doc/api/toolkits/mplot3d/view_angles.rst +++ b/doc/api/toolkits/mplot3d/view_angles.rst @@ -40,6 +40,8 @@ further documented in the `.mplot3d.axes3d.Axes3D.view_init` API. :align: center +.. _toolkit_mouse-rotation: + Rotation with mouse =================== @@ -48,99 +50,132 @@ There are various ways to accomplish this; the style of mouse rotation can be specified by setting ``rcParams.axes3d.mouserotationstyle``, see :doc:`/users/explain/customizing`. -Originally (with ``mouserotationstyle: azel``), the 2D mouse position -corresponded directly to azimuth and elevation; this is also how it is done +Originally (prior to v3.10), the 2D mouse position corresponded directly +to azimuth and elevation; this is also how it is done in `MATLAB `_. +To keep it this way, set ``mouserotationstyle: azel``. This approach works fine for polar plots, where the *z* axis is special; however, it leads to a kind of 'gimbal lock' when looking down the *z* axis: the plot reacts differently to mouse movement, dependent on the particular orientation at hand. Also, 'roll' cannot be controlled. As an alternative, there are various mouse rotation styles where the mouse -manipulates a 'trackball'. In its simplest form (``mouserotationstyle: trackball``), +manipulates a virtual 'trackball'. In its simplest form (``mouserotationstyle: trackball``), the trackball rotates around an in-plane axis perpendicular to the mouse motion (it is as if there is a plate laying on the trackball; the plate itself is fixed in orientation, but you can drag the plate with the mouse, thus rotating the ball). This is more natural to work with than the ``azel`` style; however, the plot cannot be easily rotated around the viewing direction - one has to -drag the mouse in circles with a handedness opposite to the desired rotation. +move the mouse in circles with a handedness opposite to the desired rotation, +counterintuitively. A different variety of trackball rotates along the shortest arc on the virtual sphere (``mouserotationstyle: arcball``); it is a variation on Ken Shoemake's ARCBALL [Shoemake1992]_. Rotating around the viewing direction is straightforward -with it. Shoemake's original arcball is also available -(``mouserotationstyle: Shoemake``); it is free of hysteresis, i.e., -returning mouse to the original position returns the figure to its original -orientation, the rotation is independent of the details of the path the mouse -took. However, Shoemake's arcball rotates at twice the angular rate of the -mouse movement (it is quite noticeable, especially when adjusting roll). +with it (grab the ball near its edge instead of near the center). + +Shoemake's original arcball is also available (``mouserotationstyle: Shoemake``); +it is free of hysteresis, i.e., returning mouse to the original position +returns the figure to its original orientation, the rotation is independent +of the details of the path the mouse took, which could be desirable. +However, Shoemake's arcball rotates at twice the angular rate of the +mouse movement (it is quite noticeable, especially when adjusting roll), +and it lacks an obvious mechanical equivalent; arguably, the path-independent rotation is unnatural. So it is a trade-off. Shoemake's arcball has an abrupt edge; this is remedied in Holroyd's arcball (``mouserotationstyle: Holroyd``). -Henriksen et al. [Henriksen2002]_ provide an overview. - -In summary: +Henriksen et al. [Henriksen2002]_ provide an overview. In summary: .. list-table:: :width: 100% - :widths: 30 20 20 20 35 + :widths: 30 20 20 20 20 35 * - Style - traditional [1]_ - incl. roll [2]_ - uniform [3]_ - path independent [4]_ + - mechanical counterpart [5]_ * - azel - ✔️ - ❌ - ❌ - ✔️ + - ✔️ * - trackball - ❌ - - ~ + - ✓ [6]_ - ✔️ - ❌ + - ✔️ * - arcball - ❌ - ✔️ - ✔️ - ❌ + - ✔️ * - Shoemake - ❌ - ✔️ - ✔️ - ✔️ + - ❌ * - Holroyd - ❌ - ✔️ - ✔️ - ✔️ + - ❌ -.. [1] The way it was historically; this is also MATLAB's style +.. [1] The way it was prior to v3.10; this is also MATLAB's style .. [2] Mouse controls roll too (not only azimuth and elevation) .. [3] Figure reacts the same way to mouse movements, regardless of orientation (no difference between 'poles' and 'equator') .. [4] Returning mouse to original position returns figure to original orientation (no hysteresis: rotation is independent of the details of the path the mouse took) +.. [5] The style has a corresponding natural implementation as a mechanical device +.. [6] While it is possible to control roll with the ``trackball`` style, this is not very intuitive (it requires moving the mouse in large circles) and the resulting roll is in the opposite direction -Try it out by adding a file ``matplotlibrc`` to folder ``matplotlib\galleries\examples\mplot3d``, -with contents:: +You can try out one of the various mouse rotation styles using:: - axes3d.mouserotationstyle: arcball +.. code:: + + import matplotlib as mpl + mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'arcball', 'Shoemake', or 'Holroyd' -(or any of the other styles), and run a suitable example, e.g.:: + import numpy as np + import matplotlib.pyplot as plt + from matplotlib import cm + + ax = plt.figure().add_subplot(projection='3d') + + X = np.arange(-5, 5, 0.25) + Y = np.arange(-5, 5, 0.25) + X, Y = np.meshgrid(X, Y) + R = np.sqrt(X**2 + Y**2) + Z = np.sin(R) + + surf = ax.plot_surface(X, Y, Z, cmap=cm.coolwarm, + linewidth=0, antialiased=False) - python surfaced3d.py + plt.show() -(If eternal compatibility with the horrors of the past is less of a consideration -for you, then it is likely that you would want to go with ``arcball``, ``Shoemake``, -or ``Holroyd``.) +Alternatively, create a file ``matplotlibrc``, with contents:: -The size of the trackball or arcball can be adjusted by setting -``rcParams.axes3d.trackballsize``, in units of the Axes bounding box; + axes3d.mouserotationstyle: arcball + +(or any of the other styles, instead of ``arcball``), and then run any of +the :ref:`mplot3d-examples-index` examples. + +The size of the virtual trackball or arcball can be adjusted as well, +by setting ``rcParams.axes3d.trackballsize``. This specifies how much +mouse motion is needed to obtain a given rotation angle (when near the center), +and it controls where the edge of the arcball is (how far from the center, +how close to the plot edge). +The size is specified in units of the Axes bounding box, i.e., to make the trackball span the whole bounding box, set it to 1. -A size of ca. 2/3 appears to work reasonably well. +A size of about 2/3 appears to work reasonably well; this is the default. ---- @@ -149,8 +184,10 @@ A size of ca. 2/3 appears to work reasonably well. Interface '92, 1992, pp. 151-156, https://doi.org/10.20380/GI1992.18 .. [Henriksen2002] Knud Henriksen, Jon Sporring, Kasper Hornbæk, - "Virtual Trackballs Revisited", in Proceedings of DSAGM'2002: - http://www.diku.dk/~kash/papers/DSAGM2002_henriksen.pdf; - and in IEEE Transactions on Visualization - and Computer Graphics, Volume 10, Issue 2, March-April 2004, pp. 206-216, + "Virtual Trackballs Revisited", in Proceedings of DSAGM'2002 + `[pdf]`__; + and in IEEE Transactions on Visualization and Computer Graphics, + Volume 10, Issue 2, March-April 2004, pp. 206-216, https://doi.org/10.1109/TVCG.2004.1260772 + +__ https://web.archive.org/web/20240607102518/http://hjemmesider.diku.dk/~kash/papers/DSAGM2002_henriksen.pdf diff --git a/doc/users/next_whats_new/mouse_rotation.rst b/doc/users/next_whats_new/mouse_rotation.rst index 00198565c54e..4d8257ff1182 100644 --- a/doc/users/next_whats_new/mouse_rotation.rst +++ b/doc/users/next_whats_new/mouse_rotation.rst @@ -8,7 +8,37 @@ degrees of freedom (azimuth, elevation, and roll). By default, it uses a variation on Ken Shoemake's ARCBALL [1]_. The particular style of mouse rotation can be set via ``rcParams.axes3d.mouserotationstyle``. -See also :doc:`/api/toolkits/mplot3d/view_angles`. +See also :ref:`toolkit_mouse-rotation`. + +To revert to the original mouse rotation style, +create a file ``matplotlibrc`` with contents:: + + axes3d.mouserotationstyle: azel + +To try out one of the various mouse rotation styles: + +.. code:: + + import matplotlib as mpl + mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'arcball', 'Shoemake', or 'Holroyd' + + import numpy as np + import matplotlib.pyplot as plt + from matplotlib import cm + + ax = plt.figure().add_subplot(projection='3d') + + X = np.arange(-5, 5, 0.25) + Y = np.arange(-5, 5, 0.25) + X, Y = np.meshgrid(X, Y) + R = np.sqrt(X**2 + Y**2) + Z = np.sin(R) + + surf = ax.plot_surface(X, Y, Z, cmap=cm.coolwarm, + linewidth=0, antialiased=False) + + plt.show() + .. [1] Ken Shoemake, "ARCBALL: A user interface for specifying three-dimensional rotation using a mouse", in Proceedings of Graphics diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 3d108420422e..c3c9c6f88156 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -1508,10 +1508,11 @@ def _calc_coord(self, xv, yv, renderer=None): p2 = p1 - scale*vec return p2, pane_idx - def _arcball(self, x: float, y: float, Holroyd: bool) -> np.ndarray: + def _arcball(self, x: float, y: float, style: str) -> np.ndarray: """ Convert a point (x, y) to a point on a virtual trackball - This is Ken Shoemake's arcball + either Ken Shoemake's arcball (a sphere) or + Tom Holroyd's (a sphere combined with a hyperbola). See: Ken Shoemake, "ARCBALL: A user interface for specifying three-dimensional rotation using a mouse." in Proceedings of Graphics Interface '92, 1992, pp. 151-156, @@ -1521,12 +1522,12 @@ def _arcball(self, x: float, y: float, Holroyd: bool) -> np.ndarray: x /= s y /= s r2 = x*x + y*y - if Holroyd: + if style == 'Holroyd': if r2 > 0.5: p = np.array([1/(2*math.sqrt(r2)), x, y])/math.sqrt(1/(4*r2)+r2) else: p = np.array([math.sqrt(1-r2), x, y]) - else: # Shoemake + else: # 'arcball', 'Shoemake' if r2 > 1: p = np.array([0, x/math.sqrt(r2), y/math.sqrt(r2)]) else: @@ -1577,38 +1578,24 @@ def _on_move(self, event): azim = self.azim + dazim roll = self.roll else: - # Convert to quaternion - elev = np.deg2rad(self.elev) - azim = np.deg2rad(self.azim) - roll = np.deg2rad(self.roll) - q = _Quaternion.from_cardan_angles(elev, azim, roll) + q = _Quaternion.from_cardan_angles( + *np.deg2rad((self.elev, self.azim, self.roll))) - if style in ['arcball', 'Shoemake', 'Holroyd']: - # Update quaternion - is_Holroyd = (style == 'Holroyd') - current_vec = self._arcball(self._sx/w, self._sy/h, is_Holroyd) - new_vec = self._arcball(x/w, y/h, is_Holroyd) + if style == 'trackball': + k = np.array([0, -dy/h, dx/w]) + nk = np.linalg.norm(k) + th = nk / mpl.rcParams['axes3d.trackballsize'] + dq = _Quaternion(np.cos(th), k*np.sin(th)/nk) + else: # 'arcball', 'Shoemake', 'Holroyd' + current_vec = self._arcball(self._sx/w, self._sy/h, style) + new_vec = self._arcball(x/w, y/h, style) if style == 'arcball': dq = _Quaternion.rotate_from_to(current_vec, new_vec) else: # 'Shoemake', 'Holroyd' dq = _Quaternion(0, new_vec) * _Quaternion(0, -current_vec) - q = dq * q - elif style == 'trackball': - s = mpl.rcParams['axes3d.trackballsize'] / 2 - k = np.array([0, -(y-self._sy)/h, (x-self._sx)/w]) / s - nk = np.linalg.norm(k) - th = nk / 2 - dq = _Quaternion(math.cos(th), k*math.sin(th)/nk) - q = dq * q - else: - warnings.warn("Mouse rotation style (axes3d.mouserotationstyle: " + - style + ") not recognized.") - - # Convert to elev, azim, roll - elev, azim, roll = q.as_cardan_angles() - elev = np.rad2deg(elev) - azim = np.rad2deg(azim) - roll = np.rad2deg(roll) + + q = dq * q + elev, azim, roll = np.rad2deg(q.as_cardan_angles()) # update view vertical_axis = self._axis_names[self._vertical_axis] From 0fbebc84b12c7600febb1b5c071774ca2fe0ec9d Mon Sep 17 00:00:00 2001 From: hannah Date: Thu, 26 Sep 2024 17:14:45 -0400 Subject: [PATCH 0296/1230] doc: closes #28892 by replacing toc with link out to minimal dependencies Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- doc/devel/development_setup.rst | 17 ++++---- doc/install/dependencies.rst | 72 ++++++++++++++++++--------------- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/doc/devel/development_setup.rst b/doc/devel/development_setup.rst index 5ac78cea8c90..acd004690e8b 100644 --- a/doc/devel/development_setup.rst +++ b/doc/devel/development_setup.rst @@ -155,19 +155,20 @@ The simplest way to do this is to use either Python's virtual environment Remember to activate the environment whenever you start working on Matplotlib. -Install Dependencies -==================== +Install external dependencies +============================= -Most Python dependencies will be installed when :ref:`setting up the environment ` -but non-Python dependencies like C++ compilers, LaTeX, and other system applications -must be installed separately. +Python dependencies were installed as part of :ref:`setting up the environment `. +Additionally, the following non-Python dependencies must also be installed: -.. toctree:: - :maxdepth: 2 +.. rst-class:: checklist - ../install/dependencies +* :ref:`c++ compiler` +* :ref:`documentation build dependencies ` +For a full list of dependencies, see :ref:`dependencies`. + .. _development-install: Install Matplotlib in editable mode diff --git a/doc/install/dependencies.rst b/doc/install/dependencies.rst index 3d921d2d10c9..988839114bae 100644 --- a/doc/install/dependencies.rst +++ b/doc/install/dependencies.rst @@ -250,37 +250,38 @@ development environment that must be installed before a compiler can be installe You may also need to install headers for various libraries used in the compiled extension source files. +.. _dev-compiler: .. tab-set:: - .. tab-item:: Linux + .. tab-item:: Linux - On some Linux systems, you can install a meta-build package. For example, - on Ubuntu ``apt install build-essential`` + On some Linux systems, you can install a meta-build package. For example, + on Ubuntu ``apt install build-essential`` - Otherwise, use the system distribution's package manager to install - :ref:`gcc `. + Otherwise, use the system distribution's package manager to install + :ref:`gcc `. - .. tab-item:: macOS + .. tab-item:: macOS - Install `Xcode `_ for Apple platform development. + Install `Xcode `_ for Apple platform development. - .. tab-item:: Windows + .. tab-item:: Windows - Install `Visual Studio Build Tools `_ + Install `Visual Studio Build Tools `_ - Make sure "Desktop development with C++" is selected, and that the latest MSVC, - "C++ CMake tools for Windows," and a Windows SDK compatible with your version - of Windows are selected and installed. They should be selected by default under - the "Optional" subheading, but are required to build Matplotlib from source. + Make sure "Desktop development with C++" is selected, and that the latest MSVC, + "C++ CMake tools for Windows," and a Windows SDK compatible with your version + of Windows are selected and installed. They should be selected by default under + the "Optional" subheading, but are required to build Matplotlib from source. - Alternatively, you can install a Linux-like environment such as `CygWin `_ - or `Windows Subsystem for Linux `_. - If using `MinGW-64 `_, we require **v6** of the - ```Mingw-w64-x86_64-headers``. + Alternatively, you can install a Linux-like environment such as `CygWin `_ + or `Windows Subsystem for Linux `_. + If using `MinGW-64 `_, we require **v6** of the + ```Mingw-w64-x86_64-headers``. -We highly recommend that you install a compiler using your platform tool, i.e., -Xcode, VS Code or Linux package manager. Choose **one** compiler from this list: +We highly recommend that you install a compiler using your platform tool, i.e., Xcode, +VS Code or Linux package manager. Choose **one** compiler from this list: .. _compiler-table: @@ -307,7 +308,6 @@ Xcode, VS Code or Linux package manager. Choose **one** compiler from this list: - `Visual Studio 2019 C++ `_ - .. _test-dependencies: Test dependencies @@ -327,8 +327,11 @@ Optional In addition to all of the optional dependencies on the main library, for testing the following will be used if they are installed. -- Ghostscript_ (>= 9.0, to render PDF files) -- Inkscape_ (to render SVG files) +Python +^^^^^^ +These packages are installed when :ref:`creating a virtual environment `, +otherwise they must be installed manually: + - nbformat_ and nbconvert_ used to test the notebook backend - pandas_ used to test compatibility with Pandas - pikepdf_ used in some tests for the pgf and pdf backends @@ -340,9 +343,14 @@ testing the following will be used if they are installed. - pytest-xvfb_ to run tests without windows popping up (Linux) - pytz_ used to test pytz int - sphinx_ used to test our sphinx extensions +- xarray_ used to test compatibility with xarray + +External tools +^^^^^^^^^^^^^^ +- Ghostscript_ (>= 9.0, to render PDF files) +- Inkscape_ (to render SVG files) - `WenQuanYi Zen Hei`_ and `Noto Sans CJK`_ fonts for testing font fallback and non-Western fonts -- xarray_ used to test compatibility with xarray If any of these dependencies are not discovered, then the tests that rely on them will be skipped by pytest. @@ -355,6 +363,7 @@ them will be skipped by pytest. .. _Ghostscript: https://ghostscript.com/ .. _Inkscape: https://inkscape.org +.. _WenQuanYi Zen Hei: http://wenq.org/en/ .. _flake8: https://pypi.org/project/flake8/ .. _nbconvert: https://pypi.org/project/nbconvert/ .. _nbformat: https://pypi.org/project/nbformat/ @@ -369,7 +378,6 @@ them will be skipped by pytest. .. _pytest-xvfb: https://pypi.org/project/pytest-xvfb/ .. _pytest: http://doc.pytest.org/en/latest/ .. _sphinx: https://pypi.org/project/Sphinx/ -.. _WenQuanYi Zen Hei: http://wenq.org/en/ .. _Noto Sans CJK: https://fonts.google.com/noto/use .. _xarray: https://pypi.org/project/xarray/ @@ -394,14 +402,15 @@ The content of :file:`doc-requirements.txt` is also shown below: :literal: +.. _doc-dependencies-external: + External tools -------------- -The documentation requires LaTeX and Graphviz. These are not -Python packages and must be installed separately. - Required ^^^^^^^^ +The documentation requires LaTeX and Graphviz. These are not +Python packages and must be installed separately. * `Graphviz `_ * a minimal working LaTeX distribution, e.g. `TeX Live `_ or @@ -409,15 +418,14 @@ Required The following LaTeX packages: - * `dvipng `_ - * `underscore `_ - * `cm-super `_ - * ``collection-fontsrecommended`` +* `dvipng `_ +* `underscore `_ +* `cm-super `_ +* ``collection-fontsrecommended`` The complete version of many LaTex distribution installers, e.g. "texlive-full" or "texlive-all", will often automatically include these packages. - Optional ^^^^^^^^ From dab0e8ffbec5a7ed2181fb06c8867ad2b5ac7bad Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 27 Sep 2024 18:28:10 +0200 Subject: [PATCH 0297/1230] Don't cache exception with traceback reference loop in dviread. Rather, preemptively cache the reference loop by explicitly dropping the traceback. See also db0c7c3. --- lib/matplotlib/dviread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 040ca5ef4365..0eae1852a91b 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -504,7 +504,7 @@ def _fnt_def_real(self, k, c, s, d, a, l): # and throw that error in Dvi._read. For Vf, _finalize_packet # checks whether a missing glyph has been used, and in that case # skips the glyph definition. - self.fonts[k] = exc + self.fonts[k] = exc.with_traceback(None) return if c != 0 and tfm.checksum != 0 and c != tfm.checksum: raise ValueError(f'tfm checksum mismatch: {n}') From e90952ffac668bd5115c4009bf20dd6e595c3cd5 Mon Sep 17 00:00:00 2001 From: Kyra Cho Date: Fri, 27 Sep 2024 16:55:12 -0700 Subject: [PATCH 0298/1230] Fix `axline` for slopes <= 1E-8. Closes #28386 (#28881) Hello, this PR closes #28386 by replacing `if np.isclose(slope, 0)` with `if slope == 0` in `lines.py`, allowing for better resolution of small slopes with `axline`s. Additionally, I have added the `test_line_slope` function in `test_lines.py` to ensure proper testing of this functionality. --- lib/matplotlib/lines.py | 2 +- lib/matplotlib/tests/test_lines.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index acaf6328ac49..9629a821368c 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -1521,7 +1521,7 @@ def get_transform(self): (vxlo, vylo), (vxhi, vyhi) = ax.transScale.transform(ax.viewLim) # General case: find intersections with view limits in either # direction, and draw between the middle two points. - if np.isclose(slope, 0): + if slope == 0: start = vxlo, y1 stop = vxhi, y1 elif np.isinf(slope): diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index 531237b2ba28..902b7aa2c02d 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -436,3 +436,14 @@ def test_axline_setters(): with pytest.raises(ValueError, match="Cannot set a 'slope' value while 'xy2' is set"): line2.set_slope(3) + + +def test_axline_small_slope(): + """Test that small slopes are not coerced to zero in the transform.""" + line = plt.axline((0, 0), slope=1e-14) + p1 = line.get_transform().transform_point((0, 0)) + p2 = line.get_transform().transform_point((1, 1)) + # y-values must be slightly different + dy = p2[1] - p1[1] + assert dy > 0 + assert dy < 4e-12 From 00f63b745f453f5e736b62d978df95ac9ee68f6b Mon Sep 17 00:00:00 2001 From: Kyra Cho Date: Fri, 27 Sep 2024 16:55:12 -0700 Subject: [PATCH 0299/1230] Backport PR #28881: Fix `axline` for slopes <= 1E-8. Closes #28386 --- lib/matplotlib/lines.py | 2 +- lib/matplotlib/tests/test_lines.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 72e74f4eb9c5..569669f76e8d 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -1520,7 +1520,7 @@ def get_transform(self): (vxlo, vylo), (vxhi, vyhi) = ax.transScale.transform(ax.viewLim) # General case: find intersections with view limits in either # direction, and draw between the middle two points. - if np.isclose(slope, 0): + if slope == 0: start = vxlo, y1 stop = vxhi, y1 elif np.isinf(slope): diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index 531237b2ba28..902b7aa2c02d 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -436,3 +436,14 @@ def test_axline_setters(): with pytest.raises(ValueError, match="Cannot set a 'slope' value while 'xy2' is set"): line2.set_slope(3) + + +def test_axline_small_slope(): + """Test that small slopes are not coerced to zero in the transform.""" + line = plt.axline((0, 0), slope=1e-14) + p1 = line.get_transform().transform_point((0, 0)) + p2 = line.get_transform().transform_point((1, 1)) + # y-values must be slightly different + dy = p2[1] - p1[1] + assert dy > 0 + assert dy < 4e-12 From ad717f6b0a2a64264f431d7eea0b30fcefec5ace Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 27 Sep 2024 18:46:42 +0200 Subject: [PATCH 0300/1230] DOC: Improve fancybox demo - Create subsections - The plot for the different boxstyle attributes had mixed mutation_scale with Axes aspect. This makes it more difficult to understand. Therefore, we now show the effect of mutation_scale in isoliation. - The aspect-correction topic is separated into an additional plot. --- .../shapes_and_collections/fancybox_demo.py | 84 ++++++++++++++----- 1 file changed, 63 insertions(+), 21 deletions(-) diff --git a/galleries/examples/shapes_and_collections/fancybox_demo.py b/galleries/examples/shapes_and_collections/fancybox_demo.py index 91cc1d1749ea..8d36a5a14d9d 100644 --- a/galleries/examples/shapes_and_collections/fancybox_demo.py +++ b/galleries/examples/shapes_and_collections/fancybox_demo.py @@ -3,7 +3,8 @@ Drawing fancy boxes =================== -The following examples show how to plot boxes with different visual properties. +The following examples show how to plot boxes (`.FancyBboxPatch`) with different +visual properties. """ import inspect @@ -15,7 +16,12 @@ import matplotlib.transforms as mtransforms # %% -# First we'll show some sample boxes with fancybox. +# Box styles +# ---------- +# `.FancyBboxPatch` supports different `.BoxStyle`\s. Note that `~.Axes.text` +# allows to draw a box around the text by adding the ``bbox`` parameter. Therefore, +# you don't see explicit `.FancyBboxPatch` and `.BoxStyle` calls in the following +# example. styles = mpatch.BoxStyle.get_styles() ncol = 2 @@ -41,13 +47,21 @@ # %% -# Next we'll show off multiple fancy boxes at once. - +# Parameters for modifying the box +# -------------------------------- +# `.BoxStyle`\s have additional parameters to configure their appearance. +# For example, "round" boxes can have ``pad`` and ``rounding``. +# +# Additionally, the `.FancyBboxPatch` parameters ``mutation_scale`` and +# ``mutation_aspect`` scale the box appearance. def add_fancy_patch_around(ax, bb, **kwargs): - fancy = FancyBboxPatch(bb.p0, bb.width, bb.height, - fc=(1, 0.8, 1, 0.5), ec=(1, 0.5, 1, 0.5), - **kwargs) + kwargs = { + 'facecolor': (1, 0.8, 1, 0.5), + 'edgecolor': (1, 0.5, 1, 0.5), + **kwargs + } + fancy = FancyBboxPatch(bb.p0, bb.width, bb.height, **kwargs) ax.add_patch(fancy) return fancy @@ -65,7 +79,7 @@ def draw_control_points_for_patches(ax): ax = axs[0, 0] # a fancy box with round corners. pad=0.1 -fancy = add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.1") +add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.1") ax.set(xlim=(0, 1), ylim=(0, 1), aspect=1, title='boxstyle="round,pad=0.1"') @@ -84,33 +98,61 @@ def draw_control_points_for_patches(ax): ax = axs[1, 0] # mutation_scale determines the overall scale of the mutation, i.e. both pad # and rounding_size is scaled according to this value. -fancy = add_fancy_patch_around( - ax, bb, boxstyle="round,pad=0.1", mutation_scale=2) +add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.1", mutation_scale=2) ax.set(xlim=(0, 1), ylim=(0, 1), aspect=1, title='boxstyle="round,pad=0.1"\n mutation_scale=2') ax = axs[1, 1] -# When the aspect ratio of the Axes is not 1, the fancy box may not be what you -# expected (green). -fancy = add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.2") -fancy.set(facecolor="none", edgecolor="green") -# You can compensate this by setting the mutation_aspect (pink). -fancy = add_fancy_patch_around( - ax, bb, boxstyle="round,pad=0.3", mutation_aspect=0.5) -ax.set(xlim=(-.5, 1.5), ylim=(0, 1), aspect=2, - title='boxstyle="round,pad=0.3"\nmutation_aspect=.5') +# mutation_aspect scales the vertical influence of the parameters (technically, +# it scales the height of the box down by mutation_aspect, applies the box parameters +# and scales the result back up). In effect, the vertical pad is scaled to +# pad * mutation_aspect, e.g. mutation_aspect=0.5 halves the vertical pad. +add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.1", mutation_aspect=0.5) +ax.set(xlim=(0, 1), ylim=(0, 1), + title='boxstyle="round,pad=0.1"\nmutation_aspect=0.5') for ax in axs.flat: draw_control_points_for_patches(ax) # Draw the original bbox (using boxstyle=square with pad=0). - fancy = add_fancy_patch_around(ax, bb, boxstyle="square,pad=0") - fancy.set(edgecolor="black", facecolor="none", zorder=10) + add_fancy_patch_around(ax, bb, boxstyle="square,pad=0", + edgecolor="black", facecolor="none", zorder=10) fig.tight_layout() plt.show() +# %% +# Creating visually constant padding on non-equal aspect Axes +# ----------------------------------------------------------- +# Since padding is in box coordinates, i.e. usually data coordinates, +# a given padding is rendered to different visual sizes if the +# Axes aspect is not 1. +# To get visually equal vertical and horizontal padding, set the +# mutation_aspect to the inverse of the Axes aspect. This scales +# the vertical padding appropriately. + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(6.5, 5)) + +# original boxes +bb = mtransforms.Bbox([[-0.5, -0.5], [0.5, 0.5]]) +add_fancy_patch_around(ax1, bb, boxstyle="square,pad=0", + edgecolor="black", facecolor="none", zorder=10) +add_fancy_patch_around(ax2, bb, boxstyle="square,pad=0", + edgecolor="black", facecolor="none", zorder=10) +ax1.set(xlim=(-1.5, 1.5), ylim=(-1.5, 1.5), aspect=2) +ax2.set(xlim=(-1.5, 1.5), ylim=(-1.5, 1.5), aspect=2) + + +fancy = add_fancy_patch_around( + ax1, bb, boxstyle="round,pad=0.5") +ax1.set_title("aspect=2\nmutation_aspect=1") + +fancy = add_fancy_patch_around( + ax2, bb, boxstyle="round,pad=0.5", mutation_aspect=0.5) +ax2.set_title("aspect=2\nmutation_aspect=0.5") + + # %% # # .. admonition:: References From f9fa77ba2ddd31bc53c3f3a5fbc38022da807184 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:56:35 +0000 Subject: [PATCH 0301/1230] Bump the actions group across 1 directory with 5 updates Bumps the actions group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) | `2.20.0` | `2.21.1` | | [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) | `1.4.1` | `1.4.3` | | [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) | `1.9.0` | `1.10.2` | | [scientific-python/upload-nightly-action](https://github.com/scientific-python/upload-nightly-action) | `0.5.0` | `0.6.1` | | [deadsnakes/action](https://github.com/deadsnakes/action) | `3.1.0` | `3.2.0` | Updates `pypa/cibuildwheel` from 2.20.0 to 2.21.1 - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/bd033a44476646b606efccdd5eed92d5ea1d77ad...d4a2945fcc8d13f20a1b99d461b8e844d5fc6e23) Updates `actions/attest-build-provenance` from 1.4.1 to 1.4.3 - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/310b0a4a3b0b78ef57ecda988ee04b132db73ef8...1c608d11d69870c2092266b3f9a6f3abbf17002c) Updates `pypa/gh-action-pypi-publish` from 1.9.0 to 1.10.2 - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0...897895f1e160c830e369f9779632ebc134688e1b) Updates `scientific-python/upload-nightly-action` from 0.5.0 to 0.6.1 - [Release notes](https://github.com/scientific-python/upload-nightly-action/releases) - [Commits](https://github.com/scientific-python/upload-nightly-action/compare/b67d7fcc0396e1128a474d1ab2b48aa94680f9fc...82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b) Updates `deadsnakes/action` from 3.1.0 to 3.2.0 - [Release notes](https://github.com/deadsnakes/action/releases) - [Commits](https://github.com/deadsnakes/action/compare/6c8b9b82fe0b4344f4b98f2775fcc395df45e494...e640ac8743173a67cca4d7d77cd837e514bf98e8) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: actions/attest-build-provenance dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: scientific-python/upload-nightly-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: deadsnakes/action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/cibuildwheel.yml | 14 +++++++------- .github/workflows/nightlies.yml | 2 +- .github/workflows/tests.yml | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 0db8c53b3a79..f72edc8e8e63 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -143,7 +143,7 @@ jobs: path: dist/ - name: Build wheels for CPython 3.13 - uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 + uses: pypa/cibuildwheel@d4a2945fcc8d13f20a1b99d461b8e844d5fc6e23 # v2.21.1 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -163,7 +163,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.12 - uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 + uses: pypa/cibuildwheel@d4a2945fcc8d13f20a1b99d461b8e844d5fc6e23 # v2.21.1 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -171,7 +171,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.11 - uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 + uses: pypa/cibuildwheel@d4a2945fcc8d13f20a1b99d461b8e844d5fc6e23 # v2.21.1 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -179,7 +179,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.10 - uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 + uses: pypa/cibuildwheel@d4a2945fcc8d13f20a1b99d461b8e844d5fc6e23 # v2.21.1 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -187,7 +187,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for PyPy - uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 + uses: pypa/cibuildwheel@d4a2945fcc8d13f20a1b99d461b8e844d5fc6e23 # v2.21.1 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -231,9 +231,9 @@ jobs: run: ls dist - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@310b0a4a3b0b78ef57ecda988ee04b132db73ef8 # v1.4.1 + uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 with: subject-path: dist/matplotlib-* - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # v1.9.0 + uses: pypa/gh-action-pypi-publish@897895f1e160c830e369f9779632ebc134688e1b # v1.10.2 diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 54e81f06b166..25e2bb344eda 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -59,7 +59,7 @@ jobs: ls -l dist/ - name: Upload wheels to Anaconda Cloud as nightlies - uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0 + uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4de46a1ed80f..062d742b81d9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -124,7 +124,7 @@ jobs: allow-prereleases: true - name: Set up Python ${{ matrix.python-version }} - uses: deadsnakes/action@6c8b9b82fe0b4344f4b98f2775fcc395df45e494 # v3.1.0 + uses: deadsnakes/action@e640ac8743173a67cca4d7d77cd837e514bf98e8 # v3.2.0 if: matrix.python-version == '3.13t' with: python-version: '3.13' From da2f54996c8b567183ac238f867c43d955820b0f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 24 Sep 2024 04:45:37 -0400 Subject: [PATCH 0302/1230] Remove 3.8 deprecations in toolkits --- .../next_api_changes/removals/28874-ES.rst | 33 ++++++++ .../api_changes_3.6.0/behaviour.rst | 2 +- .../api_changes_3.7.0/deprecations.rst | 2 +- .../api_changes_3.9.0/removals.rst | 2 +- doc/api/toolkits/axisartist.rst | 2 - .../axes_grid1/anchored_artists.py | 54 +------------ lib/mpl_toolkits/axes_grid1/axes_divider.py | 76 ------------------ lib/mpl_toolkits/axes_grid1/axes_grid.py | 5 -- lib/mpl_toolkits/axes_grid1/inset_locator.py | 47 +---------- .../test_axes_grid1/insetposition.png | Bin 1387 -> 0 bytes .../axes_grid1/tests/test_axes_grid1.py | 30 +++---- lib/mpl_toolkits/axisartist/axes_divider.py | 2 +- lib/mpl_toolkits/axisartist/axes_grid.py | 23 ------ lib/mpl_toolkits/axisartist/axes_rgb.py | 18 ----- lib/mpl_toolkits/axisartist/axislines.py | 4 - lib/mpl_toolkits/axisartist/floating_axes.py | 11 --- lib/mpl_toolkits/axisartist/meson.build | 2 - lib/mpl_toolkits/mplot3d/axis3d.py | 10 --- 18 files changed, 51 insertions(+), 272 deletions(-) create mode 100644 doc/api/next_api_changes/removals/28874-ES.rst delete mode 100644 lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/insetposition.png delete mode 100644 lib/mpl_toolkits/axisartist/axes_grid.py delete mode 100644 lib/mpl_toolkits/axisartist/axes_rgb.py diff --git a/doc/api/next_api_changes/removals/28874-ES.rst b/doc/api/next_api_changes/removals/28874-ES.rst new file mode 100644 index 000000000000..32c9ecb1ceaf --- /dev/null +++ b/doc/api/next_api_changes/removals/28874-ES.rst @@ -0,0 +1,33 @@ +``axes_grid1`` API changes +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``anchored_artists.AnchoredEllipse`` has been removed. Instead, directly construct an +`.AnchoredOffsetbox`, an `.AuxTransformBox`, and an `~.patches.Ellipse`, as demonstrated +in :doc:`/gallery/misc/anchored_artists`. + +The ``axes_divider.AxesLocator`` class has been removed. The ``new_locator`` method of +divider instances now instead returns an opaque callable (which can still be passed to +``ax.set_axes_locator``). + +``axes_divider.Divider.locate`` has been removed; use ``Divider.new_locator(...)(ax, +renderer)`` instead. + +``axes_grid.CbarAxesBase.toggle_label`` has been removed. Instead, use standard methods +for manipulating colorbar labels (`.Colorbar.set_label`) and tick labels +(`.Axes.tick_params`). + +``inset_location.InsetPosition`` has been removed; use `~.Axes.inset_axes` instead. + + +``axisartist`` API changes +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``axisartist.axes_grid`` and ``axisartist.axes_rgb`` modules, which provide wrappers +combining the functionality of `.axes_grid1` and `.axisartist`, have been removed; +directly use e.g. ``AxesGrid(..., axes_class=axislines.Axes)`` instead. + +Calling an axisartist Axes to mean `~matplotlib.pyplot.axis` has been removed; explicitly +call the method instead. + +``floating_axes.GridHelperCurveLinear.get_data_boundary`` has been removed. Use +``grid_finder.extreme_finder(*[None] * 5)`` to get the extremes of the grid. diff --git a/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst b/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst index 91802692ebb4..6ace010515fb 100644 --- a/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst +++ b/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst @@ -241,7 +241,7 @@ Specified exception types in ``Grid`` In a few cases an `Exception` was thrown when an incorrect argument value was set in the `mpl_toolkits.axes_grid1.axes_grid.Grid` (= -`mpl_toolkits.axisartist.axes_grid.Grid`) constructor. These are replaced as +``mpl_toolkits.axisartist.axes_grid.Grid``) constructor. These are replaced as follows: * Providing an incorrect value for *ngrids* now raises a `ValueError` diff --git a/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst index dd6d9d8e0894..55a0a7133c65 100644 --- a/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst +++ b/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst @@ -90,7 +90,7 @@ Passing undefined *label_mode* to ``Grid`` ... is deprecated. This includes `mpl_toolkits.axes_grid1.axes_grid.Grid`, `mpl_toolkits.axes_grid1.axes_grid.AxesGrid`, and `mpl_toolkits.axes_grid1.axes_grid.ImageGrid` as well as the corresponding -classes imported from `mpl_toolkits.axisartist.axes_grid`. +classes imported from ``mpl_toolkits.axisartist.axes_grid``. Pass ``label_mode='keep'`` instead to get the previous behavior of not modifying labels. diff --git a/doc/api/prev_api_changes/api_changes_3.9.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.9.0/removals.rst index b9aa03cfbf92..791c04149981 100644 --- a/doc/api/prev_api_changes/api_changes_3.9.0/removals.rst +++ b/doc/api/prev_api_changes/api_changes_3.9.0/removals.rst @@ -111,7 +111,7 @@ Passing undefined *label_mode* to ``Grid`` ... is no longer allowed. This includes `mpl_toolkits.axes_grid1.axes_grid.Grid`, `mpl_toolkits.axes_grid1.axes_grid.AxesGrid`, and `mpl_toolkits.axes_grid1.axes_grid.ImageGrid` as well as the corresponding classes -imported from `mpl_toolkits.axisartist.axes_grid`. +imported from ``mpl_toolkits.axisartist.axes_grid``. Pass ``label_mode='keep'`` instead to get the previous behavior of not modifying labels. diff --git a/doc/api/toolkits/axisartist.rst b/doc/api/toolkits/axisartist.rst index 8cac4d68a266..5f58d134d370 100644 --- a/doc/api/toolkits/axisartist.rst +++ b/doc/api/toolkits/axisartist.rst @@ -34,8 +34,6 @@ You can find a tutorial describing usage of axisartist at the axisartist.angle_helper axisartist.axes_divider - axisartist.axes_grid - axisartist.axes_rgb axisartist.axis_artist axisartist.axisline_style axisartist.axislines diff --git a/lib/mpl_toolkits/axes_grid1/anchored_artists.py b/lib/mpl_toolkits/axes_grid1/anchored_artists.py index 1238310b462b..214b15843ebf 100644 --- a/lib/mpl_toolkits/axes_grid1/anchored_artists.py +++ b/lib/mpl_toolkits/axes_grid1/anchored_artists.py @@ -1,12 +1,12 @@ -from matplotlib import _api, transforms +from matplotlib import transforms from matplotlib.offsetbox import (AnchoredOffsetbox, AuxTransformBox, DrawingArea, TextArea, VPacker) -from matplotlib.patches import (Rectangle, Ellipse, ArrowStyle, +from matplotlib.patches import (Rectangle, ArrowStyle, FancyArrowPatch, PathPatch) from matplotlib.text import TextPath __all__ = ['AnchoredDrawingArea', 'AnchoredAuxTransformBox', - 'AnchoredEllipse', 'AnchoredSizeBar', 'AnchoredDirectionArrows'] + 'AnchoredSizeBar', 'AnchoredDirectionArrows'] class AnchoredDrawingArea(AnchoredOffsetbox): @@ -124,54 +124,6 @@ def __init__(self, transform, loc, **kwargs) -@_api.deprecated("3.8") -class AnchoredEllipse(AnchoredOffsetbox): - def __init__(self, transform, width, height, angle, loc, - pad=0.1, borderpad=0.1, prop=None, frameon=True, **kwargs): - """ - Draw an anchored ellipse of a given size. - - Parameters - ---------- - transform : `~matplotlib.transforms.Transform` - The transformation object for the coordinate system in use, i.e., - :attr:`matplotlib.axes.Axes.transData`. - width, height : float - Width and height of the ellipse, given in coordinates of - *transform*. - angle : float - Rotation of the ellipse, in degrees, anti-clockwise. - loc : str - Location of the ellipse. Valid locations are - 'upper left', 'upper center', 'upper right', - 'center left', 'center', 'center right', - 'lower left', 'lower center', 'lower right'. - For backward compatibility, numeric values are accepted as well. - See the parameter *loc* of `.Legend` for details. - pad : float, default: 0.1 - Padding around the ellipse, in fraction of the font size. - borderpad : float, default: 0.1 - Border padding, in fraction of the font size. - frameon : bool, default: True - If True, draw a box around the ellipse. - prop : `~matplotlib.font_manager.FontProperties`, optional - Font property used as a reference for paddings. - **kwargs - Keyword arguments forwarded to `.AnchoredOffsetbox`. - - Attributes - ---------- - ellipse : `~matplotlib.patches.Ellipse` - Ellipse patch drawn. - """ - self._box = AuxTransformBox(transform) - self.ellipse = Ellipse((0, 0), width, height, angle=angle) - self._box.add_artist(self.ellipse) - - super().__init__(loc, pad=pad, borderpad=borderpad, child=self._box, - prop=prop, frameon=frameon, **kwargs) - - class AnchoredSizeBar(AnchoredOffsetbox): def __init__(self, transform, size, label, loc, pad=0.1, borderpad=0.1, sep=2, diff --git a/lib/mpl_toolkits/axes_grid1/axes_divider.py b/lib/mpl_toolkits/axes_grid1/axes_divider.py index f6c38f35dbc4..50365f482b72 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_divider.py +++ b/lib/mpl_toolkits/axes_grid1/axes_divider.py @@ -199,31 +199,6 @@ def new_locator(self, nx, ny, nx1=None, ny1=None): locator.get_subplotspec = self.get_subplotspec return locator - @_api.deprecated( - "3.8", alternative="divider.new_locator(...)(ax, renderer)") - def locate(self, nx, ny, nx1=None, ny1=None, axes=None, renderer=None): - """ - Implementation of ``divider.new_locator().__call__``. - - Parameters - ---------- - nx, nx1 : int - Integers specifying the column-position of the cell. When *nx1* is - None, a single *nx*-th column is specified. Otherwise, the - location of columns spanning between *nx* to *nx1* (but excluding - *nx1*-th column) is specified. - ny, ny1 : int - Same as *nx* and *nx1*, but for row positions. - axes - renderer - """ - xref = self._xrefindex - yref = self._yrefindex - return self._locate( - nx - xref, (nx + 1 if nx1 is None else nx1) - xref, - ny - yref, (ny + 1 if ny1 is None else ny1) - yref, - axes, renderer) - def _locate(self, nx, ny, nx1, ny1, axes, renderer): """ Implementation of ``divider.new_locator().__call__``. @@ -305,57 +280,6 @@ def add_auto_adjustable_area(self, use_axes, pad=0.1, adjust_dirs=None): self.append_size(d, Size._AxesDecorationsSize(use_axes, d) + pad) -@_api.deprecated("3.8") -class AxesLocator: - """ - A callable object which returns the position and size of a given - `.AxesDivider` cell. - """ - - def __init__(self, axes_divider, nx, ny, nx1=None, ny1=None): - """ - Parameters - ---------- - axes_divider : `~mpl_toolkits.axes_grid1.axes_divider.AxesDivider` - nx, nx1 : int - Integers specifying the column-position of the - cell. When *nx1* is None, a single *nx*-th column is - specified. Otherwise, location of columns spanning between *nx* - to *nx1* (but excluding *nx1*-th column) is specified. - ny, ny1 : int - Same as *nx* and *nx1*, but for row positions. - """ - self._axes_divider = axes_divider - - _xrefindex = axes_divider._xrefindex - _yrefindex = axes_divider._yrefindex - - self._nx, self._ny = nx - _xrefindex, ny - _yrefindex - - if nx1 is None: - nx1 = len(self._axes_divider) - if ny1 is None: - ny1 = len(self._axes_divider[0]) - - self._nx1 = nx1 - _xrefindex - self._ny1 = ny1 - _yrefindex - - def __call__(self, axes, renderer): - - _xrefindex = self._axes_divider._xrefindex - _yrefindex = self._axes_divider._yrefindex - - return self._axes_divider.locate(self._nx + _xrefindex, - self._ny + _yrefindex, - self._nx1 + _xrefindex, - self._ny1 + _yrefindex, - axes, - renderer) - - def get_subplotspec(self): - return self._axes_divider.get_subplotspec() - - class SubplotDivider(Divider): """ The Divider class whose rectangle area is specified as a subplot geometry. diff --git a/lib/mpl_toolkits/axes_grid1/axes_grid.py b/lib/mpl_toolkits/axes_grid1/axes_grid.py index b5663364481e..20abf18ea79c 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_grid.py +++ b/lib/mpl_toolkits/axes_grid1/axes_grid.py @@ -20,11 +20,6 @@ def colorbar(self, mappable, **kwargs): return self.get_figure(root=False).colorbar( mappable, cax=self, location=self.orientation, **kwargs) - @_api.deprecated("3.8", alternative="ax.tick_params and colorbar.set_label") - def toggle_label(self, b): - axis = self.axis[self.orientation] - axis.toggle(ticklabels=b, label=b) - _cbaraxes_class_factory = cbook._make_class_factory(CbarAxesBase, "Cbar{}") diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index 303dbbb0721e..52fe6efc0618 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -6,58 +6,13 @@ from matplotlib.offsetbox import AnchoredOffsetbox from matplotlib.patches import Patch, Rectangle from matplotlib.path import Path -from matplotlib.transforms import Bbox, BboxTransformTo +from matplotlib.transforms import Bbox from matplotlib.transforms import IdentityTransform, TransformedBbox from . import axes_size as Size from .parasite_axes import HostAxes -@_api.deprecated("3.8", alternative="Axes.inset_axes") -class InsetPosition: - @_docstring.interpd - def __init__(self, parent, lbwh): - """ - An object for positioning an inset axes. - - This is created by specifying the normalized coordinates in the axes, - instead of the figure. - - Parameters - ---------- - parent : `~matplotlib.axes.Axes` - Axes to use for normalizing coordinates. - - lbwh : iterable of four floats - The left edge, bottom edge, width, and height of the inset axes, in - units of the normalized coordinate of the *parent* axes. - - See Also - -------- - :meth:`matplotlib.axes.Axes.set_axes_locator` - - Examples - -------- - The following bounds the inset axes to a box with 20%% of the parent - axes height and 40%% of the width. The size of the axes specified - ([0, 0, 1, 1]) ensures that the axes completely fills the bounding box: - - >>> parent_axes = plt.gca() - >>> ax_ins = plt.axes([0, 0, 1, 1]) - >>> ip = InsetPosition(parent_axes, [0.5, 0.1, 0.4, 0.2]) - >>> ax_ins.set_axes_locator(ip) - """ - self.parent = parent - self.lbwh = lbwh - - def __call__(self, ax, renderer): - bbox_parent = self.parent.get_position(original=False) - trans = BboxTransformTo(bbox_parent) - bbox_inset = Bbox.from_bounds(*self.lbwh) - bb = TransformedBbox(bbox_inset, trans) - return bb - - class AnchoredLocatorBase(AnchoredOffsetbox): def __init__(self, bbox_to_anchor, offsetbox, loc, borderpad=0.5, bbox_transform=None): diff --git a/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/insetposition.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/insetposition.png deleted file mode 100644 index e8676cfd6c9528f7e002710ca048d833b19f9b72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1387 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yg_pQSlmzFem6RtIr84*?mK5aV zm*iw7DU_ua6=&w>8S9zp8R(^?mKmFx8EL1RCYzXAni`pzC+npc85kKESQ?t@C}fnB z6ck(O>*vC?>*W`v>+fMYs06fvv%n*=n1O*m5ri36*e}myU|`wh>EaktG3V`F!@gTt z5^Wdt!?!P7l*pKuIoXIifp>AD@R}Cih*_o^I%ZD0#=T%>*QMje`z@UFktuHb+eYOyqRP6 z#X|1J$BH%UUT1BMx|+0c$2rg6jazeXhfh6RxOs8z?bUnh+5dB2GVu)!-I^PH`f1UN z!u>aL%(mXm%eTA!=Jxq}@4s)2Tfcw*^L2?I9Q?M2o_@N?uKu6RmmTqEo_`K}Hp}+h zn^n7Bt=+@_65>XKpAPfp%{x=K;O2ETfz9fszi+9@bHq3i<#ZgLY_`AqH)Zq9D>Zii zU#9!}`%kVqA+i1Qr%yrp`ug^N-~9di_f@go*V_B>f7jSoR9COokKcFY_3PIw`j4dl zYO7ek^Jo)sA%|vt-nSd`4Q2S+Z@n$MYT|n~ZSzL`=Xo;y$6xJQ7q&W7SFC%ZetO7c zzLf2^uU74q`)%|t?Dn>=fBx8fOYO^WzcCC%ln_mA%VdKvlyHnZ6=f#HRd7o}C>3~G`33k28Uw@YRuahq} zyU+22nW-}88~a1OyIJ)UX0vX(oA;Lg(A7Wh?5|(`r*Qp=7&jz=sO`P-Mn|s>ZBk&}m4+7a6b;4}UCmnsG$h0xyqfNJ{F>Vb_?Gaf?y4NbN zC8hEUc5ID!9@#zhrhLoplzIVN*UUhRnoo`Y z>shN-U+=S>ozL=#vC+m1YS6(Y(`Nxge$}d5@4wgA^nL&Sy{fu;Hz+)7f7Q;dtMAXb z)L0T59vb@k>#s$hV>ao%ykzQ|eD2+@b^Gf6R$a7wcXtVM6&=rPWhs*V=Dpzj=SJe8lZ-wV{?W=GM2i?Y;H8{KH>IzINxgW!BYGjd#d@FOSy| s3%7j!pz(n1XB02siF}aboBlH}1W9dw Date: Tue, 24 Sep 2024 22:17:04 -0400 Subject: [PATCH 0303/1230] Remove 3.8 deprecations in backends --- .../next_api_changes/removals/28874-ES.rst | 63 +++++++++++++++++++ lib/matplotlib/backend_bases.py | 38 +---------- lib/matplotlib/backend_bases.pyi | 5 +- lib/matplotlib/backends/backend_agg.py | 14 ----- lib/matplotlib/backends/backend_pdf.py | 22 +------ lib/matplotlib/backends/backend_pgf.py | 22 +------ lib/matplotlib/backends/backend_ps.py | 22 +------ lib/matplotlib/backends/backend_qt.py | 4 -- lib/matplotlib/pyplot.py | 8 --- lib/matplotlib/rcsetup.py | 17 +---- lib/matplotlib/tests/test_backend_pdf.py | 38 ++--------- lib/matplotlib/tests/test_backend_pgf.py | 38 ++--------- lib/matplotlib/tests/test_backend_ps.py | 6 +- lib/matplotlib/tests/test_pyplot.py | 5 +- 14 files changed, 88 insertions(+), 214 deletions(-) diff --git a/doc/api/next_api_changes/removals/28874-ES.rst b/doc/api/next_api_changes/removals/28874-ES.rst index 32c9ecb1ceaf..d5bdaeef6458 100644 --- a/doc/api/next_api_changes/removals/28874-ES.rst +++ b/doc/api/next_api_changes/removals/28874-ES.rst @@ -1,3 +1,66 @@ +Auto-closing of figures when switching backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Allowable backend switches (i.e. those that do not swap a GUI event loop with another +one) will not close existing figures. If necessary, call ``plt.close("all")`` before +switching. + + +``FigureCanvasBase.switch_backends`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed with no replacement. + + +Accessing ``event.guiEvent`` after event handlers return +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... is no longer supported, and ``event.guiEvent`` will be set to None once the event +handlers return. For some GUI toolkits, it is unsafe to use the event, though you may +separately stash the object at your own risk. + + +``PdfPages(keep_empty=True)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A zero-page PDF is not valid, thus passing ``keep_empty=True`` to `.backend_pdf.PdfPages` +and `.backend_pgf.PdfPages`, and the ``keep_empty`` attribute of these classes, is no +longer allowed, and empty PDF files will not be created. + +Furthermore, `.backend_pdf.PdfPages` no longer immediately creates the target file upon +instantiation, but only when the first figure is saved. To fully control file creation, +directly pass an opened file object as argument (``with open(path, "wb") as file, +PdfPages(file) as pdf: ...``). + + +``backend_ps.psDefs`` +~~~~~~~~~~~~~~~~~~~~~ + +The ``psDefs`` module-level variable in ``backend_ps`` has been removed with no +replacement. + + +Automatic papersize selection in PostScript +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Setting :rc:`ps.papersize` to ``'auto'`` or passing ``papersize='auto'`` to +`.Figure.savefig` is no longer supported. Either pass an explicit paper type name, or +omit this parameter to use the default from the rcParam. + + +``RendererAgg.tostring_rgb`` and ``FigureCanvasAgg.tostring_rgb`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... have been remove with no direct replacement. Consider using ``buffer_rgba`` instead, +which should cover most use cases. + + +``NavigationToolbar2QT.message`` has been removed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... with no replacement. + + ``axes_grid1`` API changes ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 817eb51705fe..95ed49612b35 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1178,26 +1178,12 @@ class Event: def __init__(self, name, canvas, guiEvent=None): self.name = name self.canvas = canvas - self._guiEvent = guiEvent - self._guiEvent_deleted = False + self.guiEvent = guiEvent def _process(self): """Process this event on ``self.canvas``, then unset ``guiEvent``.""" self.canvas.callbacks.process(self.name, self) - self._guiEvent_deleted = True - - @property - def guiEvent(self): - # After deprecation elapses: remove _guiEvent_deleted; make guiEvent a plain - # attribute set to None by _process. - if self._guiEvent_deleted: - _api.warn_deprecated( - "3.8", message="Accessing guiEvent outside of the original GUI event " - "handler is unsafe and deprecated since %(since)s; in the future, the " - "attribute will be set to None after quitting the event handler. You " - "may separately record the value of the guiEvent attribute at your own " - "risk.") - return self._guiEvent + self.guiEvent = None class DrawEvent(Event): @@ -2097,12 +2083,6 @@ def print_figure( if dpi == 'figure': dpi = getattr(self.figure, '_original_dpi', self.figure.dpi) - if kwargs.get("papertype") == 'auto': - # When deprecation elapses, remove backend_ps._get_papertype & its callers. - _api.warn_deprecated( - "3.8", name="papertype='auto'", addendum="Pass an explicit paper type, " - "'figure', or omit the *papertype* argument entirely.") - # Remove the figure manager, if any, to avoid resizing the GUI widget. with (cbook._setattr_cm(self, manager=None), self._switch_canvas_and_return_print_method(format, backend) @@ -2207,20 +2187,6 @@ def get_default_filename(self): default_filetype = self.get_default_filetype() return f'{default_basename}.{default_filetype}' - @_api.deprecated("3.8") - def switch_backends(self, FigureCanvasClass): - """ - Instantiate an instance of FigureCanvasClass - - This is used for backend switching, e.g., to instantiate a - FigureCanvasPS from a FigureCanvasGTK. Note, deep copying is - not done, so any changes to one of the instances (e.g., setting - figure size or line props), will be reflected in the other - """ - newCanvas = FigureCanvasClass(self.figure) - newCanvas._is_saving = self._is_saving - return newCanvas - def mpl_connect(self, s, func): """ Bind function *func* to event *s*. diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index c2fc61e386d8..70be504666fc 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -199,13 +199,11 @@ class TimerBase: class Event: name: str canvas: FigureCanvasBase + guiEvent: Any def __init__( self, name: str, canvas: FigureCanvasBase, guiEvent: Any | None = ... ) -> None: ... - @property - def guiEvent(self) -> Any: ... - class DrawEvent(Event): renderer: RendererBase def __init__( @@ -348,7 +346,6 @@ class FigureCanvasBase: def get_default_filetype(cls) -> str: ... def get_default_filename(self) -> str: ... _T = TypeVar("_T", bound=FigureCanvasBase) - def switch_backends(self, FigureCanvasClass: type[_T]) -> _T: ... def mpl_connect(self, s: str, func: Callable[[Event], Any]) -> int: ... def mpl_disconnect(self, cid: int) -> None: ... def new_timer( diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 92253c02c1b5..ae361f0cceb4 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -266,10 +266,6 @@ def buffer_rgba(self): def tostring_argb(self): return np.asarray(self._renderer).take([3, 0, 1, 2], axis=2).tobytes() - @_api.deprecated("3.8", alternative="buffer_rgba") - def tostring_rgb(self): - return np.asarray(self._renderer).take([0, 1, 2], axis=2).tobytes() - def clear(self): self._renderer.clear() @@ -398,16 +394,6 @@ def get_renderer(self): self._lastKey = key return self.renderer - @_api.deprecated("3.8", alternative="buffer_rgba") - def tostring_rgb(self): - """ - Get the image as RGB `bytes`. - - `draw` must be called at least once before this function will work and - to update the renderer for any subsequent changes to the Figure. - """ - return self.renderer.tostring_rgb() - def tostring_argb(self): """ Get the image as ARGB `bytes`. diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 7e3e09f034f5..9c542e2a8f8e 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2663,9 +2663,9 @@ class PdfPages: confusion when using `~.pyplot.savefig` and forgetting the format argument. """ - _UNSET = object() - - def __init__(self, filename, keep_empty=_UNSET, metadata=None): + @_api.delete_parameter("3.10", "keep_empty", + addendum="This parameter does nothing.") + def __init__(self, filename, keep_empty=None, metadata=None): """ Create a new PdfPages object. @@ -2676,10 +2676,6 @@ def __init__(self, filename, keep_empty=_UNSET, metadata=None): The file is opened when a figure is saved for the first time (overwriting any older file with the same name). - keep_empty : bool, optional - If set to False, then empty pdf files will be deleted automatically - when closed. - metadata : dict, optional Information dictionary object (see PDF reference section 10.2.1 'Document Information Dictionary'), e.g.: @@ -2693,13 +2689,6 @@ def __init__(self, filename, keep_empty=_UNSET, metadata=None): self._filename = filename self._metadata = metadata self._file = None - if keep_empty and keep_empty is not self._UNSET: - _api.warn_deprecated("3.8", message=( - "Keeping empty pdf files is deprecated since %(since)s and support " - "will be removed %(removal)s.")) - self._keep_empty = keep_empty - - keep_empty = _api.deprecate_privatize_attribute("3.8") def __enter__(self): return self @@ -2721,11 +2710,6 @@ def close(self): self._file.finalize() self._file.close() self._file = None - elif self._keep_empty: # True *or* UNSET. - _api.warn_deprecated("3.8", message=( - "Keeping empty pdf files is deprecated since %(since)s and support " - "will be removed %(removal)s.")) - PdfFile(self._filename, metadata=self._metadata).close() # touch the file. def infodict(self): """ diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index daefdb0640ca..48b6e8ac152c 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -14,7 +14,7 @@ from PIL import Image import matplotlib as mpl -from matplotlib import _api, cbook, font_manager as fm +from matplotlib import cbook, font_manager as fm from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, RendererBase ) @@ -898,9 +898,7 @@ class PdfPages: ... pdf.savefig() """ - _UNSET = object() - - def __init__(self, filename, *, keep_empty=_UNSET, metadata=None): + def __init__(self, filename, *, metadata=None): """ Create a new PdfPages object. @@ -910,10 +908,6 @@ def __init__(self, filename, *, keep_empty=_UNSET, metadata=None): Plots using `PdfPages.savefig` will be written to a file at this location. Any older file with the same name is overwritten. - keep_empty : bool, default: True - If set to False, then empty pdf files will be deleted automatically - when closed. - metadata : dict, optional Information dictionary object (see PDF reference section 10.2.1 'Document Information Dictionary'), e.g.: @@ -929,17 +923,10 @@ def __init__(self, filename, *, keep_empty=_UNSET, metadata=None): """ self._output_name = filename self._n_figures = 0 - if keep_empty and keep_empty is not self._UNSET: - _api.warn_deprecated("3.8", message=( - "Keeping empty pdf files is deprecated since %(since)s and support " - "will be removed %(removal)s.")) - self._keep_empty = keep_empty self._metadata = (metadata or {}).copy() self._info_dict = _create_pdf_info_dict('pgf', self._metadata) self._file = BytesIO() - keep_empty = _api.deprecate_privatize_attribute("3.8") - def _write_header(self, width_inches, height_inches): pdfinfo = ','.join( _metadata_to_str(k, v) for k, v in self._info_dict.items()) @@ -969,11 +956,6 @@ def close(self): self._file.write(rb'\end{document}\n') if self._n_figures > 0: self._run_latex() - elif self._keep_empty: - _api.warn_deprecated("3.8", message=( - "Keeping empty pdf files is deprecated since %(since)s and support " - "will be removed %(removal)s.")) - open(self._output_name, 'wb').close() self._file.close() def _run_latex(self): diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 5f224f38af1e..4f4c27cce955 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -39,12 +39,6 @@ debugPS = False -@_api.caching_module_getattr -class __getattr__: - # module-level deprecations - psDefs = _api.deprecated("3.8", obj_type="")(property(lambda self: _psDefs)) - - papersize = {'letter': (8.5, 11), 'legal': (8.5, 14), 'ledger': (11, 17), @@ -72,15 +66,6 @@ class __getattr__: 'b10': (1.26, 1.76)} -def _get_papertype(w, h): - for key, (pw, ph) in sorted(papersize.items(), reverse=True): - if key.startswith('l'): - continue - if w < pw and h < ph: - return key - return 'a0' - - def _nums_to_str(*args, sep=" "): return sep.join(f"{arg:1.3f}".rstrip("0").rstrip(".") for arg in args) @@ -828,7 +813,7 @@ def _print_ps( if papertype is None: papertype = mpl.rcParams['ps.papersize'] papertype = papertype.lower() - _api.check_in_list(['figure', 'auto', *papersize], papertype=papertype) + _api.check_in_list(['figure', *papersize], papertype=papertype) orientation = _api.check_getitem( _Orientation, orientation=orientation.lower()) @@ -858,9 +843,6 @@ def _print_figure( # find the appropriate papertype width, height = self.figure.get_size_inches() - if papertype == 'auto': - papertype = _get_papertype(*orientation.swap_if_landscape((width, height))) - if is_eps or papertype == 'figure': paper_width, paper_height = width, height else: @@ -1041,8 +1023,6 @@ def _print_figure_tex( paper_width, paper_height = orientation.swap_if_landscape( self.figure.get_size_inches()) else: - if papertype == 'auto': - papertype = _get_papertype(width, height) paper_width, paper_height = papersize[papertype] psfrag_rotated = _convert_psfrags( diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index e693811df4f0..bc37a15c7a67 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -658,9 +658,6 @@ def set_window_title(self, title): class NavigationToolbar2QT(NavigationToolbar2, QtWidgets.QToolBar): - _message = QtCore.Signal(str) # Remove once deprecation below elapses. - message = _api.deprecate_privatize_attribute("3.8") - toolitems = [*NavigationToolbar2.toolitems] toolitems.insert( # Add 'customize' action after 'subplots' @@ -783,7 +780,6 @@ def zoom(self, *args): self._update_buttons_checked() def set_message(self, s): - self._message.emit(s) if self.coordinates: self.locLabel.setText(s) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 744eee0e4b9f..69c80e6d3579 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -514,14 +514,6 @@ def draw_if_interactive() -> None: # See https://github.com/matplotlib/matplotlib/issues/6092 matplotlib.backends.backend = newbackend # type: ignore[attr-defined] - if not cbook._str_equal(old_backend, newbackend): - if get_fignums(): - _api.warn_deprecated("3.8", message=( - "Auto-close()ing of figures upon backend switching is deprecated since " - "%(since)s and will be removed %(removal)s. To suppress this warning, " - "explicitly call plt.close('all') first.")) - close("all") - # Make sure the repl display hook is installed in case we become interactive. install_repl_displayhook() diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index e84b0539385b..308e02fca72b 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -463,19 +463,6 @@ def validate_ps_distiller(s): return ValidateInStrings('ps.usedistiller', ['ghostscript', 'xpdf'])(s) -def _validate_papersize(s): - # Re-inline this validator when the 'auto' deprecation expires. - s = ValidateInStrings("ps.papersize", - ["figure", "auto", "letter", "legal", "ledger", - *[f"{ab}{i}" for ab in "ab" for i in range(11)]], - ignorecase=True)(s) - if s == "auto": - _api.warn_deprecated("3.8", name="ps.papersize='auto'", - addendum="Pass an explicit paper type, figure, or omit " - "the *ps.papersize* rcParam entirely.") - return s - - # A validator dedicated to the named line styles, based on the items in # ls_mapper, and a list of possible strings read from Line2D.set_linestyle _validate_named_linestyle = ValidateInStrings( @@ -1291,7 +1278,9 @@ def _convert_validator_spec(key, conv): "tk.window_focus": validate_bool, # Maintain shell focus for TkAgg # Set the papersize/type - "ps.papersize": _validate_papersize, + "ps.papersize": _ignorecase( + ["figure", "letter", "legal", "ledger", + *[f"{ab}{i}" for ab in "ab" for i in range(11)]]), "ps.useafm": validate_bool, # use ghostscript or xpdf to distill ps output "ps.usedistiller": validate_ps_distiller, diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 6a6dc1a6bac1..3fcf124e364d 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -81,48 +81,18 @@ def test_multipage_properfinalize(): def test_multipage_keep_empty(tmp_path): - # test empty pdf files - - # an empty pdf is left behind with keep_empty unset + # An empty pdf deletes itself afterwards. fn = tmp_path / "a.pdf" - with pytest.warns(mpl.MatplotlibDeprecationWarning), PdfPages(fn) as pdf: - pass - assert fn.exists() - - # an empty pdf is left behind with keep_empty=True - fn = tmp_path / "b.pdf" - with (pytest.warns(mpl.MatplotlibDeprecationWarning), - PdfPages(fn, keep_empty=True) as pdf): - pass - assert fn.exists() - - # an empty pdf deletes itself afterwards with keep_empty=False - fn = tmp_path / "c.pdf" - with PdfPages(fn, keep_empty=False) as pdf: + with PdfPages(fn) as pdf: pass assert not fn.exists() - # test pdf files with content, they should never be deleted - - # a non-empty pdf is left behind with keep_empty unset - fn = tmp_path / "d.pdf" + # Test pdf files with content, they should never be deleted. + fn = tmp_path / "b.pdf" with PdfPages(fn) as pdf: pdf.savefig(plt.figure()) assert fn.exists() - # a non-empty pdf is left behind with keep_empty=True - fn = tmp_path / "e.pdf" - with (pytest.warns(mpl.MatplotlibDeprecationWarning), - PdfPages(fn, keep_empty=True) as pdf): - pdf.savefig(plt.figure()) - assert fn.exists() - - # a non-empty pdf is left behind with keep_empty=False - fn = tmp_path / "f.pdf" - with PdfPages(fn, keep_empty=False) as pdf: - pdf.savefig(plt.figure()) - assert fn.exists() - def test_composite_image(): # Test that figures can be saved with and without combining multiple images diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index 54b1c3b5896e..e218a81cdceb 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -288,48 +288,18 @@ def test_pdf_pages_metadata_check(monkeypatch, system): @needs_pgf_xelatex def test_multipage_keep_empty(tmp_path): - # test empty pdf files - - # an empty pdf is left behind with keep_empty unset + # An empty pdf deletes itself afterwards. fn = tmp_path / "a.pdf" - with pytest.warns(mpl.MatplotlibDeprecationWarning), PdfPages(fn) as pdf: - pass - assert fn.exists() - - # an empty pdf is left behind with keep_empty=True - fn = tmp_path / "b.pdf" - with (pytest.warns(mpl.MatplotlibDeprecationWarning), - PdfPages(fn, keep_empty=True) as pdf): - pass - assert fn.exists() - - # an empty pdf deletes itself afterwards with keep_empty=False - fn = tmp_path / "c.pdf" - with PdfPages(fn, keep_empty=False) as pdf: + with PdfPages(fn) as pdf: pass assert not fn.exists() - # test pdf files with content, they should never be deleted - - # a non-empty pdf is left behind with keep_empty unset - fn = tmp_path / "d.pdf" + # Test pdf files with content, they should never be deleted. + fn = tmp_path / "b.pdf" with PdfPages(fn) as pdf: pdf.savefig(plt.figure()) assert fn.exists() - # a non-empty pdf is left behind with keep_empty=True - fn = tmp_path / "e.pdf" - with (pytest.warns(mpl.MatplotlibDeprecationWarning), - PdfPages(fn, keep_empty=True) as pdf): - pdf.savefig(plt.figure()) - assert fn.exists() - - # a non-empty pdf is left behind with keep_empty=False - fn = tmp_path / "f.pdf" - with PdfPages(fn, keep_empty=False) as pdf: - pdf.savefig(plt.figure()) - assert fn.exists() - @needs_pgf_xelatex def test_tex_restart_after_error(): diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index c587a00c0af9..cc968795802e 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -371,10 +371,10 @@ def test_colorbar_shift(tmp_path): plt.colorbar() -def test_auto_papersize_deprecation(): +def test_auto_papersize_removal(): fig = plt.figure() - with pytest.warns(mpl.MatplotlibDeprecationWarning): + with pytest.raises(ValueError, match="'auto' is not a valid value"): fig.savefig(io.BytesIO(), format='eps', papertype='auto') - with pytest.warns(mpl.MatplotlibDeprecationWarning): + with pytest.raises(ValueError, match="'auto' is not a valid value"): mpl.rcParams['ps.papersize'] = 'auto' diff --git a/lib/matplotlib/tests/test_pyplot.py b/lib/matplotlib/tests/test_pyplot.py index 63dc239df2e8..21036e177045 100644 --- a/lib/matplotlib/tests/test_pyplot.py +++ b/lib/matplotlib/tests/test_pyplot.py @@ -439,9 +439,8 @@ def test_switch_backend_no_close(): assert len(plt.get_fignums()) == 2 plt.switch_backend('agg') assert len(plt.get_fignums()) == 2 - with pytest.warns(mpl.MatplotlibDeprecationWarning): - plt.switch_backend('svg') - assert len(plt.get_fignums()) == 0 + plt.switch_backend('svg') + assert len(plt.get_fignums()) == 2 def figure_hook_example(figure): From cf04022f427149f74a612161741de216812756a3 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 24 Sep 2024 22:28:09 -0400 Subject: [PATCH 0304/1230] Remove 3.8 cbook deprecations --- .../next_api_changes/removals/28874-ES.rst | 13 ++ lib/matplotlib/backend_tools.pyi | 4 +- lib/matplotlib/cbook.py | 120 +----------------- lib/matplotlib/cbook.pyi | 25 +--- 4 files changed, 23 insertions(+), 139 deletions(-) diff --git a/doc/api/next_api_changes/removals/28874-ES.rst b/doc/api/next_api_changes/removals/28874-ES.rst index d5bdaeef6458..0be06b000096 100644 --- a/doc/api/next_api_changes/removals/28874-ES.rst +++ b/doc/api/next_api_changes/removals/28874-ES.rst @@ -61,6 +61,19 @@ which should cover most use cases. ... with no replacement. +``cbook`` API changes +~~~~~~~~~~~~~~~~~~~~~ + +``cbook.Stack`` has been removed with no replacement. + +``Grouper.clean()`` has been removed with no replacement. The Grouper class now cleans +itself up automatically. + +The *np_load* parameter of ``cbook.get_sample_data`` has been removed; `.get_sample_data` +now auto-loads numpy arrays. Use ``get_sample_data(..., asfileobj=False)`` instead to get +the filename of the data file, which can then be passed to `open`, if desired. + + ``axes_grid1`` API changes ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/lib/matplotlib/backend_tools.pyi b/lib/matplotlib/backend_tools.pyi index 446f713292e1..f86a207c7545 100644 --- a/lib/matplotlib/backend_tools.pyi +++ b/lib/matplotlib/backend_tools.pyi @@ -75,8 +75,8 @@ class ToolXScale(AxisScaleBase): def set_scale(self, ax, scale: str | ScaleBase) -> None: ... class ToolViewsPositions(ToolBase): - views: dict[Figure | Axes, cbook.Stack] - positions: dict[Figure | Axes, cbook.Stack] + views: dict[Figure | Axes, cbook._Stack] + positions: dict[Figure | Axes, cbook._Stack] home_views: dict[Figure, dict[Axes, tuple[float, float, float, float]]] def add_figure(self, figure: Figure) -> None: ... def clear(self, figure: Figure) -> None: ... diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 7cf32c4d5f6a..fe556487410f 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -543,9 +543,7 @@ def is_scalar_or_string(val): return isinstance(val, str) or not np.iterable(val) -@_api.delete_parameter( - "3.8", "np_load", alternative="open(get_sample_data(..., asfileobj=False))") -def get_sample_data(fname, asfileobj=True, *, np_load=True): +def get_sample_data(fname, asfileobj=True): """ Return a sample data file. *fname* is a path relative to the :file:`mpl-data/sample_data` directory. If *asfileobj* is `True` @@ -564,10 +562,7 @@ def get_sample_data(fname, asfileobj=True, *, np_load=True): if suffix == '.gz': return gzip.open(path) elif suffix in ['.npy', '.npz']: - if np_load: - return np.load(path) - else: - return path.open('rb') + return np.load(path) elif suffix in ['.csv', '.xrc', '.txt']: return path.open('r') else: @@ -607,113 +602,6 @@ def flatten(seq, scalarp=is_scalar_or_string): yield from flatten(item, scalarp) -@_api.deprecated("3.8") -class Stack: - """ - Stack of elements with a movable cursor. - - Mimics home/back/forward in a web browser. - """ - - def __init__(self, default=None): - self.clear() - self._default = default - - def __call__(self): - """Return the current element, or None.""" - if not self._elements: - return self._default - else: - return self._elements[self._pos] - - def __len__(self): - return len(self._elements) - - def __getitem__(self, ind): - return self._elements[ind] - - def forward(self): - """Move the position forward and return the current element.""" - self._pos = min(self._pos + 1, len(self._elements) - 1) - return self() - - def back(self): - """Move the position back and return the current element.""" - if self._pos > 0: - self._pos -= 1 - return self() - - def push(self, o): - """ - Push *o* to the stack at current position. Discard all later elements. - - *o* is returned. - """ - self._elements = self._elements[:self._pos + 1] + [o] - self._pos = len(self._elements) - 1 - return self() - - def home(self): - """ - Push the first element onto the top of the stack. - - The first element is returned. - """ - if not self._elements: - return - self.push(self._elements[0]) - return self() - - def empty(self): - """Return whether the stack is empty.""" - return len(self._elements) == 0 - - def clear(self): - """Empty the stack.""" - self._pos = -1 - self._elements = [] - - def bubble(self, o): - """ - Raise all references of *o* to the top of the stack, and return it. - - Raises - ------ - ValueError - If *o* is not in the stack. - """ - if o not in self._elements: - raise ValueError('Given element not contained in the stack') - old_elements = self._elements.copy() - self.clear() - top_elements = [] - for elem in old_elements: - if elem == o: - top_elements.append(elem) - else: - self.push(elem) - for _ in top_elements: - self.push(o) - return o - - def remove(self, o): - """ - Remove *o* from the stack. - - Raises - ------ - ValueError - If *o* is not in the stack. - """ - if o not in self._elements: - raise ValueError('Given element not contained in the stack') - old_elements = self._elements.copy() - self.clear() - for elem in old_elements: - if elem != o: - self.push(elem) - - class _Stack: """ Stack of elements with a movable cursor. @@ -913,10 +801,6 @@ def __setstate__(self, state): def __contains__(self, item): 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.""" - def join(self, a, *args): """ Join given arguments into the same set. Accepts one or more arguments. diff --git a/lib/matplotlib/cbook.pyi b/lib/matplotlib/cbook.pyi index d727b8065b7a..cc6b4e8f4e19 100644 --- a/lib/matplotlib/cbook.pyi +++ b/lib/matplotlib/cbook.pyi @@ -74,26 +74,18 @@ def open_file_cm( def is_scalar_or_string(val: Any) -> bool: ... @overload def get_sample_data( - fname: str | os.PathLike, asfileobj: Literal[True] = ..., *, np_load: Literal[True] -) -> np.ndarray: ... + fname: str | os.PathLike, asfileobj: Literal[True] = ... +) -> np.ndarray | IO: ... @overload -def get_sample_data( - fname: str | os.PathLike, - asfileobj: Literal[True] = ..., - *, - np_load: Literal[False] = ..., -) -> IO: ... -@overload -def get_sample_data( - fname: str | os.PathLike, asfileobj: Literal[False], *, np_load: bool = ... -) -> str: ... +def get_sample_data(fname: str | os.PathLike, asfileobj: Literal[False]) -> str: ... def _get_data_path(*args: Path | str) -> Path: ... def flatten( seq: Iterable[Any], scalarp: Callable[[Any], bool] = ... ) -> Generator[Any, None, None]: ... -class Stack(Generic[_T]): - def __init__(self, default: _T | None = ...) -> None: ... +class _Stack(Generic[_T]): + def __init__(self) -> None: ... + def clear(self) -> None: ... def __call__(self) -> _T: ... def __len__(self) -> int: ... def __getitem__(self, ind: int) -> _T: ... @@ -101,10 +93,6 @@ class Stack(Generic[_T]): def back(self) -> _T: ... def push(self, o: _T) -> _T: ... def home(self) -> _T: ... - def empty(self) -> bool: ... - def clear(self) -> None: ... - def bubble(self, o: _T) -> _T: ... - def remove(self, o: _T) -> None: ... def safe_masked_invalid(x: ArrayLike, copy: bool = ...) -> np.ndarray: ... def print_cycles( @@ -114,7 +102,6 @@ def print_cycles( class Grouper(Generic[_T]): def __init__(self, init: Iterable[_T] = ...) -> None: ... def __contains__(self, item: _T) -> bool: ... - def clean(self) -> None: ... def join(self, a: _T, *args: _T) -> None: ... def joined(self, a: _T, b: _T) -> bool: ... def remove(self, a: _T) -> None: ... From d9fdae88f1e9d72fb692632088b3eee3615aaabe Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 25 Sep 2024 01:06:18 -0400 Subject: [PATCH 0305/1230] Remove remaining 3.8 deprecations --- .../next_api_changes/removals/28874-ES.rst | 71 +++++++++++++++++++ lib/matplotlib/figure.py | 17 ++--- lib/matplotlib/legend.py | 18 +---- lib/matplotlib/path.py | 5 +- lib/matplotlib/table.py | 6 +- lib/matplotlib/tests/test_figure.py | 6 +- lib/matplotlib/tests/test_legend.py | 15 +--- lib/matplotlib/tests/test_table.py | 17 +---- lib/matplotlib/tests/test_widgets.py | 11 --- lib/matplotlib/texmanager.py | 3 +- lib/matplotlib/text.py | 4 -- lib/matplotlib/transforms.py | 13 +--- lib/matplotlib/transforms.pyi | 5 +- lib/matplotlib/widgets.py | 5 -- lib/matplotlib/widgets.pyi | 2 - 15 files changed, 93 insertions(+), 105 deletions(-) diff --git a/doc/api/next_api_changes/removals/28874-ES.rst b/doc/api/next_api_changes/removals/28874-ES.rst index 0be06b000096..dbd8778dead1 100644 --- a/doc/api/next_api_changes/removals/28874-ES.rst +++ b/doc/api/next_api_changes/removals/28874-ES.rst @@ -1,3 +1,48 @@ +Passing extra positional arguments to ``Figure.add_axes`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Positional arguments passed to `.Figure.add_axes` other than a rect or an existing +``Axes`` were previously ignored, and is now an error. + + +Artists explicitly passed in will no longer be filtered by legend() based on their label +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, artists explicitly passed to ``legend(handles=[...])`` are filtered out if +their label starts with an underscore. This filter is no longer applied; explicitly +filter out such artists (``[art for art in artists if not +art.get_label().startswith('_')]``) if necessary. + +Note that if no handles are specified at all, then the default still filters out labels +starting with an underscore. + + +The parameter of ``Annotation.contains`` and ``Legend.contains`` is renamed to *mouseevent* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... consistently with `.Artist.contains`. + + +Support for passing the "frac" key in ``annotate(..., arrowprops={"frac": ...})`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed. This key has had no effect since Matplotlib 1.5. + + +Passing non-int or sequence of non-int to ``Table.auto_set_column_width`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Column numbers are ints, and formerly passing any other type was effectively ignored. +This has now become an error. + + +Widgets +~~~~~~~ + +The *visible* attribute getter of ``*Selector`` widgets has been removed; use +``get_visible`` instead. + + Auto-closing of figures when switching backend ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -61,6 +106,13 @@ which should cover most use cases. ... with no replacement. +``TexManager.texcache`` +~~~~~~~~~~~~~~~~~~~~~~~ + +... is considered private and has been removed. The location of the cache directory is +clarified in the doc-string. + + ``cbook`` API changes ~~~~~~~~~~~~~~~~~~~~~ @@ -74,6 +126,25 @@ now auto-loads numpy arrays. Use ``get_sample_data(..., asfileobj=False)`` inste the filename of the data file, which can then be passed to `open`, if desired. +Calling ``paths.get_path_collection_extents`` with empty *offsets* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Calling `~.get_path_collection_extents` with an empty *offsets* parameter has an +ambiguous interpretation and is no longer allowed. + + +``bbox.anchored()`` with no explicit container +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Not passing a *container* argument to `.BboxBase.anchored` is no longer supported. + + +``INVALID_NON_AFFINE``, ``INVALID_AFFINE``, ``INVALID`` attributes of ``TransformNode`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These attributes have been removed. + + ``axes_grid1`` API changes ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 73f1629180aa..4271bb78e8de 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -613,22 +613,22 @@ def add_axes(self, *args, **kwargs): """ if not len(args) and 'rect' not in kwargs: - raise TypeError( - "add_axes() missing 1 required positional argument: 'rect'") + raise TypeError("add_axes() missing 1 required positional argument: 'rect'") elif 'rect' in kwargs: if len(args): - raise TypeError( - "add_axes() got multiple values for argument 'rect'") + raise TypeError("add_axes() got multiple values for argument 'rect'") args = (kwargs.pop('rect'), ) + if len(args) != 1: + raise _api.nargs_error("add_axes", 1, len(args)) if isinstance(args[0], Axes): - a, *extra_args = args + a, = args key = a._projection_init if a.get_figure(root=False) is not self: raise ValueError( "The Axes must have been created in the present figure") else: - rect, *extra_args = args + rect, = args if not np.isfinite(rect).all(): raise ValueError(f'all entries in rect must be finite not {rect}') projection_class, pkw = self._process_projection_requirements(**kwargs) @@ -637,11 +637,6 @@ def add_axes(self, *args, **kwargs): a = projection_class(self, rect, **pkw) key = (projection_class, pkw) - if extra_args: - _api.warn_deprecated( - "3.8", - name="Passing more than one positional argument to Figure.add_axes", - addendum="Any additional positional arguments are currently ignored.") return self._add_axes_internal(a, key) @_docstring.interpd diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 270757fc298e..a3bf4c14e7fc 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -454,24 +454,10 @@ def __init__( self.borderaxespad = mpl._val_or_rc(borderaxespad, 'legend.borderaxespad') self.columnspacing = mpl._val_or_rc(columnspacing, 'legend.columnspacing') self.shadow = mpl._val_or_rc(shadow, 'legend.shadow') - # trim handles and labels if illegal label... - _lab, _hand = [], [] - for label, handle in zip(labels, handles): - if isinstance(label, str) and label.startswith('_'): - _api.warn_deprecated("3.8", message=( - "An artist whose label starts with an underscore was passed to " - "legend(); such artists will no longer be ignored in the future. " - "To suppress this warning, explicitly filter out such artists, " - "e.g. with `[art for art in artists if not " - "art.get_label().startswith('_')]`.")) - else: - _lab.append(label) - _hand.append(handle) - labels, handles = _lab, _hand if reverse: - labels.reverse() - handles.reverse() + labels = [*reversed(labels)] + handles = [*reversed(handles)] if len(handles) < 2: ncols = 1 diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 0e870161a48d..5f5a0f3de423 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -1086,10 +1086,7 @@ def get_path_collection_extents( if len(paths) == 0: raise ValueError("No paths provided") if len(offsets) == 0: - _api.warn_deprecated( - "3.8", message="Calling get_path_collection_extents() with an" - " empty offsets list is deprecated since %(since)s. Support will" - " be removed %(removal)s.") + raise ValueError("No offsets provided") extents, minpos = _path.get_path_collection_extents( master_transform, paths, np.atleast_3d(transforms), offsets, offset_transform) diff --git a/lib/matplotlib/table.py b/lib/matplotlib/table.py index 0f75021926fd..212cd9f45187 100644 --- a/lib/matplotlib/table.py +++ b/lib/matplotlib/table.py @@ -496,11 +496,7 @@ def auto_set_column_width(self, col): """ col1d = np.atleast_1d(col) if not np.issubdtype(col1d.dtype, np.integer): - _api.warn_deprecated("3.8", name="col", - message="%(name)r must be an int or sequence of ints. " - "Passing other types is deprecated since %(since)s " - "and will be removed %(removal)s.") - return + raise TypeError("col must be an int or sequence of ints.") for cell in col1d: self._autoColumns.append(cell) diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 99045e773d02..528df182a2d0 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -518,12 +518,10 @@ def test_invalid_figure_add_axes(): fig.add_axes(ax) fig2.delaxes(ax) - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="Passing more than one positional argument"): + with pytest.raises(TypeError, match=r"add_axes\(\) takes 1 positional arguments"): fig2.add_axes(ax, "extra positional argument") - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="Passing more than one positional argument"): + with pytest.raises(TypeError, match=r"add_axes\(\) takes 1 positional arguments"): fig.add_axes([0, 0, 1, 1], "extra positional argument") diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 62b40ddb2d7a..cf0a950de1ca 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -19,7 +19,7 @@ import matplotlib.lines as mlines from matplotlib.legend_handler import HandlerTuple import matplotlib.legend as mlegend -from matplotlib import _api, rc_context +from matplotlib import rc_context from matplotlib.font_manager import FontProperties @@ -138,19 +138,6 @@ def test_various_labels(): ax.legend(numpoints=1, loc='best') -def test_legend_label_with_leading_underscore(): - """ - Test that artists with labels starting with an underscore are not added to - the legend, and that a warning is issued if one tries to add them - explicitly. - """ - fig, ax = plt.subplots() - line, = ax.plot([0, 1], label='_foo') - with pytest.warns(_api.MatplotlibDeprecationWarning, match="with an underscore"): - legend = ax.legend(handles=[line]) - assert len(legend.legend_handles) == 0 - - @image_comparison(['legend_labels_first.png'], remove_text=True, tol=0.013 if platform.machine() == 'arm64' else 0) def test_labels_first(): diff --git a/lib/matplotlib/tests/test_table.py b/lib/matplotlib/tests/test_table.py index ea31ac124e4a..653e918eecc8 100644 --- a/lib/matplotlib/tests/test_table.py +++ b/lib/matplotlib/tests/test_table.py @@ -2,10 +2,8 @@ from unittest.mock import Mock import numpy as np -import pytest import matplotlib.pyplot as plt -import matplotlib as mpl from matplotlib.path import Path from matplotlib.table import CustomCell, Table from matplotlib.testing.decorators import image_comparison, check_figures_equal @@ -128,10 +126,9 @@ def test_customcell(): @image_comparison(['table_auto_column.png']) def test_auto_column(): - fig = plt.figure() + fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1) # iterable list input - ax1 = fig.add_subplot(4, 1, 1) ax1.axis('off') tb1 = ax1.table( cellText=[['Fit Text', 2], @@ -144,7 +141,6 @@ def test_auto_column(): tb1.auto_set_column_width([-1, 0, 1]) # iterable tuple input - ax2 = fig.add_subplot(4, 1, 2) ax2.axis('off') tb2 = ax2.table( cellText=[['Fit Text', 2], @@ -157,7 +153,6 @@ def test_auto_column(): tb2.auto_set_column_width((-1, 0, 1)) # 3 single inputs - ax3 = fig.add_subplot(4, 1, 3) ax3.axis('off') tb3 = ax3.table( cellText=[['Fit Text', 2], @@ -171,8 +166,8 @@ def test_auto_column(): tb3.auto_set_column_width(0) tb3.auto_set_column_width(1) - # 4 non integer iterable input - ax4 = fig.add_subplot(4, 1, 4) + # 4 this used to test non-integer iterable input, which did nothing, but only + # remains to avoid re-generating the test image. ax4.axis('off') tb4 = ax4.table( cellText=[['Fit Text', 2], @@ -182,12 +177,6 @@ def test_auto_column(): loc="center") tb4.auto_set_font_size(False) tb4.set_fontsize(12) - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="'col' must be an int or sequence of ints"): - tb4.auto_set_column_width("-101") # type: ignore [arg-type] - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="'col' must be an int or sequence of ints"): - tb4.auto_set_column_width(["-101"]) # type: ignore [list-item] def test_table_cells(): diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 58238cd08af2..585d846944e8 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -3,7 +3,6 @@ import operator from unittest import mock -import matplotlib as mpl from matplotlib.backend_bases import MouseEvent import matplotlib.colors as mcolors import matplotlib.widgets as widgets @@ -131,16 +130,6 @@ def test_rectangle_minspan(ax, spancoords, minspanx, x1, minspany, y1): assert kwargs == {} -def test_deprecation_selector_visible_attribute(ax): - tool = widgets.RectangleSelector(ax) - - assert tool.get_visible() - - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="was deprecated in Matplotlib 3.8"): - tool.visible - - @pytest.mark.parametrize('drag_from_anywhere, new_center', [[True, (60, 75)], [False, (30, 20)]]) diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index 8deb03b3e148..a374bfba8cab 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -31,7 +31,7 @@ import numpy as np import matplotlib as mpl -from matplotlib import _api, cbook, dviread +from matplotlib import cbook, dviread _log = logging.getLogger(__name__) @@ -63,7 +63,6 @@ class TexManager: Repeated calls to this constructor always return the same instance. """ - texcache = _api.deprecate_privatize_attribute("3.8") _texcache = os.path.join(mpl.get_cachedir(), 'tex.cache') _grey_arrayd = {} diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 6691f32f8771..782dc9754e52 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -1842,10 +1842,6 @@ def transform(renderer) -> Transform # modified YAArrow API to be used with FancyArrowPatch for key in ['width', 'headwidth', 'headlength', 'shrink']: arrowprops.pop(key, None) - if 'frac' in arrowprops: - _api.warn_deprecated( - "3.8", name="the (unused) 'frac' key in 'arrowprops'") - arrowprops.pop("frac") self.arrow_patch = FancyArrowPatch((0, 0), (1, 1), **arrowprops) else: self.arrow_patch = None diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index fde6d6732171..15caff545e73 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -93,9 +93,6 @@ class TransformNode: # Invalidation may affect only the affine part. If the # invalidation was "affine-only", the _invalid member is set to # INVALID_AFFINE_ONLY - INVALID_NON_AFFINE = _api.deprecated("3.8")(_api.classproperty(lambda cls: 1)) - INVALID_AFFINE = _api.deprecated("3.8")(_api.classproperty(lambda cls: 2)) - INVALID = _api.deprecated("3.8")(_api.classproperty(lambda cls: 3)) # Possible values for the _invalid attribute. _VALID, _INVALID_AFFINE_ONLY, _INVALID_FULL = range(3) @@ -480,7 +477,7 @@ def transformed(self, transform): 'NW': (0, 1.0), 'W': (0, 0.5)} - def anchored(self, c, container=None): + def anchored(self, c, container): """ Return a copy of the `Bbox` anchored to *c* within *container*. @@ -490,19 +487,13 @@ def anchored(self, c, container=None): Either an (*x*, *y*) pair of relative coordinates (0 is left or bottom, 1 is right or top), 'C' (center), or a cardinal direction ('SW', southwest, is bottom left, etc.). - container : `Bbox`, optional + container : `Bbox` The box within which the `Bbox` is positioned. See Also -------- .Axes.set_anchor """ - if container is None: - _api.warn_deprecated( - "3.8", message="Calling anchored() with no container bbox " - "returns a frozen copy of the original bbox and is deprecated " - "since %(since)s.") - container = self l, b, w, h = container.bounds L, B, W, H = self.bounds cx, cy = self.coefs[c] if isinstance(c, str) else c diff --git a/lib/matplotlib/transforms.pyi b/lib/matplotlib/transforms.pyi index 90a527e5bfc5..c87a965b1e4a 100644 --- a/lib/matplotlib/transforms.pyi +++ b/lib/matplotlib/transforms.pyi @@ -77,9 +77,10 @@ class BboxBase(TransformNode): def fully_overlaps(self, other: BboxBase) -> bool: ... def transformed(self, transform: Transform) -> Bbox: ... coefs: dict[str, tuple[float, float]] - # anchored type can be s/str/Literal["C", "SW", "S", "SE", "E", "NE", "N", "NW", "W"] def anchored( - self, c: tuple[float, float] | str, container: BboxBase | None = ... + self, + c: tuple[float, float] | Literal['C', 'SW', 'S', 'SE', 'E', 'NE', 'N', 'NW', 'W'], + container: BboxBase, ) -> Bbox: ... def shrunk(self, mx: float, my: float) -> Bbox: ... def shrunk_to_aspect( diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 0e8eaa68b6b4..9c676574310c 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2350,11 +2350,6 @@ def get_visible(self): """Get the visibility of the selector artists.""" return self._visible - @property - def visible(self): - _api.warn_deprecated("3.8", alternative="get_visible") - return self.get_visible() - def clear(self): """Clear the selection and set the selector ready to make a new one.""" self._clear_without_update() diff --git a/lib/matplotlib/widgets.pyi b/lib/matplotlib/widgets.pyi index 96bc0c431ac3..0fcd1990e17e 100644 --- a/lib/matplotlib/widgets.pyi +++ b/lib/matplotlib/widgets.pyi @@ -294,8 +294,6 @@ class _SelectorWidget(AxesWidget): def on_key_release(self, event: Event) -> None: ... def set_visible(self, visible: bool) -> None: ... def get_visible(self) -> bool: ... - @property - def visible(self) -> bool: ... def clear(self) -> None: ... @property def artists(self) -> tuple[Artist]: ... From 24c5048a15dbcd7031031e25c3e554cd9a4416d8 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 1 Oct 2024 00:27:52 +0200 Subject: [PATCH 0306/1230] MNT: Cleanup docstring substitution mechanisms (#28795) * MNT: Make Substitution and _ArtistPropertiesSubstitution independent They do not have relevant functional overlap. Thus, it's simpler to keep them completely separated. * Rename _ArtistPropertiesSubstitution.update() to register() While it's internally a dict.update. The logical process is a registration to make the names publically available. Also, remove the possibility to pass a dict. Passing kwargs is enough and we can simplify the API to only one type of usage. --- lib/matplotlib/_docstring.py | 48 +++++++++++++++++--------- lib/matplotlib/_docstring.pyi | 3 +- lib/matplotlib/_enums.py | 6 ++-- lib/matplotlib/axes/_secondary_axes.py | 2 +- lib/matplotlib/cm.py | 2 +- lib/matplotlib/colorbar.py | 2 +- lib/matplotlib/contour.py | 4 +-- lib/matplotlib/legend.py | 8 ++--- lib/matplotlib/mlab.py | 2 +- lib/matplotlib/patches.py | 4 +-- lib/matplotlib/projections/__init__.py | 2 +- lib/matplotlib/quiver.py | 4 +-- lib/matplotlib/scale.py | 2 +- lib/matplotlib/text.py | 2 +- lib/matplotlib/tri/_tricontour.py | 2 +- 15 files changed, 56 insertions(+), 37 deletions(-) diff --git a/lib/matplotlib/_docstring.py b/lib/matplotlib/_docstring.py index 7e9448fd63c8..8cc7d623efe5 100644 --- a/lib/matplotlib/_docstring.py +++ b/lib/matplotlib/_docstring.py @@ -68,12 +68,6 @@ def __call__(self, func): func.__doc__ = inspect.cleandoc(func.__doc__) % self.params return func - def update(self, *args, **kwargs): - """ - Update ``self.params`` (which must be a dict) with the supplied args. - """ - self.params.update(*args, **kwargs) - class _ArtistKwdocLoader(dict): def __missing__(self, key): @@ -89,23 +83,45 @@ def __missing__(self, key): return self.setdefault(key, kwdoc(cls)) -class _ArtistPropertiesSubstitution(Substitution): +class _ArtistPropertiesSubstitution: """ - A `.Substitution` with two additional features: - - - Substitutions of the form ``%(classname:kwdoc)s`` (ending with the - literal ":kwdoc" suffix) trigger lookup of an Artist subclass with the - given *classname*, and are substituted with the `.kwdoc` of that class. - - Decorating a class triggers substitution both on the class docstring and - on the class' ``__init__`` docstring (which is a commonly required - pattern for Artist subclasses). + A class to substitute formatted placeholders in docstrings. + + This is realized in a single instance ``_docstring.interpd``. + + Use `~._ArtistPropertiesSubstition.register` to define placeholders and + their substitution, e.g. ``_docstring.interpd.register(name="some value")``. + + Use this as a decorator to apply the substitution:: + + @_docstring.interpd + def some_func(): + '''Replace %(name)s.''' + + Decorating a class triggers substitution both on the class docstring and + on the class' ``__init__`` docstring (which is a commonly required + pattern for Artist subclasses). + + Substitutions of the form ``%(classname:kwdoc)s`` (ending with the + literal ":kwdoc" suffix) trigger lookup of an Artist subclass with the + given *classname*, and are substituted with the `.kwdoc` of that class. """ def __init__(self): self.params = _ArtistKwdocLoader() + def register(self, **kwargs): + """ + Register substitutions. + + ``_docstring.interpd.register(name="some value")`` makes "name" available + as a named parameter that will be replaced by "some value". + """ + self.params.update(**kwargs) + def __call__(self, obj): - super().__call__(obj) + if obj.__doc__: + obj.__doc__ = inspect.cleandoc(obj.__doc__) % self.params if isinstance(obj, type) and obj.__init__ != object.__init__: self(obj.__init__) return obj diff --git a/lib/matplotlib/_docstring.pyi b/lib/matplotlib/_docstring.pyi index 62cea3da4476..fb52d0846123 100644 --- a/lib/matplotlib/_docstring.pyi +++ b/lib/matplotlib/_docstring.pyi @@ -21,8 +21,9 @@ class _ArtistKwdocLoader(dict[str, str]): def __missing__(self, key: str) -> str: ... -class _ArtistPropertiesSubstitution(Substitution): +class _ArtistPropertiesSubstitution: def __init__(self) -> None: ... + def register(self, **kwargs) -> None: ... def __call__(self, obj: _T) -> _T: ... diff --git a/lib/matplotlib/_enums.py b/lib/matplotlib/_enums.py index c8c50f7c3028..773011d36bf6 100644 --- a/lib/matplotlib/_enums.py +++ b/lib/matplotlib/_enums.py @@ -181,5 +181,7 @@ def demo(): + ", ".join([f"'{cs.name}'" for cs in CapStyle]) \ + "}" -_docstring.interpd.update({'JoinStyle': JoinStyle.input_description, - 'CapStyle': CapStyle.input_description}) +_docstring.interpd.register( + JoinStyle=JoinStyle.input_description, + CapStyle=CapStyle.input_description, +) diff --git a/lib/matplotlib/axes/_secondary_axes.py b/lib/matplotlib/axes/_secondary_axes.py index b01acc4b127d..15a1970fa4a6 100644 --- a/lib/matplotlib/axes/_secondary_axes.py +++ b/lib/matplotlib/axes/_secondary_axes.py @@ -319,4 +319,4 @@ def set_color(self, color): **kwargs : `~matplotlib.axes.Axes` properties. Other miscellaneous Axes parameters. ''' -_docstring.interpd.update(_secax_docstring=_secax_docstring) +_docstring.interpd.register(_secax_docstring=_secax_docstring) diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index 025cb84db1d7..27333f8dba8a 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -651,7 +651,7 @@ def _format_cursor_data_override(self, data): # The docstrings here must be generic enough to apply to all relevant methods. -mpl._docstring.interpd.update( +mpl._docstring.interpd.register( cmap_doc="""\ cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` The Colormap instance or registered colormap name used to map scalar data diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 296f072a4af1..2d2fe42dd16a 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -26,7 +26,7 @@ _log = logging.getLogger(__name__) -_docstring.interpd.update( +_docstring.interpd.register( _make_axes_kw_doc=""" location : None or {'left', 'right', 'top', 'bottom'} The location, relative to the parent Axes, where the colorbar Axes diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index ff115b10e6d8..2bfd32690297 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -536,7 +536,7 @@ def _find_closest_point_on_path(xys, p): return (d2s[imin], projs[imin], (imin, imin+1)) -_docstring.interpd.update(contour_set_attributes=r""" +_docstring.interpd.register(contour_set_attributes=r""" Attributes ---------- ax : `~matplotlib.axes.Axes` @@ -1450,7 +1450,7 @@ def _initialize_x_y(self, z): return np.meshgrid(x, y) -_docstring.interpd.update(contour_doc=""" +_docstring.interpd.register(contour_doc=""" `.contour` and `.contourf` draw contour lines and filled contours, respectively. Except as noted, function signatures and return values are the same for both versions. diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 270757fc298e..f5b7d3f1482a 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -305,7 +305,7 @@ def _update_bbox_to_anchor(self, loc_in_canvas): _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) +_docstring.interpd.register(_legend_kw_axes=_legend_kw_axes_st) _outside_doc = """ If a figure is using the constrained layout manager, the string codes @@ -323,20 +323,20 @@ def _update_bbox_to_anchor(self, loc_in_canvas): _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) +_docstring.interpd.register(_legend_kw_figure=_legend_kw_figure_st) _legend_kw_both_st = ( _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) +_docstring.interpd.register(_legend_kw_doc=_legend_kw_both_st) _legend_kw_set_loc_st = ( _loc_doc_base.format(parent='axes/figure', default=":rc:`legend.loc` for Axes, 'upper right' for Figure", best=_loc_doc_best, outside=_outside_doc)) -_docstring.interpd.update(_legend_kw_set_loc_doc=_legend_kw_set_loc_st) +_docstring.interpd.register(_legend_kw_set_loc_doc=_legend_kw_set_loc_st) class Legend(Artist): diff --git a/lib/matplotlib/mlab.py b/lib/matplotlib/mlab.py index fad8d648f6db..8326ac186e31 100644 --- a/lib/matplotlib/mlab.py +++ b/lib/matplotlib/mlab.py @@ -400,7 +400,7 @@ def _single_spectrum_helper( # Split out these keyword docs so that they can be used elsewhere -_docstring.interpd.update( +_docstring.interpd.register( Spectral="""\ Fs : float, default: 2 The sampling frequency (samples per time unit). It is used to calculate diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 1c19d8424db0..2db678587ec7 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -1552,7 +1552,7 @@ def _make_verts(self): ] -_docstring.interpd.update( +_docstring.interpd.register( FancyArrow="\n".join( (inspect.getdoc(FancyArrow.__init__) or "").splitlines()[2:])) @@ -2290,7 +2290,7 @@ def __init_subclass__(cls): # - %(BoxStyle:table_and_accepts)s # - %(ConnectionStyle:table_and_accepts)s # - %(ArrowStyle:table_and_accepts)s - _docstring.interpd.update({ + _docstring.interpd.register(**{ f"{cls.__name__}:table": cls.pprint_styles(), f"{cls.__name__}:table_and_accepts": ( cls.pprint_styles() diff --git a/lib/matplotlib/projections/__init__.py b/lib/matplotlib/projections/__init__.py index b58d1ceb754d..f7b46192a84e 100644 --- a/lib/matplotlib/projections/__init__.py +++ b/lib/matplotlib/projections/__init__.py @@ -123,4 +123,4 @@ def get_projection_class(projection=None): get_projection_names = projection_registry.get_projection_names -_docstring.interpd.update(projection_names=get_projection_names()) +_docstring.interpd.register(projection_names=get_projection_names()) diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index 16c8a2195f67..c7408476c784 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -230,7 +230,7 @@ of the head in forward direction so that the arrow head looks broken. """ % _docstring.interpd.params -_docstring.interpd.update(quiver_doc=_quiver_doc) +_docstring.interpd.register(quiver_doc=_quiver_doc) class QuiverKey(martist.Artist): @@ -865,7 +865,7 @@ def _h_arrows(self, length): %(PolyCollection:kwdoc)s """ % _docstring.interpd.params -_docstring.interpd.update(barbs_doc=_barbs_doc) +_docstring.interpd.register(barbs_doc=_barbs_doc) class Barbs(mcollections.PolyCollection): diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index f81137c75082..ccaaae6caf5d 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -742,7 +742,7 @@ def _get_scale_docs(): return "\n".join(docs) -_docstring.interpd.update( +_docstring.interpd.register( scale_type='{%s}' % ', '.join([repr(x) for x in get_scale_names()]), scale_docs=_get_scale_docs().rstrip(), ) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 6691f32f8771..237ad8e1001c 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -2029,4 +2029,4 @@ def get_tightbbox(self, renderer=None): return super().get_tightbbox(renderer) -_docstring.interpd.update(Annotation=Annotation.__init__.__doc__) +_docstring.interpd.register(Annotation=Annotation.__init__.__doc__) diff --git a/lib/matplotlib/tri/_tricontour.py b/lib/matplotlib/tri/_tricontour.py index c09d04f9e543..8250515f3ef8 100644 --- a/lib/matplotlib/tri/_tricontour.py +++ b/lib/matplotlib/tri/_tricontour.py @@ -79,7 +79,7 @@ def _contour_args(self, args, kwargs): return (tri, z) -_docstring.interpd.update(_tricontour_doc=""" +_docstring.interpd.register(_tricontour_doc=""" Draw contour %%(type)s on an unstructured triangular grid. Call signatures:: From 7c7f94c5f71e99f148255e3bb570fec25c8fe754 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 30 Sep 2024 18:31:34 -0400 Subject: [PATCH 0307/1230] TST: Fix minor issues in interactive backend test (#28838) - Close the figure immediately, to avoid the deprecation warning about automatically closing figure when changing backends. This test doesn't intend to test the warning, just the change behaviour itself. - Enable the post-`show()` result check everywhere. The comment implies it should only be skipped on `(macOS and Qt5)`, but the condition is actually only enabled on `macOS and (not Qt5)`. --- lib/matplotlib/tests/test_backends_interactive.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index ca702bc1d99c..7621ac5b5689 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -170,7 +170,8 @@ def _test_interactive_impl(): if backend.endswith("agg") and not backend.startswith(("gtk", "web")): # Force interactive framework setup. - plt.figure() + fig = plt.figure() + plt.close(fig) # Check that we cannot switch to a backend using another interactive # framework, but can switch to a backend using cairo instead of agg, @@ -228,10 +229,7 @@ def check_alt_backend(alt_backend): result_after = io.BytesIO() fig.savefig(result_after, format='png') - if not backend.startswith('qt5') and sys.platform == 'darwin': - # FIXME: This should be enabled everywhere once Qt5 is fixed on macOS - # to not resize incorrectly. - assert result.getvalue() == result_after.getvalue() + assert result.getvalue() == result_after.getvalue() @pytest.mark.parametrize("env", _get_testable_interactive_backends()) @@ -448,8 +446,7 @@ def qt5_and_qt6_pairs(): for qt5 in qt5_bindings: for qt6 in qt6_bindings: - for pair in ([qt5, qt6], [qt6, qt5]): - yield pair + yield from ([qt5, qt6], [qt6, qt5]) @pytest.mark.parametrize('host, mpl', [*qt5_and_qt6_pairs()]) From f1b0a28574a06f94ae2369f3c8c017b081bf5de9 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:25:06 +0200 Subject: [PATCH 0308/1230] MNT: Fix double evaluation of _LazyTickList Closes #28908. The final instance.majorTicks list must be assigned after running `_get_tick()` because that may call `reset_ticks()`, which invalidates a previous set list. We still temporarily assign an empty tick list to instance.majorTicks, because `_get_tick()` may rely on that attibute to exist. --- doc/users/next_whats_new/axes_creation_speedup.rst | 4 ++++ lib/matplotlib/axis.py | 10 ++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 doc/users/next_whats_new/axes_creation_speedup.rst diff --git a/doc/users/next_whats_new/axes_creation_speedup.rst b/doc/users/next_whats_new/axes_creation_speedup.rst new file mode 100644 index 000000000000..c9eaa48c0060 --- /dev/null +++ b/doc/users/next_whats_new/axes_creation_speedup.rst @@ -0,0 +1,4 @@ +Axes creation speedup +~~~~~~~~~~~~~~~~~~~~~ + +Creating an Axes is now 20-25% faster due to internal optimizations. diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index cbf7928ec44e..d7ba29e1d595 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -538,17 +538,19 @@ def __get__(self, instance, owner): # instance._get_tick() can itself try to access the majorTicks # attribute (e.g. in certain projection classes which override # e.g. get_xaxis_text1_transform). In order to avoid infinite - # recursion, first set the majorTicks on the instance to an empty - # list, then create the tick and append it. + # recursion, first set the majorTicks on the instance temporarily + # to an empty lis. Then create the tick; note that _get_tick() + # may call reset_ticks(). Therefore, the final tick list is + # created and assigned afterwards. if self._major: instance.majorTicks = [] tick = instance._get_tick(major=True) - instance.majorTicks.append(tick) + instance.majorTicks = [tick] return instance.majorTicks else: instance.minorTicks = [] tick = instance._get_tick(major=False) - instance.minorTicks.append(tick) + instance.minorTicks = [tick] return instance.minorTicks From f8666be3f6979864b5023037c9d8337201569399 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 1 Oct 2024 01:52:28 +0200 Subject: [PATCH 0309/1230] MNT: Check the input sizes of regular X,Y in pcolorfast (#28853) * MNT: Check the input sizes of regular X,Y in pcolorfast Closes #28059. * Apply suggestions from code review Co-authored-by: Elliott Sales de Andrade --------- Co-authored-by: Elliott Sales de Andrade --- lib/matplotlib/axes/_axes.py | 9 +++++++++ lib/matplotlib/tests/test_axes.py | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 415a88b28435..de0c6854cbb1 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6627,6 +6627,15 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, if x.size == 2 and y.size == 2: style = "image" else: + if x.size != nc + 1: + raise ValueError( + f"Length of X ({x.size}) must be one larger than the " + f"number of columns in C ({nc})") + if y.size != nr + 1: + raise ValueError( + f"Length of Y ({y.size}) must be one larger than the " + f"number of rows in C ({nr})" + ) dx = np.diff(x) dy = np.diff(y) if (np.ptp(dx) < 0.01 * abs(dx.mean()) and diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index e3877dbad7af..aff414696d47 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6647,6 +6647,27 @@ def test_pcolorfast_bad_dims(): ax.pcolorfast(np.empty(6), np.empty((4, 7)), np.empty((8, 8))) +def test_pcolorfast_regular_xy_incompatible_size(): + """ + Test that the sizes of X, Y, C are compatible for regularly spaced X, Y. + + Note that after the regualar-spacing check, pcolorfast may go into the + fast "image" mode, where the individual X, Y positions are not used anymore. + Therefore, the algorithm had worked with any regularly number of regularly + spaced values, but discarded their values. + """ + fig, ax = plt.subplots() + with pytest.raises( + ValueError, match=r"Length of X \(5\) must be one larger than the " + r"number of columns in C \(20\)"): + ax.pcolorfast(np.arange(5), np.arange(11), np.random.rand(10, 20)) + + with pytest.raises( + ValueError, match=r"Length of Y \(5\) must be one larger than the " + r"number of rows in C \(10\)"): + ax.pcolorfast(np.arange(21), np.arange(5), np.random.rand(10, 20)) + + def test_shared_scale(): fig, axs = plt.subplots(2, 2, sharex=True, sharey=True) From 537ea7acc0b533c0f10a39c8265541c7a4358dc3 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 1 Oct 2024 01:55:14 +0200 Subject: [PATCH 0310/1230] MNT: Prevent users from erroneously using legend label API on Axis (#28584) Closes #27971. For a complete explanation see https://github.com/matplotlib/matplotlib/issues/27971#issuecomment-2016955731 --- galleries/examples/axes_grid1/parasite_simple.py | 4 ++-- lib/matplotlib/axes/_base.py | 8 ++++---- lib/matplotlib/axis.py | 15 ++++++++++++++- .../backends/qt_editor/figureoptions.py | 2 +- lib/matplotlib/tests/test_axes.py | 14 +++++++------- lib/mpl_toolkits/axisartist/axis_artist.py | 4 ++-- lib/mpl_toolkits/mplot3d/axes3d.py | 2 +- 7 files changed, 31 insertions(+), 18 deletions(-) diff --git a/galleries/examples/axes_grid1/parasite_simple.py b/galleries/examples/axes_grid1/parasite_simple.py index ad4922308a3f..a0c4d68051a9 100644 --- a/galleries/examples/axes_grid1/parasite_simple.py +++ b/galleries/examples/axes_grid1/parasite_simple.py @@ -20,7 +20,7 @@ host.legend(labelcolor="linecolor") -host.yaxis.get_label().set_color(p1.get_color()) -par.yaxis.get_label().set_color(p2.get_color()) +host.yaxis.label.set_color(p1.get_color()) +par.yaxis.label.set_color(p2.get_color()) plt.show() diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 92c5354435e7..8fc9e3e3cf4d 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -780,8 +780,8 @@ def __repr__(self): if titles: fields += [f"title={titles}"] for name, axis in self._axis_map.items(): - if axis.get_label() and axis.get_label().get_text(): - fields += [f"{name}label={axis.get_label().get_text()!r}"] + if axis.label and axis.label.get_text(): + fields += [f"{name}label={axis.label.get_text()!r}"] return f"<{self.__class__.__name__}: " + ", ".join(fields) + ">" def get_subplotspec(self): @@ -3528,7 +3528,7 @@ def get_xlabel(self): """ Get the xlabel text string. """ - label = self.xaxis.get_label() + label = self.xaxis.label return label.get_text() def set_xlabel(self, xlabel, fontdict=None, labelpad=None, *, @@ -3781,7 +3781,7 @@ def get_ylabel(self): """ Get the ylabel text string. """ - label = self.yaxis.get_label() + label = self.yaxis.label return label.get_text() def set_ylabel(self, ylabel, fontdict=None, labelpad=None, *, diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index cbf7928ec44e..6b37c8be626d 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1421,8 +1421,21 @@ def get_gridlines(self): return cbook.silent_list('Line2D gridline', [tick.gridline for tick in ticks]) + def set_label(self, s): + """Assigning legend labels is not supported. Raises RuntimeError.""" + raise RuntimeError( + "A legend label cannot be assigned to an Axis. Did you mean to " + "set the axis label via set_label_text()?") + def get_label(self): - """Return the axis label as a Text instance.""" + """ + Return the axis label as a Text instance. + + .. admonition:: Discouraged + + This overrides `.Artist.get_label`, which is for legend labels, with a new + semantic. It is recommended to use the attribute ``Axis.label`` instead. + """ return self.label def get_offset_text(self): diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index 529f45829999..b025ef3e056e 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -54,7 +54,7 @@ def convert_limits(lim, converter): (None, f"{name.title()}-Axis"), ('Min', axis_limits[name][0]), ('Max', axis_limits[name][1]), - ('Label', axis.get_label().get_text()), + ('Label', axis.label.get_text()), ('Scale', [axis.get_scale(), 'linear', 'log', 'symlog', 'logit']), sep, diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index aff414696d47..5d50df4ffd20 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -136,20 +136,20 @@ def test_label_shift(): # Test label re-centering on x-axis ax.set_xlabel("Test label", loc="left") ax.set_xlabel("Test label", loc="center") - assert ax.xaxis.get_label().get_horizontalalignment() == "center" + assert ax.xaxis.label.get_horizontalalignment() == "center" ax.set_xlabel("Test label", loc="right") - assert ax.xaxis.get_label().get_horizontalalignment() == "right" + assert ax.xaxis.label.get_horizontalalignment() == "right" ax.set_xlabel("Test label", loc="center") - assert ax.xaxis.get_label().get_horizontalalignment() == "center" + assert ax.xaxis.label.get_horizontalalignment() == "center" # Test label re-centering on y-axis ax.set_ylabel("Test label", loc="top") ax.set_ylabel("Test label", loc="center") - assert ax.yaxis.get_label().get_horizontalalignment() == "center" + assert ax.yaxis.label.get_horizontalalignment() == "center" ax.set_ylabel("Test label", loc="bottom") - assert ax.yaxis.get_label().get_horizontalalignment() == "left" + assert ax.yaxis.label.get_horizontalalignment() == "left" ax.set_ylabel("Test label", loc="center") - assert ax.yaxis.get_label().get_horizontalalignment() == "center" + assert ax.yaxis.label.get_horizontalalignment() == "center" @check_figures_equal(extensions=["png"]) @@ -8463,7 +8463,7 @@ def test_ylabel_ha_with_position(ha): ax = fig.subplots() ax.set_ylabel("test", y=1, ha=ha) ax.yaxis.set_label_position("right") - assert ax.yaxis.get_label().get_ha() == ha + assert ax.yaxis.label.get_ha() == ha def test_bar_label_location_vertical(): diff --git a/lib/mpl_toolkits/axisartist/axis_artist.py b/lib/mpl_toolkits/axisartist/axis_artist.py index d58313bd99ef..b416d56abe6b 100644 --- a/lib/mpl_toolkits/axisartist/axis_artist.py +++ b/lib/mpl_toolkits/axisartist/axis_artist.py @@ -312,13 +312,13 @@ def get_pad(self): def get_ref_artist(self): # docstring inherited - return self._axis.get_label() + return self._axis.label def get_text(self): # docstring inherited t = super().get_text() if t == "__from_axes__": - return self._axis.get_label().get_text() + return self._axis.label.get_text() return self._text _default_alignments = dict(left=("bottom", "center"), diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 5d522cd0988a..b977474e1f28 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -1801,7 +1801,7 @@ def get_zlabel(self): """ Get the z-label text string. """ - label = self.zaxis.get_label() + label = self.zaxis.label return label.get_text() # Axes rectangle characteristics From 6ffebd8e3a9ccea5fc104b86166dd4c926d556d1 Mon Sep 17 00:00:00 2001 From: MischaMegens2 <122418839+MischaMegens2@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:22:52 -0700 Subject: [PATCH 0311/1230] Update view_angles.rst: polar plot -> spherical coordinate plot --- doc/api/toolkits/mplot3d/view_angles.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/toolkits/mplot3d/view_angles.rst b/doc/api/toolkits/mplot3d/view_angles.rst index 6ddb757f44d3..d5ea17bc7f27 100644 --- a/doc/api/toolkits/mplot3d/view_angles.rst +++ b/doc/api/toolkits/mplot3d/view_angles.rst @@ -54,7 +54,7 @@ Originally (prior to v3.10), the 2D mouse position corresponded directly to azimuth and elevation; this is also how it is done in `MATLAB `_. To keep it this way, set ``mouserotationstyle: azel``. -This approach works fine for polar plots, where the *z* axis is special; +This approach works fine for spherical coordinate plots, where the *z* axis is special; however, it leads to a kind of 'gimbal lock' when looking down the *z* axis: the plot reacts differently to mouse movement, dependent on the particular orientation at hand. Also, 'roll' cannot be controlled. From d35e0cf33e578488ac05535ce99363c7ec19ba65 Mon Sep 17 00:00:00 2001 From: MischaMegens2 <122418839+MischaMegens2@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:25:30 -0700 Subject: [PATCH 0312/1230] Update lib/mpl_toolkits/mplot3d/tests/test_axes3d.py Co-authored-by: Elliott Sales de Andrade --- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 02a58eadff1a..0a5c0f116e8a 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -1971,7 +1971,7 @@ def test_rotate(style): mock_event(ax, button=MouseButton.LEFT, xdata=0, ydata=0)) ax._on_move( mock_event(ax, button=MouseButton.LEFT, - xdata=s*dx*ax._pseudo_w, ydata=s*dy*ax._pseudo_h)) + xdata=s*dx*ax._pseudo_w, ydata=s*dy*ax._pseudo_h)) ax.figure.canvas.draw() c = np.sqrt(3)/2 From 9421ad06ce4d3e95cb779be2f0f840e30b502c23 Mon Sep 17 00:00:00 2001 From: MischaMegens2 <122418839+MischaMegens2@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:28:22 -0700 Subject: [PATCH 0313/1230] Update doc/api/toolkits/mplot3d/view_angles.rst Co-authored-by: Elliott Sales de Andrade --- doc/api/toolkits/mplot3d/view_angles.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/toolkits/mplot3d/view_angles.rst b/doc/api/toolkits/mplot3d/view_angles.rst index d5ea17bc7f27..85eceac32b85 100644 --- a/doc/api/toolkits/mplot3d/view_angles.rst +++ b/doc/api/toolkits/mplot3d/view_angles.rst @@ -50,7 +50,7 @@ There are various ways to accomplish this; the style of mouse rotation can be specified by setting ``rcParams.axes3d.mouserotationstyle``, see :doc:`/users/explain/customizing`. -Originally (prior to v3.10), the 2D mouse position corresponded directly +Prior to v3.10, the 2D mouse position corresponded directly to azimuth and elevation; this is also how it is done in `MATLAB `_. To keep it this way, set ``mouserotationstyle: azel``. From e7665b7296c7ea3b59becd6017333a242daebacf Mon Sep 17 00:00:00 2001 From: MischaMegens2 <122418839+MischaMegens2@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:28:57 -0700 Subject: [PATCH 0314/1230] Update doc/users/next_whats_new/mouse_rotation.rst Co-authored-by: Elliott Sales de Andrade --- doc/users/next_whats_new/mouse_rotation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/users/next_whats_new/mouse_rotation.rst b/doc/users/next_whats_new/mouse_rotation.rst index 4d8257ff1182..5c1b1480c595 100644 --- a/doc/users/next_whats_new/mouse_rotation.rst +++ b/doc/users/next_whats_new/mouse_rotation.rst @@ -7,7 +7,7 @@ particular orientation at hand; and it is possible to control all 3 rotational degrees of freedom (azimuth, elevation, and roll). By default, it uses a variation on Ken Shoemake's ARCBALL [1]_. The particular style of mouse rotation can be set via -``rcParams.axes3d.mouserotationstyle``. +:rc:`axes3d.mouserotationstyle`. See also :ref:`toolkit_mouse-rotation`. To revert to the original mouse rotation style, From 686f0ca93ce918a6b50d0883ab2efc3fc86b630f Mon Sep 17 00:00:00 2001 From: MischaMegens2 <122418839+MischaMegens2@users.noreply.github.com> Date: Mon, 30 Sep 2024 23:09:11 -0700 Subject: [PATCH 0315/1230] Suggestions from the reviewer --- doc/api/toolkits/mplot3d/view_angles.rst | 4 ++-- lib/mpl_toolkits/mplot3d/axes3d.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/api/toolkits/mplot3d/view_angles.rst b/doc/api/toolkits/mplot3d/view_angles.rst index 85eceac32b85..b7ac360b499b 100644 --- a/doc/api/toolkits/mplot3d/view_angles.rst +++ b/doc/api/toolkits/mplot3d/view_angles.rst @@ -47,7 +47,7 @@ Rotation with mouse 3D plots can be reoriented by dragging the mouse. There are various ways to accomplish this; the style of mouse rotation -can be specified by setting ``rcParams.axes3d.mouserotationstyle``, see +can be specified by setting :rc:`axes3d.mouserotationstyle`, see :doc:`/users/explain/customizing`. Prior to v3.10, the 2D mouse position corresponded directly @@ -169,7 +169,7 @@ Alternatively, create a file ``matplotlibrc``, with contents:: the :ref:`mplot3d-examples-index` examples. The size of the virtual trackball or arcball can be adjusted as well, -by setting ``rcParams.axes3d.trackballsize``. This specifies how much +by setting :rc:`axes3d.trackballsize`. This specifies how much mouse motion is needed to obtain a given rotation angle (when near the center), and it controls where the edge of the arcball is (how far from the center, how close to the plot edge). diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index c3c9c6f88156..8a61e793f1d2 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -1510,8 +1510,9 @@ def _calc_coord(self, xv, yv, renderer=None): def _arcball(self, x: float, y: float, style: str) -> np.ndarray: """ - Convert a point (x, y) to a point on a virtual trackball - either Ken Shoemake's arcball (a sphere) or + Convert a point (x, y) to a point on a virtual trackball. + + This is either Ken Shoemake's arcball (a sphere) or Tom Holroyd's (a sphere combined with a hyperbola). See: Ken Shoemake, "ARCBALL: A user interface for specifying three-dimensional rotation using a mouse." in From 22c02a8cdbf0ef42d47b3a5933202411cea0abb4 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 1 Oct 2024 08:19:03 +0200 Subject: [PATCH 0316/1230] DOC: Fix Axis.set_label reference Follow-up to #28584. --- doc/api/axis_api.rst | 4 +++- lib/matplotlib/axis.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/api/axis_api.rst b/doc/api/axis_api.rst index 424213445169..0870d38c9439 100644 --- a/doc/api/axis_api.rst +++ b/doc/api/axis_api.rst @@ -73,10 +73,10 @@ Axis Label :template: autosummary.rst :nosignatures: + Axis.label Axis.set_label_coords Axis.set_label_position Axis.set_label_text - Axis.get_label Axis.get_label_position Axis.get_label_text @@ -235,6 +235,8 @@ specify a matching series of labels. Calling ``set_ticks`` makes a :template: autosummary.rst :nosignatures: + Axis.get_label + Axis.set_label Axis.set_ticks Axis.set_ticklabels diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 6b37c8be626d..da0ce31c5a6b 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -637,7 +637,8 @@ def __init__(self, axes, *, pickradius=15, clear=True): fontsize=mpl.rcParams['axes.labelsize'], fontweight=mpl.rcParams['axes.labelweight'], color=mpl.rcParams['axes.labelcolor'], - ) + ) #: The `.Text` object of the axis label. + self._set_artist_props(self.label) self.offsetText = mtext.Text(np.nan, np.nan) self._set_artist_props(self.offsetText) From 88fbc2666dc0bcd00d16ecb9ca55edcc3f13ff07 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 29 Sep 2024 11:00:24 +0200 Subject: [PATCH 0317/1230] Re-fix exception caching in dviread. The original solution using with_traceback didn't actually work because with_traceback doesn't return a new exception instance but rather modifies the original one in place, and the traceback will then be attached to it by the raise statement. Instead, we actually need to build a new instance, so reuse the _ExceptionProxy machinery from font_manager (slightly expanded). --- lib/matplotlib/cbook.py | 23 +++++++++++++++++++++++ lib/matplotlib/dviread.py | 10 +++++----- lib/matplotlib/font_manager.py | 11 ++++------- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 7cf32c4d5f6a..95e857891190 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -32,6 +32,29 @@ from matplotlib import _api, _c_internal_utils +class _ExceptionInfo: + """ + A class to carry exception information around. + + This is used to store and later raise exceptions. It's an alternative to + directly storing Exception instances that circumvents traceback-related + issues: caching tracebacks can keep user's objects in local namespaces + alive indefinitely, which can lead to very surprising memory issues for + users and result in incorrect tracebacks. + """ + + def __init__(self, cls, *args): + self._cls = cls + self._args = args + + @classmethod + def from_exception(cls, exc): + return cls(type(exc), *exc.args) + + def to_exception(self): + return self._cls(*self._args) + + def _get_running_interactive_framework(): """ Return the interactive framework whose event loop is currently running, if diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 0eae1852a91b..bd21367ce73d 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -340,7 +340,7 @@ def _read(self): byte = self.file.read(1)[0] self._dtable[byte](self, byte) if self._missing_font: - raise self._missing_font + raise self._missing_font.to_exception() name = self._dtable[byte].__name__ if name == "_push": down_stack.append(down_stack[-1]) @@ -368,14 +368,14 @@ def _read_arg(self, nbytes, signed=False): @_dispatch(min=0, max=127, state=_dvistate.inpage) def _set_char_immediate(self, char): self._put_char_real(char) - if isinstance(self.fonts[self.f], FileNotFoundError): + if isinstance(self.fonts[self.f], cbook._ExceptionInfo): return self.h += self.fonts[self.f]._width_of(char) @_dispatch(min=128, max=131, state=_dvistate.inpage, args=('olen1',)) def _set_char(self, char): self._put_char_real(char) - if isinstance(self.fonts[self.f], FileNotFoundError): + if isinstance(self.fonts[self.f], cbook._ExceptionInfo): return self.h += self.fonts[self.f]._width_of(char) @@ -390,7 +390,7 @@ def _put_char(self, char): def _put_char_real(self, char): font = self.fonts[self.f] - if isinstance(font, FileNotFoundError): + if isinstance(font, cbook._ExceptionInfo): self._missing_font = font elif font._vf is None: self.text.append(Text(self.h, self.v, font, char, @@ -504,7 +504,7 @@ def _fnt_def_real(self, k, c, s, d, a, l): # and throw that error in Dvi._read. For Vf, _finalize_packet # checks whether a missing glyph has been used, and in that case # skips the glyph definition. - self.fonts[k] = exc.with_traceback(None) + self.fonts[k] = cbook._ExceptionInfo.from_exception(exc) return if c != 0 and tfm.checksum != 0 and c != tfm.checksum: raise ValueError(f'tfm checksum mismatch: {n}') diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 890663381b3d..98731af3463f 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -28,7 +28,6 @@ from __future__ import annotations from base64 import b64encode -from collections import namedtuple import copy import dataclasses from functools import lru_cache @@ -133,8 +132,6 @@ 'sans', } -_ExceptionProxy = namedtuple('_ExceptionProxy', ['klass', 'message']) - # OS Font paths try: _HOME = Path.home() @@ -1355,8 +1352,8 @@ def findfont(self, prop, fontext='ttf', directory=None, ret = self._findfont_cached( prop, fontext, directory, fallback_to_default, rebuild_if_missing, rc_params) - if isinstance(ret, _ExceptionProxy): - raise ret.klass(ret.message) + if isinstance(ret, cbook._ExceptionInfo): + raise ret.to_exception() return ret def get_font_names(self): @@ -1509,7 +1506,7 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, # This return instead of raise is intentional, as we wish to # cache that it was not found, which will not occur if it was # actually raised. - return _ExceptionProxy( + return cbook._ExceptionInfo( ValueError, f"Failed to find font {prop}, and fallback to the default font was " f"disabled" @@ -1535,7 +1532,7 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, # This return instead of raise is intentional, as we wish to # cache that it was not found, which will not occur if it was # actually raised. - return _ExceptionProxy(ValueError, "No valid font could be found") + return cbook._ExceptionInfo(ValueError, "No valid font could be found") return _cached_realpath(result) From fb4d7c33c43ad92b492c5815408e46d0a3e690c9 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 11 Sep 2024 07:56:06 -0400 Subject: [PATCH 0318/1230] Check ndim in check_trailing_shape helpers This was previously checked by using an `array_view`, but moving to pybind11 we won't have that until cast to `unchecked`. --- src/mplutils.h | 12 ++++++++++++ src/numpy_cpp.h | 8 ++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/mplutils.h b/src/mplutils.h index 58eb32d190ec..05c3436626e2 100644 --- a/src/mplutils.h +++ b/src/mplutils.h @@ -71,6 +71,12 @@ inline int prepare_and_add_type(PyTypeObject *type, PyObject *module) template inline bool check_trailing_shape(T array, char const* name, long d1) { + if (array.ndim() != 2) { + PyErr_Format(PyExc_ValueError, + "Expected 2-dimensional array, got %ld", + array.ndim()); + return false; + } if (array.shape(1) != d1) { PyErr_Format(PyExc_ValueError, "%s must have shape (N, %ld), got (%ld, %ld)", @@ -83,6 +89,12 @@ inline bool check_trailing_shape(T array, char const* name, long d1) template inline bool check_trailing_shape(T array, char const* name, long d1, long d2) { + if (array.ndim() != 3) { + PyErr_Format(PyExc_ValueError, + "Expected 3-dimensional array, got %ld", + array.ndim()); + return false; + } if (array.shape(1) != d1 || array.shape(2) != d2) { PyErr_Format(PyExc_ValueError, "%s must have shape (N, %ld, %ld), got (%ld, %ld, %ld)", diff --git a/src/numpy_cpp.h b/src/numpy_cpp.h index 6165789b7603..6b7446337bb7 100644 --- a/src/numpy_cpp.h +++ b/src/numpy_cpp.h @@ -365,10 +365,6 @@ class array_view : public detail::array_view_accessors public: typedef T value_type; - enum { - ndim = ND - }; - array_view() : m_arr(NULL), m_data(NULL) { m_shape = zeros; @@ -492,6 +488,10 @@ class array_view : public detail::array_view_accessors return true; } + npy_intp ndim() const { + return ND; + } + npy_intp shape(size_t i) const { if (i >= ND) { From 45ab00eac5660789f55c3939049c91d7daea4c86 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 13 Sep 2024 02:21:21 -0400 Subject: [PATCH 0319/1230] Port py_adaptors to pybind11 --- src/_backend_agg_wrapper.cpp | 6 +- src/_path_wrapper.cpp | 12 +-- src/py_adaptors.h | 164 ++++++++++++++++++----------------- src/py_converters_11.h | 23 ----- 4 files changed, 87 insertions(+), 118 deletions(-) diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 79cab02e419d..71042be73cc5 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -111,7 +111,7 @@ static void PyRendererAgg_draw_path_collection(RendererAgg *self, GCAgg &gc, agg::trans_affine master_transform, - py::object paths_obj, + mpl::PathGenerator paths, py::object transforms_obj, py::object offsets_obj, agg::trans_affine offset_trans, @@ -124,7 +124,6 @@ PyRendererAgg_draw_path_collection(RendererAgg *self, // offset position is no longer used py::object Py_UNUSED(offset_position_obj)) { - mpl::PathGenerator paths; numpy::array_view transforms; numpy::array_view offsets; numpy::array_view facecolors; @@ -132,9 +131,6 @@ PyRendererAgg_draw_path_collection(RendererAgg *self, numpy::array_view linewidths; numpy::array_view antialiaseds; - if (!convert_pathgen(paths_obj.ptr(), &paths)) { - throw py::error_already_set(); - } if (!convert_transforms(transforms_obj.ptr(), &transforms)) { throw py::error_already_set(); } diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index b4eb5d19177f..83a6402740d4 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -123,17 +123,13 @@ Py_update_path_extents(mpl::PathIterator path, agg::trans_affine trans, static py::tuple Py_get_path_collection_extents(agg::trans_affine master_transform, - py::object paths_obj, py::object transforms_obj, + mpl::PathGenerator paths, py::object transforms_obj, py::object offsets_obj, agg::trans_affine offset_trans) { - mpl::PathGenerator paths; numpy::array_view transforms; numpy::array_view offsets; extent_limits e; - if (!convert_pathgen(paths_obj.ptr(), &paths)) { - throw py::error_already_set(); - } if (!convert_transforms(transforms_obj.ptr(), &transforms)) { throw py::error_already_set(); } @@ -161,18 +157,14 @@ Py_get_path_collection_extents(agg::trans_affine master_transform, static py::object Py_point_in_path_collection(double x, double y, double radius, - agg::trans_affine master_transform, py::object paths_obj, + agg::trans_affine master_transform, mpl::PathGenerator paths, py::object transforms_obj, py::object offsets_obj, agg::trans_affine offset_trans, bool filled) { - mpl::PathGenerator paths; numpy::array_view transforms; numpy::array_view offsets; std::vector result; - if (!convert_pathgen(paths_obj.ptr(), &paths)) { - throw py::error_already_set(); - } if (!convert_transforms(transforms_obj.ptr(), &transforms)) { throw py::error_already_set(); } diff --git a/src/py_adaptors.h b/src/py_adaptors.h index b0cec6c1d004..298943006ce8 100644 --- a/src/py_adaptors.h +++ b/src/py_adaptors.h @@ -8,16 +8,12 @@ * structures to C++ and Agg-friendly interfaces. */ -#include - -#include "numpy/arrayobject.h" +#include +#include #include "agg_basics.h" -#include "py_exceptions.h" -extern "C" { -int convert_path(PyObject *obj, void *pathp); -} +namespace py = pybind11; namespace mpl { @@ -35,8 +31,8 @@ class PathIterator underlying data arrays, so that Python reference counting can work. */ - PyArrayObject *m_vertices; - PyArrayObject *m_codes; + py::array_t m_vertices; + py::array_t m_codes; unsigned m_iterator; unsigned m_total_vertices; @@ -50,38 +46,29 @@ class PathIterator public: inline PathIterator() - : m_vertices(NULL), - m_codes(NULL), - m_iterator(0), + : m_iterator(0), m_total_vertices(0), m_should_simplify(false), m_simplify_threshold(1.0 / 9.0) { } - inline PathIterator(PyObject *vertices, - PyObject *codes, - bool should_simplify, + inline PathIterator(py::object vertices, py::object codes, bool should_simplify, double simplify_threshold) - : m_vertices(NULL), m_codes(NULL), m_iterator(0) + : m_iterator(0) { - if (!set(vertices, codes, should_simplify, simplify_threshold)) - throw mpl::exception(); + set(vertices, codes, should_simplify, simplify_threshold); } - inline PathIterator(PyObject *vertices, PyObject *codes) - : m_vertices(NULL), m_codes(NULL), m_iterator(0) + inline PathIterator(py::object vertices, py::object codes) + : m_iterator(0) { - if (!set(vertices, codes)) - throw mpl::exception(); + set(vertices, codes); } inline PathIterator(const PathIterator &other) { - Py_XINCREF(other.m_vertices); m_vertices = other.m_vertices; - - Py_XINCREF(other.m_codes); m_codes = other.m_codes; m_iterator = 0; @@ -91,47 +78,45 @@ class PathIterator m_simplify_threshold = other.m_simplify_threshold; } - ~PathIterator() - { - Py_XDECREF(m_vertices); - Py_XDECREF(m_codes); - } - - inline int - set(PyObject *vertices, PyObject *codes, bool should_simplify, double simplify_threshold) + inline void + set(py::object vertices, py::object codes, bool should_simplify, double simplify_threshold) { m_should_simplify = should_simplify; m_simplify_threshold = simplify_threshold; - Py_XDECREF(m_vertices); - m_vertices = (PyArrayObject *)PyArray_FromObject(vertices, NPY_DOUBLE, 2, 2); - - if (!m_vertices || PyArray_DIM(m_vertices, 1) != 2) { - PyErr_SetString(PyExc_ValueError, "Invalid vertices array"); - return 0; + m_vertices = vertices.cast>(); + if (m_vertices.ndim() != 2 || m_vertices.shape(1) != 2) { + throw py::value_error("Invalid vertices array"); } + m_total_vertices = m_vertices.shape(0); - Py_XDECREF(m_codes); - m_codes = NULL; - - if (codes != NULL && codes != Py_None) { - m_codes = (PyArrayObject *)PyArray_FromObject(codes, NPY_UINT8, 1, 1); - - if (!m_codes || PyArray_DIM(m_codes, 0) != PyArray_DIM(m_vertices, 0)) { - PyErr_SetString(PyExc_ValueError, "Invalid codes array"); - return 0; + m_codes.release().dec_ref(); + if (!codes.is_none()) { + m_codes = codes.cast>(); + if (m_codes.ndim() != 1 || m_codes.shape(0) != m_total_vertices) { + throw py::value_error("Invalid codes array"); } } - m_total_vertices = (unsigned)PyArray_DIM(m_vertices, 0); m_iterator = 0; + } + inline int + set(PyObject *vertices, PyObject *codes, bool should_simplify, double simplify_threshold) + { + try { + set(py::reinterpret_borrow(vertices), + py::reinterpret_borrow(codes), + should_simplify, simplify_threshold); + } catch(const py::error_already_set &) { + return 0; + } return 1; } - inline int set(PyObject *vertices, PyObject *codes) + inline void set(py::object vertices, py::object codes) { - return set(vertices, codes, false, 0.0); + set(vertices, codes, false, 0.0); } inline unsigned vertex(double *x, double *y) @@ -144,12 +129,11 @@ class PathIterator const size_t idx = m_iterator++; - char *pair = (char *)PyArray_GETPTR2(m_vertices, idx, 0); - *x = *(double *)pair; - *y = *(double *)(pair + PyArray_STRIDE(m_vertices, 1)); + *x = *m_vertices.data(idx, 0); + *y = *m_vertices.data(idx, 1); - if (m_codes != NULL) { - return (unsigned)(*(char *)PyArray_GETPTR1(m_codes, idx)); + if (m_codes) { + return *m_codes.data(idx); } else { return idx == 0 ? agg::path_cmd_move_to : agg::path_cmd_line_to; } @@ -177,42 +161,38 @@ class PathIterator inline bool has_codes() const { - return m_codes != NULL; + return bool(m_codes); } inline void *get_id() { - return (void *)m_vertices; + return (void *)m_vertices.ptr(); } }; class PathGenerator { - PyObject *m_paths; + py::sequence m_paths; Py_ssize_t m_npaths; public: typedef PathIterator path_iterator; - PathGenerator() : m_paths(NULL), m_npaths(0) {} + PathGenerator() : m_npaths(0) {} - ~PathGenerator() + void set(py::object obj) { - Py_XDECREF(m_paths); + m_paths = obj.cast(); + m_npaths = m_paths.size(); } int set(PyObject *obj) { - if (!PySequence_Check(obj)) { + try { + set(py::reinterpret_borrow(obj)); + } catch(const py::error_already_set &) { return 0; } - - Py_XDECREF(m_paths); - m_paths = obj; - Py_INCREF(m_paths); - - m_npaths = PySequence_Size(m_paths); - return 1; } @@ -229,20 +209,44 @@ class PathGenerator path_iterator operator()(size_t i) { path_iterator path; - PyObject *item; - item = PySequence_GetItem(m_paths, i % m_npaths); - if (item == NULL) { - throw mpl::exception(); - } - if (!convert_path(item, &path)) { - Py_DECREF(item); - throw mpl::exception(); - } - Py_DECREF(item); + auto item = m_paths[i % m_npaths]; + path = item.cast(); return path; } }; } +namespace PYBIND11_NAMESPACE { namespace detail { + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(mpl::PathIterator, const_name("PathIterator")); + + bool load(handle src, bool) { + if (src.is_none()) { + return true; + } + + py::object vertices = src.attr("vertices"); + py::object codes = src.attr("codes"); + auto should_simplify = src.attr("should_simplify").cast(); + auto simplify_threshold = src.attr("simplify_threshold").cast(); + + value.set(vertices, codes, should_simplify, simplify_threshold); + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(mpl::PathGenerator, const_name("PathGenerator")); + + bool load(handle src, bool) { + value.set(py::reinterpret_borrow(src)); + return true; + } + }; +}} // namespace PYBIND11_NAMESPACE::detail + #endif diff --git a/src/py_converters_11.h b/src/py_converters_11.h index ef5d8989c072..6bf85733ebfd 100644 --- a/src/py_converters_11.h +++ b/src/py_converters_11.h @@ -167,29 +167,6 @@ namespace PYBIND11_NAMESPACE { namespace detail { return true; } }; - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(mpl::PathIterator, const_name("PathIterator")); - - bool load(handle src, bool) { - if (src.is_none()) { - return true; - } - - py::object vertices = src.attr("vertices"); - py::object codes = src.attr("codes"); - auto should_simplify = src.attr("should_simplify").cast(); - auto simplify_threshold = src.attr("simplify_threshold").cast(); - - if (!value.set(vertices.inc_ref().ptr(), codes.inc_ref().ptr(), - should_simplify, simplify_threshold)) { - throw py::error_already_set(); - } - - return true; - } - }; #endif /* Remove all this macro magic after dropping NumPy usage and just include `_backend_agg_basic_types.h`. */ From 086add18d724fb1ba7d25767938b2c78f72975e4 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 20 Sep 2024 20:21:05 -0400 Subject: [PATCH 0320/1230] Don't enforce trailing shape on empty arrays They need only be the same number of dimensions, as sometimes code does `np.atleast_3d(array)` on something empty, which inserts the 0-dimension in a spot that messes with the expected trailing shape. --- src/mplutils.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/mplutils.h b/src/mplutils.h index 05c3436626e2..cac710617626 100644 --- a/src/mplutils.h +++ b/src/mplutils.h @@ -77,6 +77,11 @@ inline bool check_trailing_shape(T array, char const* name, long d1) array.ndim()); return false; } + if (array.size() == 0) { + // Sometimes things come through as atleast_2d, etc., but they're empty, so + // don't bother enforcing the trailing shape. + return true; + } if (array.shape(1) != d1) { PyErr_Format(PyExc_ValueError, "%s must have shape (N, %ld), got (%ld, %ld)", @@ -95,6 +100,11 @@ inline bool check_trailing_shape(T array, char const* name, long d1, long d2) array.ndim()); return false; } + if (array.size() == 0) { + // Sometimes things come through as atleast_3d, etc., but they're empty, so + // don't bother enforcing the trailing shape. + return true; + } if (array.shape(1) != d1 || array.shape(2) != d2) { PyErr_Format(PyExc_ValueError, "%s must have shape (N, %ld, %ld), got (%ld, %ld, %ld)", From 0305729159b8703c1addd73d53fc19b248916a01 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 13 Sep 2024 19:37:09 -0400 Subject: [PATCH 0321/1230] Convert remaining `array_view` to pybind11 `array_t` --- src/_backend_agg_wrapper.cpp | 65 ++++++++++-------------------------- src/_path.h | 47 +++++++++++++------------- src/_path_wrapper.cpp | 51 ++++++++-------------------- src/mplutils.h | 23 +++++++++++++ src/py_converters_11.h | 32 ++++++++++++++++++ 5 files changed, 110 insertions(+), 108 deletions(-) diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 71042be73cc5..fb241b217fe9 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -112,43 +112,24 @@ PyRendererAgg_draw_path_collection(RendererAgg *self, GCAgg &gc, agg::trans_affine master_transform, mpl::PathGenerator paths, - py::object transforms_obj, - py::object offsets_obj, + py::array_t transforms_obj, + py::array_t offsets_obj, agg::trans_affine offset_trans, - py::object facecolors_obj, - py::object edgecolors_obj, - py::object linewidths_obj, + py::array_t facecolors_obj, + py::array_t edgecolors_obj, + py::array_t linewidths_obj, DashesVector dashes, - py::object antialiaseds_obj, + py::array_t antialiaseds_obj, py::object Py_UNUSED(ignored_obj), // offset position is no longer used py::object Py_UNUSED(offset_position_obj)) { - numpy::array_view transforms; - numpy::array_view offsets; - numpy::array_view facecolors; - numpy::array_view edgecolors; - numpy::array_view linewidths; - numpy::array_view antialiaseds; - - if (!convert_transforms(transforms_obj.ptr(), &transforms)) { - throw py::error_already_set(); - } - if (!convert_points(offsets_obj.ptr(), &offsets)) { - throw py::error_already_set(); - } - if (!convert_colors(facecolors_obj.ptr(), &facecolors)) { - throw py::error_already_set(); - } - if (!convert_colors(edgecolors_obj.ptr(), &edgecolors)) { - throw py::error_already_set(); - } - if (!linewidths.converter(linewidths_obj.ptr(), &linewidths)) { - throw py::error_already_set(); - } - if (!antialiaseds.converter(antialiaseds_obj.ptr(), &antialiaseds)) { - throw py::error_already_set(); - } + auto transforms = convert_transforms(transforms_obj); + auto offsets = convert_points(offsets_obj); + auto facecolors = convert_colors(facecolors_obj); + auto edgecolors = convert_colors(edgecolors_obj); + auto linewidths = linewidths_obj.unchecked<1>(); + auto antialiaseds = antialiaseds_obj.unchecked<1>(); self->draw_path_collection(gc, master_transform, @@ -170,26 +151,16 @@ PyRendererAgg_draw_quad_mesh(RendererAgg *self, unsigned int mesh_width, unsigned int mesh_height, py::array_t coordinates_obj, - py::object offsets_obj, + py::array_t offsets_obj, agg::trans_affine offset_trans, - py::object facecolors_obj, + py::array_t facecolors_obj, bool antialiased, - py::object edgecolors_obj) + py::array_t edgecolors_obj) { - numpy::array_view offsets; - numpy::array_view facecolors; - numpy::array_view edgecolors; - auto coordinates = coordinates_obj.mutable_unchecked<3>(); - if (!convert_points(offsets_obj.ptr(), &offsets)) { - throw py::error_already_set(); - } - if (!convert_colors(facecolors_obj.ptr(), &facecolors)) { - throw py::error_already_set(); - } - if (!convert_colors(edgecolors_obj.ptr(), &edgecolors)) { - throw py::error_already_set(); - } + auto offsets = convert_points(offsets_obj); + auto facecolors = convert_colors(facecolors_obj); + auto edgecolors = convert_colors(edgecolors_obj); self->draw_quad_mesh(gc, master_transform, diff --git a/src/_path.h b/src/_path.h index 7f17d0bc2933..0e1561223442 100644 --- a/src/_path.h +++ b/src/_path.h @@ -245,8 +245,7 @@ inline void points_in_path(PointArray &points, typedef agg::conv_curve curve_t; typedef agg::conv_contour contour_t; - size_t i; - for (i = 0; i < safe_first_shape(points); ++i) { + for (auto i = 0; i < safe_first_shape(points); ++i) { result[i] = false; } @@ -270,10 +269,11 @@ template inline bool point_in_path( double x, double y, const double r, PathIterator &path, agg::trans_affine &trans) { - npy_intp shape[] = {1, 2}; - numpy::array_view points(shape); - points(0, 0) = x; - points(0, 1) = y; + py::ssize_t shape[] = {1, 2}; + py::array_t points_arr(shape); + *points_arr.mutable_data(0, 0) = x; + *points_arr.mutable_data(0, 1) = y; + auto points = points_arr.mutable_unchecked<2>(); int result[1]; result[0] = 0; @@ -292,10 +292,11 @@ inline bool point_on_path( typedef agg::conv_curve curve_t; typedef agg::conv_stroke stroke_t; - npy_intp shape[] = {1, 2}; - numpy::array_view points(shape); - points(0, 0) = x; - points(0, 1) = y; + py::ssize_t shape[] = {1, 2}; + py::array_t points_arr(shape); + *points_arr.mutable_data(0, 0) = x; + *points_arr.mutable_data(0, 1) = y; + auto points = points_arr.mutable_unchecked<2>(); int result[1]; result[0] = 0; @@ -382,20 +383,19 @@ void get_path_collection_extents(agg::trans_affine &master_transform, throw std::runtime_error("Offsets array must have shape (N, 2)"); } - size_t Npaths = paths.size(); - size_t Noffsets = safe_first_shape(offsets); - size_t N = std::max(Npaths, Noffsets); - size_t Ntransforms = std::min(safe_first_shape(transforms), N); - size_t i; + auto Npaths = paths.size(); + auto Noffsets = safe_first_shape(offsets); + auto N = std::max(Npaths, Noffsets); + auto Ntransforms = std::min(safe_first_shape(transforms), N); agg::trans_affine trans; reset_limits(extent); - for (i = 0; i < N; ++i) { + for (auto i = 0; i < N; ++i) { typename PathGenerator::path_iterator path(paths(i % Npaths)); if (Ntransforms) { - size_t ti = i % Ntransforms; + py::ssize_t ti = i % Ntransforms; trans = agg::trans_affine(transforms(ti, 0, 0), transforms(ti, 1, 0), transforms(ti, 0, 1), @@ -429,24 +429,23 @@ void point_in_path_collection(double x, bool filled, std::vector &result) { - size_t Npaths = paths.size(); + auto Npaths = paths.size(); if (Npaths == 0) { return; } - size_t Noffsets = safe_first_shape(offsets); - size_t N = std::max(Npaths, Noffsets); - size_t Ntransforms = std::min(safe_first_shape(transforms), N); - size_t i; + auto Noffsets = safe_first_shape(offsets); + auto N = std::max(Npaths, Noffsets); + auto Ntransforms = std::min(safe_first_shape(transforms), N); agg::trans_affine trans; - for (i = 0; i < N; ++i) { + for (auto i = 0; i < N; ++i) { typename PathGenerator::path_iterator path = paths(i % Npaths); if (Ntransforms) { - size_t ti = i % Ntransforms; + auto ti = i % Ntransforms; trans = agg::trans_affine(transforms(ti, 0, 0), transforms(ti, 1, 0), transforms(ti, 0, 1), diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index 83a6402740d4..d3a42ca0830d 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -44,17 +44,9 @@ static py::array_t Py_points_in_path(py::array_t points_obj, double r, mpl::PathIterator path, agg::trans_affine trans) { - numpy::array_view points; + auto points = convert_points(points_obj); - if (!convert_points(points_obj.ptr(), &points)) { - throw py::error_already_set(); - } - - if (!check_trailing_shape(points, "points", 2)) { - throw py::error_already_set(); - } - - py::ssize_t dims[] = { static_cast(points.size()) }; + py::ssize_t dims[] = { points.shape(0) }; py::array_t results(dims); auto results_mutable = results.mutable_unchecked<1>(); @@ -123,20 +115,15 @@ Py_update_path_extents(mpl::PathIterator path, agg::trans_affine trans, static py::tuple Py_get_path_collection_extents(agg::trans_affine master_transform, - mpl::PathGenerator paths, py::object transforms_obj, - py::object offsets_obj, agg::trans_affine offset_trans) + mpl::PathGenerator paths, + py::array_t transforms_obj, + py::array_t offsets_obj, + agg::trans_affine offset_trans) { - numpy::array_view transforms; - numpy::array_view offsets; + auto transforms = convert_transforms(transforms_obj); + auto offsets = convert_points(offsets_obj); extent_limits e; - if (!convert_transforms(transforms_obj.ptr(), &transforms)) { - throw py::error_already_set(); - } - if (!convert_points(offsets_obj.ptr(), &offsets)) { - throw py::error_already_set(); - } - get_path_collection_extents( master_transform, paths, transforms, offsets, offset_trans, e); @@ -158,20 +145,14 @@ Py_get_path_collection_extents(agg::trans_affine master_transform, static py::object Py_point_in_path_collection(double x, double y, double radius, agg::trans_affine master_transform, mpl::PathGenerator paths, - py::object transforms_obj, py::object offsets_obj, + py::array_t transforms_obj, + py::array_t offsets_obj, agg::trans_affine offset_trans, bool filled) { - numpy::array_view transforms; - numpy::array_view offsets; + auto transforms = convert_transforms(transforms_obj); + auto offsets = convert_points(offsets_obj); std::vector result; - if (!convert_transforms(transforms_obj.ptr(), &transforms)) { - throw py::error_already_set(); - } - if (!convert_points(offsets_obj.ptr(), &offsets)) { - throw py::error_already_set(); - } - point_in_path_collection(x, y, radius, master_transform, paths, transforms, offsets, offset_trans, filled, result); @@ -229,13 +210,9 @@ Py_affine_transform(py::array_t bboxes_obj) { - numpy::array_view bboxes; - - if (!convert_bboxes(bboxes_obj.ptr(), &bboxes)) { - throw py::error_already_set(); - } + auto bboxes = convert_bboxes(bboxes_obj); return count_bboxes_overlapping_bbox(bbox, bboxes); } diff --git a/src/mplutils.h b/src/mplutils.h index cac710617626..b7a80a84429c 100644 --- a/src/mplutils.h +++ b/src/mplutils.h @@ -67,6 +67,10 @@ inline int prepare_and_add_type(PyTypeObject *type, PyObject *module) #ifdef __cplusplus // not for macosx.m // Check that array has shape (N, d1) or (N, d1, d2). We cast d1, d2 to longs // so that we don't need to access the NPY_INTP_FMT macro here. +#include +#include + +namespace py = pybind11; template inline bool check_trailing_shape(T array, char const* name, long d1) @@ -113,6 +117,25 @@ inline bool check_trailing_shape(T array, char const* name, long d1, long d2) } return true; } + +/* In most cases, code should use safe_first_shape(obj) instead of obj.shape(0), since + safe_first_shape(obj) == 0 when any dimension is 0. */ +template +py::ssize_t +safe_first_shape(const py::detail::unchecked_reference &a) +{ + bool empty = (ND == 0); + for (py::ssize_t i = 0; i < ND; i++) { + if (a.shape(i) == 0) { + empty = true; + } + } + if (empty) { + return 0; + } else { + return a.shape(0); + } +} #endif #endif diff --git a/src/py_converters_11.h b/src/py_converters_11.h index 6bf85733ebfd..e57e6072ef79 100644 --- a/src/py_converters_11.h +++ b/src/py_converters_11.h @@ -17,6 +17,38 @@ namespace py = pybind11; void convert_trans_affine(const py::object& transform, agg::trans_affine& affine); +inline auto convert_points(py::array_t obj) +{ + if (!check_trailing_shape(obj, "points", 2)) { + throw py::error_already_set(); + } + return obj.unchecked<2>(); +} + +inline auto convert_transforms(py::array_t obj) +{ + if (!check_trailing_shape(obj, "transforms", 3, 3)) { + throw py::error_already_set(); + } + return obj.unchecked<3>(); +} + +inline auto convert_bboxes(py::array_t obj) +{ + if (!check_trailing_shape(obj, "bbox array", 2, 2)) { + throw py::error_already_set(); + } + return obj.unchecked<3>(); +} + +inline auto convert_colors(py::array_t obj) +{ + if (!check_trailing_shape(obj, "colors", 4)) { + throw py::error_already_set(); + } + return obj.unchecked<2>(); +} + namespace PYBIND11_NAMESPACE { namespace detail { template <> struct type_caster { public: From 2b2baa4f2caf2826c3cf223e6a7a8944d344131e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 20 Sep 2024 21:21:51 -0400 Subject: [PATCH 0322/1230] Use pybind11 type caster for GCAgg.clippath Now that everything else is using pybind11, this works without issue. --- src/py_converters_11.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/py_converters_11.h b/src/py_converters_11.h index e57e6072ef79..3d5673548366 100644 --- a/src/py_converters_11.h +++ b/src/py_converters_11.h @@ -294,8 +294,7 @@ namespace PYBIND11_NAMESPACE { namespace detail { value.join = src.attr("_joinstyle").cast(); value.dashes = src.attr("get_dashes")().cast(); value.cliprect = src.attr("_cliprect").cast(); - /* value.clippath = src.attr("get_clip_path")().cast(); */ - convert_clippath(src.attr("get_clip_path")().ptr(), &value.clippath); + value.clippath = src.attr("get_clip_path")().cast(); value.snap_mode = src.attr("get_snap")().cast(); value.hatchpath = src.attr("get_hatch_path")().cast(); value.hatch_color = src.attr("get_hatch_color")().cast(); From bab748c21a1b9be2a5de8e817995043aac072a88 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 20 Sep 2024 22:07:48 -0400 Subject: [PATCH 0323/1230] Move type casters into files that define their types Since we're using pybind11 everywhere, it should be fine now to access it in any header, and putting the type caster there is clearer. We don't need the weird macro checks to conditionally define them either. --- src/_backend_agg_basic_types.h | 136 ++++++++++++++++++++++++++++ src/path_converters.h | 20 +++++ src/py_converters_11.h | 159 +-------------------------------- 3 files changed, 157 insertions(+), 158 deletions(-) diff --git a/src/_backend_agg_basic_types.h b/src/_backend_agg_basic_types.h index 4fbf846d8cb4..bbd636e68b33 100644 --- a/src/_backend_agg_basic_types.h +++ b/src/_backend_agg_basic_types.h @@ -4,6 +4,9 @@ /* Contains some simple types from the Agg backend that are also used by other modules */ +#include + +#include #include #include "agg_color_rgba.h" @@ -13,6 +16,8 @@ #include "py_adaptors.h" +namespace py = pybind11; + struct ClipPath { mpl::PathIterator path; @@ -121,4 +126,135 @@ class GCAgg GCAgg &operator=(const GCAgg &); }; +namespace PYBIND11_NAMESPACE { namespace detail { + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::line_cap_e, const_name("line_cap_e")); + + bool load(handle src, bool) { + const std::unordered_map enum_values = { + {"butt", agg::butt_cap}, + {"round", agg::round_cap}, + {"projecting", agg::square_cap}, + }; + value = enum_values.at(src.cast()); + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::line_join_e, const_name("line_join_e")); + + bool load(handle src, bool) { + const std::unordered_map enum_values = { + {"miter", agg::miter_join_revert}, + {"round", agg::round_join}, + {"bevel", agg::bevel_join}, + }; + value = agg::miter_join_revert; + value = enum_values.at(src.cast()); + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(ClipPath, const_name("ClipPath")); + + bool load(handle src, bool) { + if (src.is_none()) { + return true; + } + + auto clippath_tuple = src.cast(); + + auto path = clippath_tuple[0]; + if (!path.is_none()) { + value.path = path.cast(); + } + value.trans = clippath_tuple[1].cast(); + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(Dashes, const_name("Dashes")); + + bool load(handle src, bool) { + auto dash_tuple = src.cast(); + auto dash_offset = dash_tuple[0].cast(); + auto dashes_seq_or_none = dash_tuple[1]; + + if (dashes_seq_or_none.is_none()) { + return true; + } + + auto dashes_seq = dashes_seq_or_none.cast(); + + auto nentries = dashes_seq.size(); + // If the dashpattern has odd length, iterate through it twice (in + // accordance with the pdf/ps/svg specs). + auto dash_pattern_length = (nentries % 2) ? 2 * nentries : nentries; + + for (py::size_t i = 0; i < dash_pattern_length; i += 2) { + auto length = dashes_seq[i % nentries].cast(); + auto skip = dashes_seq[(i + 1) % nentries].cast(); + + value.add_dash_pair(length, skip); + } + + value.set_dash_offset(dash_offset); + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(SketchParams, const_name("SketchParams")); + + bool load(handle src, bool) { + if (src.is_none()) { + value.scale = 0.0; + value.length = 0.0; + value.randomness = 0.0; + return true; + } + + auto params = src.cast>(); + std::tie(value.scale, value.length, value.randomness) = params; + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(GCAgg, const_name("GCAgg")); + + bool load(handle src, bool) { + value.linewidth = src.attr("_linewidth").cast(); + value.alpha = src.attr("_alpha").cast(); + value.forced_alpha = src.attr("_forced_alpha").cast(); + value.color = src.attr("_rgb").cast(); + value.isaa = src.attr("_antialiased").cast(); + value.cap = src.attr("_capstyle").cast(); + value.join = src.attr("_joinstyle").cast(); + value.dashes = src.attr("get_dashes")().cast(); + value.cliprect = src.attr("_cliprect").cast(); + value.clippath = src.attr("get_clip_path")().cast(); + value.snap_mode = src.attr("get_snap")().cast(); + value.hatchpath = src.attr("get_hatch_path")().cast(); + value.hatch_color = src.attr("get_hatch_color")().cast(); + value.hatch_linewidth = src.attr("get_hatch_linewidth")().cast(); + value.sketch = src.attr("get_sketch_params")().cast(); + + return true; + } + }; +}} // namespace PYBIND11_NAMESPACE::detail + #endif diff --git a/src/path_converters.h b/src/path_converters.h index 6d242e74415b..6877ab6ed4c3 100644 --- a/src/path_converters.h +++ b/src/path_converters.h @@ -3,6 +3,8 @@ #ifndef MPL_PATH_CONVERTERS_H #define MPL_PATH_CONVERTERS_H +#include + #include #include #include @@ -530,6 +532,24 @@ enum e_snap_mode { SNAP_TRUE }; +namespace PYBIND11_NAMESPACE { namespace detail { + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(e_snap_mode, const_name("e_snap_mode")); + + bool load(handle src, bool) { + if (src.is_none()) { + value = SNAP_AUTO; + return true; + } + + value = src.cast() ? SNAP_TRUE : SNAP_FALSE; + + return true; + } + }; +}} // namespace PYBIND11_NAMESPACE::detail + template class PathSnapper { diff --git a/src/py_converters_11.h b/src/py_converters_11.h index 3d5673548366..b093f71b181a 100644 --- a/src/py_converters_11.h +++ b/src/py_converters_11.h @@ -8,12 +8,10 @@ namespace py = pybind11; -#include - #include "agg_basics.h" #include "agg_color_rgba.h" #include "agg_trans_affine.h" -#include "path_converters.h" +#include "mplutils.h" void convert_trans_affine(const py::object& transform, agg::trans_affine& affine); @@ -150,161 +148,6 @@ namespace PYBIND11_NAMESPACE { namespace detail { return true; } }; - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(e_snap_mode, const_name("e_snap_mode")); - - bool load(handle src, bool) { - if (src.is_none()) { - value = SNAP_AUTO; - return true; - } - - value = src.cast() ? SNAP_TRUE : SNAP_FALSE; - - return true; - } - }; - -/* Remove all this macro magic after dropping NumPy usage and just include `py_adaptors.h`. */ -#ifdef MPL_PY_ADAPTORS_H - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(agg::line_cap_e, const_name("line_cap_e")); - - bool load(handle src, bool) { - const std::unordered_map enum_values = { - {"butt", agg::butt_cap}, - {"round", agg::round_cap}, - {"projecting", agg::square_cap}, - }; - value = enum_values.at(src.cast()); - return true; - } - }; - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(agg::line_join_e, const_name("line_join_e")); - - bool load(handle src, bool) { - const std::unordered_map enum_values = { - {"miter", agg::miter_join_revert}, - {"round", agg::round_join}, - {"bevel", agg::bevel_join}, - }; - value = agg::miter_join_revert; - value = enum_values.at(src.cast()); - return true; - } - }; -#endif - -/* Remove all this macro magic after dropping NumPy usage and just include `_backend_agg_basic_types.h`. */ -#ifdef MPL_BACKEND_AGG_BASIC_TYPES_H -# ifndef MPL_PY_ADAPTORS_H -# error "py_adaptors.h must be included to get Agg type casters" -# endif - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(ClipPath, const_name("ClipPath")); - - bool load(handle src, bool) { - if (src.is_none()) { - return true; - } - - auto clippath_tuple = src.cast(); - - auto path = clippath_tuple[0]; - if (!path.is_none()) { - value.path = path.cast(); - } - value.trans = clippath_tuple[1].cast(); - - return true; - } - }; - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(Dashes, const_name("Dashes")); - - bool load(handle src, bool) { - auto dash_tuple = src.cast(); - auto dash_offset = dash_tuple[0].cast(); - auto dashes_seq_or_none = dash_tuple[1]; - - if (dashes_seq_or_none.is_none()) { - return true; - } - - auto dashes_seq = dashes_seq_or_none.cast(); - - auto nentries = dashes_seq.size(); - // If the dashpattern has odd length, iterate through it twice (in - // accordance with the pdf/ps/svg specs). - auto dash_pattern_length = (nentries % 2) ? 2 * nentries : nentries; - - for (py::size_t i = 0; i < dash_pattern_length; i += 2) { - auto length = dashes_seq[i % nentries].cast(); - auto skip = dashes_seq[(i + 1) % nentries].cast(); - - value.add_dash_pair(length, skip); - } - - value.set_dash_offset(dash_offset); - - return true; - } - }; - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(SketchParams, const_name("SketchParams")); - - bool load(handle src, bool) { - if (src.is_none()) { - value.scale = 0.0; - value.length = 0.0; - value.randomness = 0.0; - return true; - } - - auto params = src.cast>(); - std::tie(value.scale, value.length, value.randomness) = params; - - return true; - } - }; - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(GCAgg, const_name("GCAgg")); - - bool load(handle src, bool) { - value.linewidth = src.attr("_linewidth").cast(); - value.alpha = src.attr("_alpha").cast(); - value.forced_alpha = src.attr("_forced_alpha").cast(); - value.color = src.attr("_rgb").cast(); - value.isaa = src.attr("_antialiased").cast(); - value.cap = src.attr("_capstyle").cast(); - value.join = src.attr("_joinstyle").cast(); - value.dashes = src.attr("get_dashes")().cast(); - value.cliprect = src.attr("_cliprect").cast(); - value.clippath = src.attr("get_clip_path")().cast(); - value.snap_mode = src.attr("get_snap")().cast(); - value.hatchpath = src.attr("get_hatch_path")().cast(); - value.hatch_color = src.attr("get_hatch_color")().cast(); - value.hatch_linewidth = src.attr("get_hatch_linewidth")().cast(); - value.sketch = src.attr("get_sketch_params")().cast(); - - return true; - } - }; -#endif }} // namespace PYBIND11_NAMESPACE::detail #endif /* MPL_PY_CONVERTERS_11_H */ From 1818c7bbb4cae5e5a173268ab840eeb414306116 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 20 Sep 2024 22:39:11 -0400 Subject: [PATCH 0324/1230] Convert _path.is_sorted_and_has_non_nan to pybind11 --- src/_path.h | 10 ++++------ src/_path_wrapper.cpp | 45 +++++++++++++++---------------------------- 2 files changed, 19 insertions(+), 36 deletions(-) diff --git a/src/_path.h b/src/_path.h index 0e1561223442..693862c7a829 100644 --- a/src/_path.h +++ b/src/_path.h @@ -1223,17 +1223,15 @@ bool convert_to_string(PathIterator &path, } template -bool is_sorted_and_has_non_nan(PyArrayObject *array) +bool is_sorted_and_has_non_nan(py::array_t array) { - char* ptr = PyArray_BYTES(array); - npy_intp size = PyArray_DIM(array, 0), - stride = PyArray_STRIDE(array, 0); + auto size = array.shape(0); using limits = std::numeric_limits; T last = limits::has_infinity ? -limits::infinity() : limits::min(); bool found_non_nan = false; - for (npy_intp i = 0; i < size; ++i, ptr += stride) { - T current = *(T*)ptr; + for (auto i = 0; i < size; ++i) { + T current = *array.data(i); // The following tests !isnan(current), but also works for integral // types. (The isnan(IntegralType) overload is absent on MSVC.) if (current == current) { diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index d3a42ca0830d..13431601e5af 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -350,41 +350,26 @@ Py_is_sorted_and_has_non_nan(py::object obj) { bool result; - PyArrayObject *array = (PyArrayObject *)PyArray_CheckFromAny( - obj.ptr(), NULL, 1, 1, NPY_ARRAY_NOTSWAPPED, NULL); - - if (array == NULL) { - throw py::error_already_set(); + py::array array = py::array::ensure(obj); + if (array.ndim() != 1) { + throw std::invalid_argument("array must be 1D"); } + auto dtype = array.dtype(); /* Handle just the most common types here, otherwise coerce to double */ - switch (PyArray_TYPE(array)) { - case NPY_INT: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_LONG: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_LONGLONG: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_FLOAT: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_DOUBLE: - result = is_sorted_and_has_non_nan(array); - break; - default: - Py_DECREF(array); - array = (PyArrayObject *)PyArray_FromObject(obj.ptr(), NPY_DOUBLE, 1, 1); - if (array == NULL) { - throw py::error_already_set(); - } - result = is_sorted_and_has_non_nan(array); + if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else { + array = py::array_t::ensure(obj); + result = is_sorted_and_has_non_nan(array); } - Py_DECREF(array); - return result; } From 37206c2fc1a9fe174ca0a4933d3b395b84395e84 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 1 Oct 2024 22:22:40 -0400 Subject: [PATCH 0325/1230] Clean up some pybind11 Agg type casters Co-authored-by: Antony Lee --- src/_backend_agg_basic_types.h | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/_backend_agg_basic_types.h b/src/_backend_agg_basic_types.h index bbd636e68b33..e3e6be9a4532 100644 --- a/src/_backend_agg_basic_types.h +++ b/src/_backend_agg_basic_types.h @@ -152,7 +152,6 @@ namespace PYBIND11_NAMESPACE { namespace detail { {"round", agg::round_join}, {"bevel", agg::bevel_join}, }; - value = agg::miter_join_revert; value = enum_values.at(src.cast()); return true; } @@ -167,13 +166,12 @@ namespace PYBIND11_NAMESPACE { namespace detail { return true; } - auto clippath_tuple = src.cast(); - - auto path = clippath_tuple[0]; - if (!path.is_none()) { - value.path = path.cast(); + auto [path, trans] = + src.cast, agg::trans_affine>>(); + if (path) { + value.path = *path; } - value.trans = clippath_tuple[1].cast(); + value.trans = trans; return true; } @@ -184,15 +182,14 @@ namespace PYBIND11_NAMESPACE { namespace detail { PYBIND11_TYPE_CASTER(Dashes, const_name("Dashes")); bool load(handle src, bool) { - auto dash_tuple = src.cast(); - auto dash_offset = dash_tuple[0].cast(); - auto dashes_seq_or_none = dash_tuple[1]; + auto [dash_offset, dashes_seq_or_none] = + src.cast>>(); - if (dashes_seq_or_none.is_none()) { + if (!dashes_seq_or_none) { return true; } - auto dashes_seq = dashes_seq_or_none.cast(); + auto dashes_seq = *dashes_seq_or_none; auto nentries = dashes_seq.size(); // If the dashpattern has odd length, iterate through it twice (in From 7d73e2a426338c09b836eac218c2be7c27059258 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 27 Sep 2024 21:04:25 -0400 Subject: [PATCH 0326/1230] Agg: Use 32-bit scan line classes When rendering objects, Agg rasterizes them into scan line objects (an x/y point, horizontal length, and colour), and the renderer class writes those to the pixels in the final buffer. Though we have determined that Agg buffers cannot be larger than 2**16, the scan line classes that we use contain 16-bit _signed_ integers internally, cutting off positive values at half the maximum. Since the renderer uses 32-bit integers, this can cause odd behaviour where any horizontal span that _starts_ before 2**15 (such as a horizontal spine) is rendered correctly even if it cross that point, but those that start after (such as a vertical spine or any portion of an angled line) end up clipped. For example, see how the spines and lines break in #28893. A similar problem occurs for resampled images, which also uses Agg scanlines internally. See the breakage in #26368 for an example. The example in that issue also contains horizontal spines that are wider than 2**15, which also exhibit strange behaviour. By moving to 32-bit scan lines, positions and lengths of the lines will no longer be clipped, and this fixes rendering on very large figures. Fixes #23826 Fixes #26368 Fixes #28893 --- src/_backend_agg.h | 6 +++--- src/_image_resample.h | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/_backend_agg.h b/src/_backend_agg.h index 5549978cfb80..a0147f9832c3 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -117,10 +117,10 @@ class RendererAgg typedef agg::renderer_scanline_bin_solid renderer_bin; typedef agg::rasterizer_scanline_aa rasterizer; - typedef agg::scanline_p8 scanline_p8; - typedef agg::scanline_bin scanline_bin; + typedef agg::scanline32_p8 scanline_p8; + typedef agg::scanline32_bin scanline_bin; typedef agg::amask_no_clip_gray8 alpha_mask_type; - typedef agg::scanline_u8_am scanline_am; + typedef agg::scanline32_u8_am scanline_am; typedef agg::renderer_base renderer_base_alpha_mask_type; typedef agg::renderer_scanline_aa_solid renderer_alpha_mask_type; diff --git a/src/_image_resample.h b/src/_image_resample.h index ddf1a4050325..dbc6171f3ec2 100644 --- a/src/_image_resample.h +++ b/src/_image_resample.h @@ -712,6 +712,7 @@ void resample( using renderer_t = agg::renderer_base; using rasterizer_t = agg::rasterizer_scanline_aa; + using scanline_t = agg::scanline32_u8; using reflect_t = agg::wrap_mode_reflect; using image_accessor_t = agg::image_accessor_wrap; @@ -739,7 +740,7 @@ void resample( span_alloc_t span_alloc; rasterizer_t rasterizer; - agg::scanline_u8 scanline; + scanline_t scanline; span_conv_alpha_t conv_alpha(params.alpha); From 474bb4f156c0df6766ad66490693ab785bdc13de Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 1 Oct 2024 20:25:25 -0400 Subject: [PATCH 0327/1230] Agg: Fix overflow when splitting large lines For very large Figures with a single Axes, the horizontal spines may be long enough to need splitting into separate lines. For large enough coordinates, the `x1+x2` calculation may overflow, flipping the sign bit, causing the coordinates to become negative. This causes an infinite loop as some internal condition is never met. By dividing `x1`/`x2` by 2 first, we avoid the overflow, and can calculate the split point correctly. --- extern/agg24-svn/include/agg_rasterizer_cells_aa.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extern/agg24-svn/include/agg_rasterizer_cells_aa.h b/extern/agg24-svn/include/agg_rasterizer_cells_aa.h index d1cc705405dc..44a55417b492 100644 --- a/extern/agg24-svn/include/agg_rasterizer_cells_aa.h +++ b/extern/agg24-svn/include/agg_rasterizer_cells_aa.h @@ -325,8 +325,10 @@ namespace agg if(dx >= dx_limit || dx <= -dx_limit) { - int cx = (x1 + x2) >> 1; - int cy = (y1 + y2) >> 1; + // These are overflow safe versions of (x1 + x2) >> 1; divide each by 2 + // first, then add 1 if both were odd. + int cx = (x1 >> 1) + (x2 >> 1) + ((x1 & 1) & (x2 & 1)); + int cy = (y1 >> 1) + (y2 >> 1) + ((y1 & 1) & (y2 & 1)); line(x1, y1, cx, cy); line(cx, cy, x2, y2); return; From 9259989e017e601e36d06f53ef1cda3547be82c5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 2 Oct 2024 03:38:35 -0400 Subject: [PATCH 0328/1230] Agg: Increase maximum size to 2**23 With 32-bit scan lines, we are able to take advantage of the full 32-bit range of coordinates. However, as noted in `extern/agg24-svn/include/agg_rasterizer_scanline_aa.h`, Agg uses 24.8-bit fixed point coordinates. With one bit taken for the sign, the maximum coordinate is now 2**23. --- doc/users/next_whats_new/increased_figure_limits.rst | 9 +++++++++ lib/matplotlib/tests/test_agg.py | 2 +- src/_backend_agg.cpp | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 doc/users/next_whats_new/increased_figure_limits.rst diff --git a/doc/users/next_whats_new/increased_figure_limits.rst b/doc/users/next_whats_new/increased_figure_limits.rst new file mode 100644 index 000000000000..499701cbca38 --- /dev/null +++ b/doc/users/next_whats_new/increased_figure_limits.rst @@ -0,0 +1,9 @@ +Increased Figure limits with Agg renderer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Figures using the Agg renderer are now limited to 2**23 pixels in each +direction, instead of 2**16. Additionally, bugs that caused artists to not +render past 2**15 pixels horizontally have been fixed. + +Note that if you are using a GUI backend, it may have its own smaller limits +(which may themselves depend on screen size.) diff --git a/lib/matplotlib/tests/test_agg.py b/lib/matplotlib/tests/test_agg.py index 80c1f165382c..59387793605a 100644 --- a/lib/matplotlib/tests/test_agg.py +++ b/lib/matplotlib/tests/test_agg.py @@ -199,7 +199,7 @@ def process_image(self, padded_src, dpi): def test_too_large_image(): - fig = plt.figure(figsize=(300, 1000)) + fig = plt.figure(figsize=(300, 2**25)) buff = io.BytesIO() with pytest.raises(ValueError): fig.savefig(buff) diff --git a/src/_backend_agg.cpp b/src/_backend_agg.cpp index 3460b429ec12..eed27323ba9e 100644 --- a/src/_backend_agg.cpp +++ b/src/_backend_agg.cpp @@ -33,10 +33,10 @@ RendererAgg::RendererAgg(unsigned int width, unsigned int height, double dpi) throw std::range_error("dpi must be positive"); } - if (width >= 1 << 16 || height >= 1 << 16) { + if (width >= 1 << 23 || height >= 1 << 23) { throw std::range_error( "Image size of " + std::to_string(width) + "x" + std::to_string(height) + - " pixels is too large. It must be less than 2^16 in each direction."); + " pixels is too large. It must be less than 2^23 in each direction."); } unsigned stride(width * 4); From cf84d9a6410dec07f57916905c9f54bf00d22b2e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 2 Oct 2024 16:23:38 -0400 Subject: [PATCH 0329/1230] ci: Bump build image on AppVeyor to MSVC 2019 (#28869) * ci: Bump build image on AppVeyor to MSVC 2019 According to the SciPy toolchain roadmap [1], we should be supporting at minimum MSVC 2019. The AppVeyor image has been held back to MSVC 2017 (probably just forgotten since it didn't complain), which is starting to cause issues for more modern code. [1] https://docs.scipy.org/doc/scipy/dev/toolchain.html * ci: Pin micromamba on AppVeyor to v1 Version 2 appears to be broken WRT installing PyPI packages: https://github.com/mamba-org/mamba/issues/3467 --- .appveyor.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 63746ab2b372..da2aec993979 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -17,7 +17,7 @@ skip_commits: clone_depth: 50 -image: Visual Studio 2017 +image: Visual Studio 2019 environment: @@ -45,8 +45,9 @@ cache: init: - ps: + # Pinned due to https://github.com/mamba-org/mamba/issues/3467 Invoke-Webrequest - -URI https://micro.mamba.pm/api/micromamba/win-64/latest + -URI https://github.com/mamba-org/micromamba-releases/releases/download/1.5.10-0/micromamba-win-64.tar.bz2 -OutFile C:\projects\micromamba.tar.bz2 - ps: C:\PROGRA~1\7-Zip\7z.exe x C:\projects\micromamba.tar.bz2 -aoa -oC:\projects\ - ps: C:\PROGRA~1\7-Zip\7z.exe x C:\projects\micromamba.tar -ttar -aoa -oC:\projects\ From ab09fcc97c9ad791ba41dcfdd4276f634585263a Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 15 Aug 2024 13:36:36 +0200 Subject: [PATCH 0330/1230] Backport PR #28689: ci: Enable testing on Python 3.13 --- .github/workflows/tests.yml | 54 +++++++++++++++++-- lib/matplotlib/tests/test_axes.py | 4 +- lib/matplotlib/tests/test_contour.py | 2 +- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 2 +- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 634c83fa57fd..cd4c08e29fb9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -88,14 +88,31 @@ jobs: pyqt6-ver: '!=6.6.0' # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 pyside6-ver: '!=6.5.1' - - os: macos-12 # This runnre is on Intel chips. - python-version: 3.9 + - os: ubuntu-22.04 + python-version: '3.13' + # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html + pyqt6-ver: '!=6.6.0' + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 + pyside6-ver: '!=6.5.1' + - name-suffix: "Free-threaded" + os: ubuntu-22.04 + python-version: '3.13t' + # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html + pyqt6-ver: '!=6.6.0' + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 + pyside6-ver: '!=6.5.1' + - os: macos-12 # This runner is on Intel chips. + python-version: '3.9' # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 pyside6-ver: '!=6.5.1' - os: macos-14 # This runner is on M1 (arm64) chips. python-version: '3.12' # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 pyside6-ver: '!=6.5.1' + - os: macos-14 # This runner is on M1 (arm64) chips. + python-version: '3.13' + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346 + pyside6-ver: '!=6.5.1' steps: - uses: actions/checkout@v4 @@ -104,8 +121,17 @@ jobs: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 + if: matrix.python-version != '3.13t' with: python-version: ${{ matrix.python-version }} + allow-prereleases: true + + - name: Set up Python ${{ matrix.python-version }} + uses: deadsnakes/action@6c8b9b82fe0b4344f4b98f2775fcc395df45e494 # v3.1.0 + if: matrix.python-version == '3.13t' + with: + python-version: '3.13' + nogil: true - name: Install OS dependencies run: | @@ -152,6 +178,11 @@ jobs: texlive-luatex \ texlive-pictures \ texlive-xetex + if [[ "${{ matrix.python-version }}" = '3.13t' ]]; then + # TODO: Remove this once setup-python supports nogil distributions. + sudo apt-get install -yy --no-install-recommends \ + python3.13-tk-nogil + fi if [[ "${{ matrix.os }}" = ubuntu-20.04 ]]; then sudo apt-get install -yy --no-install-recommends libopengl0 else # ubuntu-22.04 @@ -202,6 +233,15 @@ jobs: 4-${{ runner.os }}-py${{ matrix.python-version }}-mpl-${{ github.ref }}- 4-${{ runner.os }}-py${{ matrix.python-version }}-mpl- + - name: Install the nightly dependencies + if: matrix.python-version == '3.13t' + run: | + python -m pip install pytz tzdata python-dateutil # Must be installed for Pandas. + python -m pip install \ + --pre \ + --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \ + --upgrade --only-binary=:all: numpy pandas pillow contourpy + - name: Install Python dependencies run: | # Upgrade pip and setuptools and wheel to get as clean an install as @@ -227,6 +267,7 @@ jobs: # Sphinx is needed to run sphinxext tests python -m pip install --upgrade sphinx!=6.1.2 + if [[ "${{ matrix.python-version }}" != '3.13t' ]]; then # GUI toolkits are pip-installable only for some versions of Python # so don't fail if we can't install them. Make it easier to check # whether the install was successful by trying to import the toolkit @@ -246,11 +287,11 @@ jobs: python -c 'import PyQt5.QtCore' && echo 'PyQt5 is available' || echo 'PyQt5 is not available' - # Even though PySide2 wheels can be installed on Python 3.12, they are broken and since PySide2 is + # Even though PySide2 wheels can be installed on Python 3.12+, they are broken and since PySide2 is # deprecated, they are unlikely to be fixed. For the same deprecation reason, there are no wheels # on M1 macOS, so don't bother there either. if [[ "${{ matrix.os }}" != 'macos-14' - && "${{ matrix.python-version }}" != '3.12' ]]; then + && "${{ matrix.python-version }}" != '3.12' && "${{ matrix.python-version }}" != '3.13' ]]; then python -mpip install --upgrade pyside2${{ matrix.pyside2-ver }} && python -c 'import PySide2.QtCore' && echo 'PySide2 is available' || @@ -272,6 +313,8 @@ jobs: echo 'wxPython is available' || echo 'wxPython is not available' + fi # Skip backends on Python 3.13t. + - name: Install the nightly dependencies # Only install the nightly dependencies during the scheduled event if: github.event_name == 'schedule' && matrix.name-suffix != '(Minimum Versions)' @@ -310,6 +353,9 @@ jobs: - name: Run pytest run: | + if [[ "${{ matrix.python-version }}" == '3.13t' ]]; then + export PYTHON_GIL=0 + fi pytest -rfEsXR -n auto \ --maxfail=50 --timeout=300 --durations=25 \ --cov-report=xml --cov=lib --log-level=DEBUG --color=yes diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 3ec9923c0840..e99ef129eb9a 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1186,7 +1186,7 @@ def test_imshow(): @image_comparison( ['imshow_clip'], style='mpl20', - tol=1.24 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) + tol=1.24 if platform.machine() in ('aarch64', 'arm64', 'ppc64le', 's390x') else 0) def test_imshow_clip(): # As originally reported by Gellule Xg # use former defaults to match existing baseline image @@ -2570,7 +2570,7 @@ def test_contour_hatching(): @image_comparison( ['contour_colorbar'], style='mpl20', - tol=0.54 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) + tol=0.54 if platform.machine() in ('aarch64', 'arm64', 'ppc64le', 's390x') else 0) def test_contour_colorbar(): x, y, z = contour_dat() diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index d4600a14fe1c..0622c099a20c 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -442,7 +442,7 @@ def test_contourf_log_extension(split_collections): @pytest.mark.parametrize("split_collections", [False, True]) @image_comparison( ['contour_addlines.png'], remove_text=True, style='mpl20', - tol=0.15 if platform.machine() in ('aarch64', 'ppc64le', 's390x') + tol=0.15 if platform.machine() in ('aarch64', 'arm64', 'ppc64le', 's390x') else 0.03) # tolerance is because image changed minutely when tick finding on # colorbars was cleaned up... diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index ecb51b724c27..ff5ab230ef06 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -221,7 +221,7 @@ def test_bar3d_lightsource(): @mpl3d_image_comparison( ['contour3d.png'], style='mpl20', - tol=0.002 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) + tol=0.002 if platform.machine() in ('aarch64', 'arm64', 'ppc64le', 's390x') else 0) def test_contour3d(): plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated fig = plt.figure() From bb489d7307445ada13427dba95aa8f47a5409c80 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 2 Oct 2024 16:27:12 -0400 Subject: [PATCH 0331/1230] TST: handle change in pytest.importorskip behavior It now warns if the module is found, but fails to import (rather than not existing and raising ModuleNotFound). --- lib/matplotlib/tests/test_rcparams.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 25ae258ffcbb..0aa3ec0ba603 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -5,6 +5,7 @@ from unittest import mock from cycler import cycler, Cycler +from packaging.version import parse as parse_version import pytest import matplotlib as mpl @@ -539,7 +540,12 @@ def test_backend_fallback_headless(tmp_path): sys.platform == "linux" and not _c_internal_utils.xdisplay_is_valid(), reason="headless") def test_backend_fallback_headful(tmp_path): - pytest.importorskip("tkinter") + if parse_version(pytest.__version__) >= parse_version('8.2.0'): + pytest_kwargs = dict(exc_type=ImportError) + else: + pytest_kwargs = {} + + pytest.importorskip("tkinter", **pytest_kwargs) env = {**os.environ, "MPLBACKEND": "", "MPLCONFIGDIR": str(tmp_path)} backend = subprocess_run_for_testing( [sys.executable, "-c", From c5e33874d00a40ae72865418b640056ef74de862 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 3 Oct 2024 16:41:19 -0400 Subject: [PATCH 0332/1230] TST: Increase test_axes3d_primary_views tolerance on all macOS Apparently, Azure updated from macOS 12 to 14, which caused the change in results outside of the Intel->ARM change. --- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 0afcae99c980..d97637636f10 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -1,6 +1,7 @@ import functools import itertools import platform +import sys import pytest @@ -115,7 +116,7 @@ def test_axes3d_repr(): @mpl3d_image_comparison(['axes3d_primary_views.png'], style='mpl20', - tol=0.05 if platform.machine() == "arm64" else 0) + tol=0.05 if sys.platform == "darwin" else 0) def test_axes3d_primary_views(): # (elev, azim, roll) views = [(90, -90, 0), # XY From 04c7dbe0975ce28f8d936225fefa1575b809fba2 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 3 Oct 2024 18:44:02 -0400 Subject: [PATCH 0333/1230] DOC: Fix invalid rcParam references These have either been deprecated and removed, or just simple typos. --- doc/api/prev_api_changes/api_changes_2.2.0.rst | 2 +- doc/api/prev_api_changes/api_changes_3.1.0.rst | 4 ++-- doc/api/prev_api_changes/api_changes_3.2.0/removals.rst | 2 +- doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst | 6 +++--- doc/api/prev_api_changes/api_changes_3.5.0/removals.rst | 3 +-- doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst | 4 ++-- doc/devel/MEP/MEP23.rst | 4 ++-- doc/users/prev_whats_new/whats_new_1.3.rst | 6 +++--- doc/users/prev_whats_new/whats_new_1.5.rst | 4 ++-- doc/users/prev_whats_new/whats_new_3.1.0.rst | 3 +-- doc/users/prev_whats_new/whats_new_3.3.0.rst | 4 ++-- doc/users/prev_whats_new/whats_new_3.6.0.rst | 2 +- doc/users/prev_whats_new/whats_new_3.8.0.rst | 2 +- lib/matplotlib/axis.py | 2 +- lib/matplotlib/collections.py | 4 ++-- lib/matplotlib/contour.py | 2 +- lib/matplotlib/dates.py | 4 ++-- 17 files changed, 28 insertions(+), 30 deletions(-) diff --git a/doc/api/prev_api_changes/api_changes_2.2.0.rst b/doc/api/prev_api_changes/api_changes_2.2.0.rst index 83369b66f8ab..404d0ca3ba38 100644 --- a/doc/api/prev_api_changes/api_changes_2.2.0.rst +++ b/doc/api/prev_api_changes/api_changes_2.2.0.rst @@ -61,7 +61,7 @@ the future, only broadcasting (1 column to *n* columns) will be performed. rcparams ~~~~~~~~ -The :rc:`backend.qt4` and :rc:`backend.qt5` rcParams were deprecated +The ``backend.qt4`` and ``backend.qt5`` rcParams were deprecated in version 2.2. In order to force the use of a specific Qt binding, either import that binding first, or set the ``QT_API`` environment variable. diff --git a/doc/api/prev_api_changes/api_changes_3.1.0.rst b/doc/api/prev_api_changes/api_changes_3.1.0.rst index 5b06af781938..365476f54e3c 100644 --- a/doc/api/prev_api_changes/api_changes_3.1.0.rst +++ b/doc/api/prev_api_changes/api_changes_3.1.0.rst @@ -743,8 +743,8 @@ The following signature related behaviours are deprecated: `.Axes.annotate()` instead. - Passing (n, 1)-shaped error arrays to `.Axes.errorbar()`, which was not documented and did not work for ``n = 2``. Pass a 1D array instead. -- The *frameon* kwarg to `~.Figure.savefig` and the :rc:`savefig.frameon` rcParam. - To emulate ``frameon = False``, set *facecolor* to fully +- The *frameon* keyword argument to `~.Figure.savefig` and the ``savefig.frameon`` + rcParam. To emulate ``frameon = False``, set *facecolor* to fully transparent (``"none"``, or ``(0, 0, 0, 0)``). - Passing a non-1D (typically, (n, 1)-shaped) input to `.Axes.pie`. Pass a 1D array instead. diff --git a/doc/api/prev_api_changes/api_changes_3.2.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.2.0/removals.rst index 8e4c1e81f9ec..53d76d667509 100644 --- a/doc/api/prev_api_changes/api_changes_3.2.0/removals.rst +++ b/doc/api/prev_api_changes/api_changes_3.2.0/removals.rst @@ -61,7 +61,7 @@ The following API elements have been removed: - passing ``(verts, 0)`` or ``(..., 3)`` when specifying a marker to specify a path or a circle, respectively (instead, use ``verts`` or ``"o"``, respectively) -- :rc:`examples.directory` +- the ``examples.directory`` rcParam The following members of ``matplotlib.backends.backend_pdf.PdfFile`` were removed: diff --git a/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst index 256c33ed762f..76c43b12aaaa 100644 --- a/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst +++ b/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst @@ -83,8 +83,8 @@ Passing both singular and plural *colors*, *linewidths*, *linestyles* to `.Axes. Passing e.g. both *linewidth* and *linewidths* will raise a TypeError in the future. -Setting :rc:`text.latex.preamble` or :rc:`pdf.preamble` to non-strings -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Setting ``text.latex.preamble`` or ``pdf.preamble`` rcParams to non-strings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ These rcParams should be set to string values. Support for None (meaning the empty string) and lists of strings (implicitly joined with newlines) is deprecated. @@ -311,7 +311,7 @@ JPEG options ~~~~~~~~~~~~ The *quality*, *optimize*, and *progressive* keyword arguments to `~.Figure.savefig`, which were only used when saving to JPEG, are deprecated. -:rc:`savefig.jpeg_quality` is likewise deprecated. +The ``savefig.jpeg_quality`` rcParam is likewise deprecated. Such options should now be directly passed to Pillow using ``savefig(..., pil_kwargs={"quality": ..., "optimize": ..., "progressive": ...})``. diff --git a/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst index 0dcf76cbbe7a..3acab92c3577 100644 --- a/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst +++ b/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst @@ -359,7 +359,6 @@ rcParams - :rc:`axes.axisbelow` no longer accepts strings starting with "line" (case-insensitive) as "line"; use "line" (case-sensitive) instead. - - :rc:`text.latex.preamble` and :rc:`pdf.preamble` no longer accept - non-string values. + - :rc:`text.latex.preamble` and ``pdf.preamble`` no longer accept non-string values. - All ``*.linestyle`` rcParams no longer accept ``offset = None``; set the offset to 0 instead. diff --git a/doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst b/doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst index 3476a05394df..0b598723e26c 100644 --- a/doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst +++ b/doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst @@ -159,10 +159,10 @@ Passing ``None`` as ``rotation_mode`` to `.Text` (the default value) or passing `.Text.set_rotation_mode` will make `.Text.get_rotation_mode` return ``"default"`` instead of ``None``. The behaviour otherwise is the same. -PostScript paper type adds option to use figure size +PostScript paper size adds option to use figure size ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The :rc:`ps.papertype` rcParam can now be set to ``'figure'``, which will use +The :rc:`ps.papersize` rcParam can now be set to ``'figure'``, which will use a paper size that corresponds exactly with the size of the figure that is being saved. diff --git a/doc/devel/MEP/MEP23.rst b/doc/devel/MEP/MEP23.rst index d6b342877959..ec56f362c867 100644 --- a/doc/devel/MEP/MEP23.rst +++ b/doc/devel/MEP/MEP23.rst @@ -38,8 +38,8 @@ desirable to be able to group these under the same window. See :ghpull:`2194`. The proposed solution modifies `.FigureManagerBase` to contain and manage more -than one ``Canvas``. The settings parameter :rc:`backend.multifigure` control -when the **MultiFigure** behaviour is desired. +than one ``Canvas``. The ``backend.multifigure`` rcParam controls when the +**MultiFigure** behaviour is desired. **Note** diff --git a/doc/users/prev_whats_new/whats_new_1.3.rst b/doc/users/prev_whats_new/whats_new_1.3.rst index 10811632c5c4..af40f37f92b7 100644 --- a/doc/users/prev_whats_new/whats_new_1.3.rst +++ b/doc/users/prev_whats_new/whats_new_1.3.rst @@ -292,9 +292,9 @@ rcParam has been set, and will not retroactively affect already existing text objects. This brings their behavior in line with most other rcParams. -Added :rc:`savefig.jpeg_quality` -```````````````````````````````` -rcParam value :rc:`savefig.jpeg_quality` was added so that the user can +Added ``savefig.jpeg_quality`` rcParam +`````````````````````````````````````` +The ``savefig.jpeg_quality`` rcParam was added so that the user can configure the default quality used when a figure is written as a JPEG. The default quality is 95; previously, the default quality was 75. This change minimizes the artifacting inherent in JPEG images, diff --git a/doc/users/prev_whats_new/whats_new_1.5.rst b/doc/users/prev_whats_new/whats_new_1.5.rst index 039f65e2eba6..5bb4d4b9b5e9 100644 --- a/doc/users/prev_whats_new/whats_new_1.5.rst +++ b/doc/users/prev_whats_new/whats_new_1.5.rst @@ -190,8 +190,8 @@ Some parameters have been added, others have been improved. +---------------------------+--------------------------------------------------+ | Parameter | Description | +===========================+==================================================+ -|:rc:`xaxis.labelpad`, | mplot3d now respects these parameters | -|:rc:`yaxis.labelpad` | | +|``xaxis.labelpad``, | mplot3d now respects these attributes, which | +|``yaxis.labelpad`` | default to :rc:`axes.labelpad` | +---------------------------+--------------------------------------------------+ |:rc:`axes.labelpad` | Default space between the axis and the label | +---------------------------+--------------------------------------------------+ diff --git a/doc/users/prev_whats_new/whats_new_3.1.0.rst b/doc/users/prev_whats_new/whats_new_3.1.0.rst index 3d63768f9c7a..9f53435b89f6 100644 --- a/doc/users/prev_whats_new/whats_new_3.1.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.1.0.rst @@ -260,8 +260,7 @@ Default minor tick spacing was changed from 0.625 to 0.5 for major ticks spaced A public API has been added to `.EngFormatter` to control how the numbers in the ticklabels will be rendered. By default, *useMathText* evaluates to -:rc:`axes.formatter.use_mathtext'` and *usetex* evaluates to -:rc:`'text.usetex'`. +:rc:`axes.formatter.use_mathtext` and *usetex* evaluates to :rc:`text.usetex`. If either is `True` then the numbers will be encapsulated by ``$`` signs. When using ``TeX`` this implies that the numbers will be shown diff --git a/doc/users/prev_whats_new/whats_new_3.3.0.rst b/doc/users/prev_whats_new/whats_new_3.3.0.rst index 00ea10620d14..94914bcc75db 100644 --- a/doc/users/prev_whats_new/whats_new_3.3.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.3.0.rst @@ -292,8 +292,8 @@ positioning. For the xlabel, the supported values are 'left', 'center', or 'right'. For the ylabel, the supported values are 'bottom', 'center', or 'top'. -The default is controlled via :rc:`xaxis.labelposition` and -:rc:`yaxis.labelposition`; the Colorbar label takes the rcParam based on its +The default is controlled via :rc:`xaxis.labellocation` and +:rc:`yaxis.labellocation`; the Colorbar label takes the rcParam based on its orientation. .. plot:: diff --git a/doc/users/prev_whats_new/whats_new_3.6.0.rst b/doc/users/prev_whats_new/whats_new_3.6.0.rst index 859bbb47e354..9fcf8cebfc6f 100644 --- a/doc/users/prev_whats_new/whats_new_3.6.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.6.0.rst @@ -217,7 +217,7 @@ Linestyles for negative contours may be set individually The line style of negative contours may be set by passing the *negative_linestyles* argument to `.Axes.contour`. Previously, this style could -only be set globally via :rc:`contour.negative_linestyles`. +only be set globally via :rc:`contour.negative_linestyle`. .. plot:: :alt: Two contour plots, each showing two positive and two negative contours. The positive contours are shown in solid black lines in both plots. In one plot the negative contours are shown in dashed lines, which is the current styling. In the other plot they're shown in dotted lines, which is one of the new options. diff --git a/doc/users/prev_whats_new/whats_new_3.8.0.rst b/doc/users/prev_whats_new/whats_new_3.8.0.rst index 8c34252098db..88f987172adb 100644 --- a/doc/users/prev_whats_new/whats_new_3.8.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.8.0.rst @@ -496,7 +496,7 @@ Other improvements macosx: New figures can be opened in either windows or tabs ----------------------------------------------------------- -There is a new :rc:`macosx.window_mode`` rcParam to control how +There is a new :rc:`macosx.window_mode` rcParam to control how new figures are opened with the macosx backend. The default is **system** which uses the system settings, or one can specify either **tab** or **window** to explicitly choose the mode used to open new figures. diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index d4d032af75fb..8e612bd8c702 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -574,7 +574,7 @@ class Axis(martist.Artist): The axis label. labelpad : float The distance between the axis label and the tick labels. - Defaults to :rc:`axes.labelpad` = 4. + Defaults to :rc:`axes.labelpad`. offsetText : `~matplotlib.text.Text` A `.Text` object containing the data offset of the ticks (if any). pickradius : float diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index ef333d396101..397e4c12557d 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -113,10 +113,10 @@ def __init__(self, *, where *onoffseq* is an even length tuple of on and off ink lengths in points. For examples, see :doc:`/gallery/lines_bars_and_markers/linestyles`. - capstyle : `.CapStyle`-like, default: :rc:`patch.capstyle` + capstyle : `.CapStyle`-like, default: 'butt' Style to use for capping lines for all paths in the collection. Allowed values are %(CapStyle)s. - joinstyle : `.JoinStyle`-like, default: :rc:`patch.joinstyle` + joinstyle : `.JoinStyle`-like, default: 'round' Style to use for joining lines for all paths in the collection. Allowed values are %(JoinStyle)s. antialiaseds : bool or list of bool, default: :rc:`patch.antialiased` diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 2bfd32690297..05fbedef2c68 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -1643,7 +1643,7 @@ def _initialize_x_y(self, z): specifies the line style for negative contours. If *negative_linestyles* is *None*, the default is taken from - :rc:`contour.negative_linestyles`. + :rc:`contour.negative_linestyle`. *negative_linestyles* can also be an iterable of the above strings specifying a set of linestyles to be used. If this iterable is shorter than diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 15de61f69df7..511e1c6df6cc 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -37,7 +37,7 @@ is achievable for (approximately) 70 years on either side of the epoch, and 20 microseconds for the rest of the allowable range of dates (year 0001 to 9999). The epoch can be changed at import time via `.dates.set_epoch` or -:rc:`dates.epoch` to other dates if necessary; see +:rc:`date.epoch` to other dates if necessary; see :doc:`/gallery/ticks/date_precision_and_epochs` for a discussion. .. note:: @@ -267,7 +267,7 @@ def set_epoch(epoch): """ Set the epoch (origin for dates) for datetime calculations. - The default epoch is :rc:`dates.epoch` (by default 1970-01-01T00:00). + The default epoch is :rc:`date.epoch`. If microsecond accuracy is desired, the date being plotted needs to be within approximately 70 years of the epoch. Matplotlib internally From 1f7c55ba1a1378fa0230306bbba7669228e07f14 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 3 Oct 2024 13:43:18 +0200 Subject: [PATCH 0334/1230] Switch AxLine.set_xy{1,2} to take a single argument. --- .../deprecations/28933-AL.rst | 5 +++ lib/matplotlib/lines.py | 32 +++++++++++++++---- lib/matplotlib/lines.pyi | 4 +-- lib/matplotlib/tests/test_lines.py | 10 ++++-- 4 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/28933-AL.rst diff --git a/doc/api/next_api_changes/deprecations/28933-AL.rst b/doc/api/next_api_changes/deprecations/28933-AL.rst new file mode 100644 index 000000000000..b551c124b4e0 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/28933-AL.rst @@ -0,0 +1,5 @@ +``AxLine`` ``xy1`` and ``xy2`` setters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +These setters now each take a single argument, ``xy1`` or ``xy2`` as a tuple. +The old form, where ``x`` and ``y`` were passed as separate arguments, is +deprecated. diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 9629a821368c..5a7c83ccbc06 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -1553,18 +1553,28 @@ def get_slope(self): """Return the *slope* value of the line.""" return self._slope - def set_xy1(self, x, y): + def set_xy1(self, *args, **kwargs): """ Set the *xy1* value of the line. Parameters ---------- - x, y : float + xy1 : tuple[float, float] Points for the line to pass through. """ - self._xy1 = x, y + params = _api.select_matching_signature([ + lambda self, x, y: locals(), lambda self, xy1: locals(), + ], self, *args, **kwargs) + if "x" in params: + _api.warn_deprecated("3.10", message=( + "Passing x and y separately to AxLine.set_xy1 is deprecated since " + "%(since)s; pass them as a single tuple instead.")) + xy1 = params["x"], params["y"] + else: + xy1 = params["xy1"] + self._xy1 = xy1 - def set_xy2(self, x, y): + def set_xy2(self, *args, **kwargs): """ Set the *xy2* value of the line. @@ -1576,11 +1586,21 @@ def set_xy2(self, x, y): Parameters ---------- - x, y : float + xy2 : tuple[float, float] Points for the line to pass through. """ if self._slope is None: - self._xy2 = x, y + params = _api.select_matching_signature([ + lambda self, x, y: locals(), lambda self, xy2: locals(), + ], self, *args, **kwargs) + if "x" in params: + _api.warn_deprecated("3.10", message=( + "Passing x and y separately to AxLine.set_xy2 is deprecated since " + "%(since)s; pass them as a single tuple instead.")) + xy2 = params["x"], params["y"] + else: + xy2 = params["xy2"] + self._xy2 = xy2 else: raise ValueError("Cannot set an 'xy2' value while 'slope' is set;" " they differ but their functionalities overlap") diff --git a/lib/matplotlib/lines.pyi b/lib/matplotlib/lines.pyi index 161f99100bf5..7989a03dae3a 100644 --- a/lib/matplotlib/lines.pyi +++ b/lib/matplotlib/lines.pyi @@ -130,8 +130,8 @@ class AxLine(Line2D): def get_xy1(self) -> tuple[float, float] | None: ... def get_xy2(self) -> tuple[float, float] | None: ... def get_slope(self) -> float: ... - def set_xy1(self, x: float, y: float) -> None: ... - def set_xy2(self, x: float, y: float) -> None: ... + def set_xy1(self, xy1: tuple[float, float]) -> None: ... + def set_xy2(self, xy2: tuple[float, float]) -> None: ... def set_slope(self, slope: float) -> None: ... class VertexSelector: diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index 902b7aa2c02d..ee8b5b4aaa9e 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -417,16 +417,20 @@ def test_axline_setters(): line2 = ax.axline((.1, .1), (.8, .4)) # Testing xy1, xy2 and slope setters. # This should not produce an error. - line1.set_xy1(.2, .3) + line1.set_xy1((.2, .3)) line1.set_slope(2.4) - line2.set_xy1(.3, .2) - line2.set_xy2(.6, .8) + line2.set_xy1((.3, .2)) + line2.set_xy2((.6, .8)) # Testing xy1, xy2 and slope getters. # Should return the modified values. assert line1.get_xy1() == (.2, .3) assert line1.get_slope() == 2.4 assert line2.get_xy1() == (.3, .2) assert line2.get_xy2() == (.6, .8) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + line1.set_xy1(.2, .3) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + line2.set_xy2(.6, .8) # Testing setting xy2 and slope together. # These test should raise a ValueError with pytest.raises(ValueError, From fad3579287324c16daeb8719af68ccb58fe1b94b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 4 Oct 2024 05:02:26 -0400 Subject: [PATCH 0335/1230] Backport PR #28900: DOC: Improve fancybox demo --- .../shapes_and_collections/fancybox_demo.py | 84 ++++++++++++++----- 1 file changed, 63 insertions(+), 21 deletions(-) diff --git a/galleries/examples/shapes_and_collections/fancybox_demo.py b/galleries/examples/shapes_and_collections/fancybox_demo.py index 91cc1d1749ea..8d36a5a14d9d 100644 --- a/galleries/examples/shapes_and_collections/fancybox_demo.py +++ b/galleries/examples/shapes_and_collections/fancybox_demo.py @@ -3,7 +3,8 @@ Drawing fancy boxes =================== -The following examples show how to plot boxes with different visual properties. +The following examples show how to plot boxes (`.FancyBboxPatch`) with different +visual properties. """ import inspect @@ -15,7 +16,12 @@ import matplotlib.transforms as mtransforms # %% -# First we'll show some sample boxes with fancybox. +# Box styles +# ---------- +# `.FancyBboxPatch` supports different `.BoxStyle`\s. Note that `~.Axes.text` +# allows to draw a box around the text by adding the ``bbox`` parameter. Therefore, +# you don't see explicit `.FancyBboxPatch` and `.BoxStyle` calls in the following +# example. styles = mpatch.BoxStyle.get_styles() ncol = 2 @@ -41,13 +47,21 @@ # %% -# Next we'll show off multiple fancy boxes at once. - +# Parameters for modifying the box +# -------------------------------- +# `.BoxStyle`\s have additional parameters to configure their appearance. +# For example, "round" boxes can have ``pad`` and ``rounding``. +# +# Additionally, the `.FancyBboxPatch` parameters ``mutation_scale`` and +# ``mutation_aspect`` scale the box appearance. def add_fancy_patch_around(ax, bb, **kwargs): - fancy = FancyBboxPatch(bb.p0, bb.width, bb.height, - fc=(1, 0.8, 1, 0.5), ec=(1, 0.5, 1, 0.5), - **kwargs) + kwargs = { + 'facecolor': (1, 0.8, 1, 0.5), + 'edgecolor': (1, 0.5, 1, 0.5), + **kwargs + } + fancy = FancyBboxPatch(bb.p0, bb.width, bb.height, **kwargs) ax.add_patch(fancy) return fancy @@ -65,7 +79,7 @@ def draw_control_points_for_patches(ax): ax = axs[0, 0] # a fancy box with round corners. pad=0.1 -fancy = add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.1") +add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.1") ax.set(xlim=(0, 1), ylim=(0, 1), aspect=1, title='boxstyle="round,pad=0.1"') @@ -84,33 +98,61 @@ def draw_control_points_for_patches(ax): ax = axs[1, 0] # mutation_scale determines the overall scale of the mutation, i.e. both pad # and rounding_size is scaled according to this value. -fancy = add_fancy_patch_around( - ax, bb, boxstyle="round,pad=0.1", mutation_scale=2) +add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.1", mutation_scale=2) ax.set(xlim=(0, 1), ylim=(0, 1), aspect=1, title='boxstyle="round,pad=0.1"\n mutation_scale=2') ax = axs[1, 1] -# When the aspect ratio of the Axes is not 1, the fancy box may not be what you -# expected (green). -fancy = add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.2") -fancy.set(facecolor="none", edgecolor="green") -# You can compensate this by setting the mutation_aspect (pink). -fancy = add_fancy_patch_around( - ax, bb, boxstyle="round,pad=0.3", mutation_aspect=0.5) -ax.set(xlim=(-.5, 1.5), ylim=(0, 1), aspect=2, - title='boxstyle="round,pad=0.3"\nmutation_aspect=.5') +# mutation_aspect scales the vertical influence of the parameters (technically, +# it scales the height of the box down by mutation_aspect, applies the box parameters +# and scales the result back up). In effect, the vertical pad is scaled to +# pad * mutation_aspect, e.g. mutation_aspect=0.5 halves the vertical pad. +add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.1", mutation_aspect=0.5) +ax.set(xlim=(0, 1), ylim=(0, 1), + title='boxstyle="round,pad=0.1"\nmutation_aspect=0.5') for ax in axs.flat: draw_control_points_for_patches(ax) # Draw the original bbox (using boxstyle=square with pad=0). - fancy = add_fancy_patch_around(ax, bb, boxstyle="square,pad=0") - fancy.set(edgecolor="black", facecolor="none", zorder=10) + add_fancy_patch_around(ax, bb, boxstyle="square,pad=0", + edgecolor="black", facecolor="none", zorder=10) fig.tight_layout() plt.show() +# %% +# Creating visually constant padding on non-equal aspect Axes +# ----------------------------------------------------------- +# Since padding is in box coordinates, i.e. usually data coordinates, +# a given padding is rendered to different visual sizes if the +# Axes aspect is not 1. +# To get visually equal vertical and horizontal padding, set the +# mutation_aspect to the inverse of the Axes aspect. This scales +# the vertical padding appropriately. + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(6.5, 5)) + +# original boxes +bb = mtransforms.Bbox([[-0.5, -0.5], [0.5, 0.5]]) +add_fancy_patch_around(ax1, bb, boxstyle="square,pad=0", + edgecolor="black", facecolor="none", zorder=10) +add_fancy_patch_around(ax2, bb, boxstyle="square,pad=0", + edgecolor="black", facecolor="none", zorder=10) +ax1.set(xlim=(-1.5, 1.5), ylim=(-1.5, 1.5), aspect=2) +ax2.set(xlim=(-1.5, 1.5), ylim=(-1.5, 1.5), aspect=2) + + +fancy = add_fancy_patch_around( + ax1, bb, boxstyle="round,pad=0.5") +ax1.set_title("aspect=2\nmutation_aspect=1") + +fancy = add_fancy_patch_around( + ax2, bb, boxstyle="round,pad=0.5", mutation_aspect=0.5) +ax2.set_title("aspect=2\nmutation_aspect=0.5") + + # %% # # .. admonition:: References From a293e31bea61607be481d8889ad84db2a88ca113 Mon Sep 17 00:00:00 2001 From: MischaMegens2 <122418839+MischaMegens2@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:08:15 -0700 Subject: [PATCH 0336/1230] Replace Holroyd by Bell Gavin Bell appears to be the accurate reference/attribution, not Holroyd, replace it. --- doc/api/toolkits/mplot3d/view_angles.rst | 24 +++++++++++-------- doc/users/next_whats_new/mouse_rotation.rst | 2 +- lib/matplotlib/mpl-data/matplotlibrc | 2 +- lib/matplotlib/rcsetup.py | 2 +- lib/mpl_toolkits/mplot3d/axes3d.py | 8 +++---- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 16 ++++++------- 6 files changed, 29 insertions(+), 25 deletions(-) diff --git a/doc/api/toolkits/mplot3d/view_angles.rst b/doc/api/toolkits/mplot3d/view_angles.rst index b7ac360b499b..841cc8bb0163 100644 --- a/doc/api/toolkits/mplot3d/view_angles.rst +++ b/doc/api/toolkits/mplot3d/view_angles.rst @@ -83,8 +83,9 @@ mouse movement (it is quite noticeable, especially when adjusting roll), and it lacks an obvious mechanical equivalent; arguably, the path-independent rotation is unnatural. So it is a trade-off. -Shoemake's arcball has an abrupt edge; this is remedied in Holroyd's arcball -(``mouserotationstyle: Holroyd``). +Shoemake's arcball has an abrupt edge; this is remedied in Gavin Bell's arcball +(``mouserotationstyle: Bell``), originally written for OpenGL [Bell1988]_. It is used +in Blender and Meshlab. Henriksen et al. [Henriksen2002]_ provide an overview. In summary: @@ -122,7 +123,7 @@ Henriksen et al. [Henriksen2002]_ provide an overview. In summary: - ✔️ - ✔️ - ❌ - * - Holroyd + * - Bell - ❌ - ✔️ - ✔️ @@ -142,7 +143,7 @@ You can try out one of the various mouse rotation styles using:: .. code:: import matplotlib as mpl - mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'arcball', 'Shoemake', or 'Holroyd' + mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'arcball', 'Shoemake', or 'Bell' import numpy as np import matplotlib.pyplot as plt @@ -183,11 +184,14 @@ A size of about 2/3 appears to work reasonably well; this is the default. three-dimensional rotation using a mouse", in Proceedings of Graphics Interface '92, 1992, pp. 151-156, https://doi.org/10.20380/GI1992.18 + +.. [Bell1988] Gavin Bell, in the examples included with the GLUT (OpenGL + Utility Toolkit) library, + https://github.com/markkilgard/glut/blob/master/progs/examples/trackball.h + .. [Henriksen2002] Knud Henriksen, Jon Sporring, Kasper Hornbæk, - "Virtual Trackballs Revisited", in Proceedings of DSAGM'2002 - `[pdf]`__; - and in IEEE Transactions on Visualization and Computer Graphics, - Volume 10, Issue 2, March-April 2004, pp. 206-216, - https://doi.org/10.1109/TVCG.2004.1260772 + "Virtual Trackballs Revisited", in IEEE Transactions on Visualization + and Computer Graphics, Volume 10, Issue 2, March-April 2004, pp. 206-216, + https://doi.org/10.1109/TVCG.2004.1260772 `[full-text]`__; -__ https://web.archive.org/web/20240607102518/http://hjemmesider.diku.dk/~kash/papers/DSAGM2002_henriksen.pdf +__ https://www.researchgate.net/publication/8329656_Virtual_Trackballs_Revisited#fullTextFileContent diff --git a/doc/users/next_whats_new/mouse_rotation.rst b/doc/users/next_whats_new/mouse_rotation.rst index 5c1b1480c595..cdd8efb5494d 100644 --- a/doc/users/next_whats_new/mouse_rotation.rst +++ b/doc/users/next_whats_new/mouse_rotation.rst @@ -20,7 +20,7 @@ To try out one of the various mouse rotation styles: .. code:: import matplotlib as mpl - mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'arcball', 'Shoemake', or 'Holroyd' + mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'arcball', 'Shoemake', or 'Bell' import numpy as np import matplotlib.pyplot as plt diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index d56043d5581c..0818ba8694dc 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -433,7 +433,7 @@ #axes3d.yaxis.panecolor: (0.90, 0.90, 0.90, 0.5) # background pane on 3D axes #axes3d.zaxis.panecolor: (0.925, 0.925, 0.925, 0.5) # background pane on 3D axes -#axes3d.mouserotationstyle: arcball # {azel, trackball, arcball, Shoemake, Holroyd} +#axes3d.mouserotationstyle: arcball # {azel, trackball, arcball, Shoemake, Bell} # See also https://matplotlib.org/stable/api/toolkits/mplot3d/view_angles.html#rotation-with-mouse #axes3d.trackballsize: 0.667 # trackball diameter, in units of the Axes bbox diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index e9395207fb99..1cfa613df0e0 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -1133,7 +1133,7 @@ def _convert_validator_spec(key, conv): "axes3d.zaxis.panecolor": validate_color, # 3d background pane "axes3d.mouserotationstyle": ["azel", "trackball", "arcball", - "Shoemake", "Holroyd"], + "Shoemake", "Bell"], "axes3d.trackballsize": validate_float, # scatter props diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 8a61e793f1d2..a3071b32acf3 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -1513,7 +1513,7 @@ def _arcball(self, x: float, y: float, style: str) -> np.ndarray: Convert a point (x, y) to a point on a virtual trackball. This is either Ken Shoemake's arcball (a sphere) or - Tom Holroyd's (a sphere combined with a hyperbola). + Gavin Bell's (a sphere combined with a hyperbola). See: Ken Shoemake, "ARCBALL: A user interface for specifying three-dimensional rotation using a mouse." in Proceedings of Graphics Interface '92, 1992, pp. 151-156, @@ -1523,7 +1523,7 @@ def _arcball(self, x: float, y: float, style: str) -> np.ndarray: x /= s y /= s r2 = x*x + y*y - if style == 'Holroyd': + if style == 'Bell': if r2 > 0.5: p = np.array([1/(2*math.sqrt(r2)), x, y])/math.sqrt(1/(4*r2)+r2) else: @@ -1587,12 +1587,12 @@ def _on_move(self, event): nk = np.linalg.norm(k) th = nk / mpl.rcParams['axes3d.trackballsize'] dq = _Quaternion(np.cos(th), k*np.sin(th)/nk) - else: # 'arcball', 'Shoemake', 'Holroyd' + else: # 'arcball', 'Shoemake', 'Bell' current_vec = self._arcball(self._sx/w, self._sy/h, style) new_vec = self._arcball(x/w, y/h, style) if style == 'arcball': dq = _Quaternion.rotate_from_to(current_vec, new_vec) - else: # 'Shoemake', 'Holroyd' + else: # 'Shoemake', 'Bell' dq = _Quaternion(0, new_vec) * _Quaternion(0, -current_vec) q = dq * q diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 0a5c0f116e8a..f88053e04e4d 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -1944,7 +1944,7 @@ def test_quaternion(): @pytest.mark.parametrize('style', - ('azel', 'trackball', 'arcball', 'Shoemake', 'Holroyd')) + ('azel', 'trackball', 'arcball', 'Shoemake', 'Bell')) def test_rotate(style): """Test rotating using the left mouse button.""" if style == 'azel': @@ -2008,13 +2008,13 @@ def test_rotate(style): ('Shoemake', 30, 0, 1): (-48.590378, -40.893395, 49.106605), ('Shoemake', 30, 0.5, c): (-25.658906, -56.309932, 43.897886), - ('Holroyd', 0, 1, 0): (0, -60, 0), - ('Holroyd', 0, 0, 1): (-60, 0, 0), - ('Holroyd', 0, 0.5, c): (-48.590378, -40.893395, 19.106605), - ('Holroyd', 0, 2, 0): (0, -126.869898, 0), - ('Holroyd', 30, 1, 0): (25.658906, -56.309932, 16.102114), - ('Holroyd', 30, 0, 1): (-48.590378, -40.893395, 49.106605), - ('Holroyd', 30, 0.5, c): (-25.658906, -56.309932, 43.897886)} + ('Bell', 0, 1, 0): (0, -60, 0), + ('Bell', 0, 0, 1): (-60, 0, 0), + ('Bell', 0, 0.5, c): (-48.590378, -40.893395, 19.106605), + ('Bell', 0, 2, 0): (0, -126.869898, 0), + ('Bell', 30, 1, 0): (25.658906, -56.309932, 16.102114), + ('Bell', 30, 0, 1): (-48.590378, -40.893395, 49.106605), + ('Bell', 30, 0.5, c): (-25.658906, -56.309932, 43.897886)} new_elev, new_azim, new_roll = expectations[(style, roll, dx, dy)] np.testing.assert_allclose((ax.elev, ax.azim, ax.roll), (new_elev, new_azim, new_roll), atol=1e-6) From ccb788e2db3a2ee0b9bf02d07294c633f9c87663 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 7 Oct 2024 10:30:53 +0200 Subject: [PATCH 0337/1230] In colorbar docs, add ref from 'boundaries' doc to 'spacing' doc. The `spacing` kwarg only makes sense in relation to `boundaries`, so put the docs for them next to one another and add an explicit reference from `boundaries` to `spacing`. (The order of the kwargs in the signature did not change, because it already doesn't match the docs anyways; reordering them could be done separately.) --- lib/matplotlib/colorbar.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 2d2fe42dd16a..89e511fa1428 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -86,11 +86,6 @@ If *False* the minimum and maximum colorbar extensions will be triangular (the default). If *True* the extensions will be rectangular. -spacing : {'uniform', 'proportional'} - For discrete colorbars (`.BoundaryNorm` or contours), 'uniform' gives each - color the same space; 'proportional' makes the space proportional to the - data interval. - ticks : None or list of ticks or Locator If None, ticks are determined automatically from the input. @@ -109,9 +104,15 @@ If unset, the colormap will be displayed on a 0-1 scale. If sequences, *values* must have a length 1 less than *boundaries*. For each region delimited by adjacent entries in *boundaries*, the color mapped - to the corresponding value in values will be used. + to the corresponding value in *values* will be used. The size of each + region is determined by the *spacing* parameter. Normally only useful for indexed colors (i.e. ``norm=NoNorm()``) or other - unusual circumstances.""") + unusual circumstances. + +spacing : {'uniform', 'proportional'} + For discrete colorbars (`.BoundaryNorm` or contours), 'uniform' gives each + color the same space; 'proportional' makes the space proportional to the + data interval.""") def _set_ticks_on_axis_warn(*args, **kwargs): From ea41be321f98d8d122b371437910f5452d21d74e Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:31:54 +0200 Subject: [PATCH 0338/1230] DOC: Clarify the returned line of axhline()/axvline() Closes #28927. --- lib/matplotlib/axes/_axes.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index de0c6854cbb1..a16e19f90152 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -754,6 +754,15 @@ def axhline(self, y=0, xmin=0, xmax=1, **kwargs): Returns ------- `~matplotlib.lines.Line2D` + A `.Line2D` specified via two points ``(xmin, y)``, ``(xmax, y)``. + Its transform is set such that *x* is in + :ref:`axes coordinates ` and *y* is in + :ref:`data coordinates `. + + This is still a generic line and the horizontal character is only + realized through using identical *y* values for both points. Thus, + if you want to change the *y* value later, you have to provide two + values ``line.set_ydata([3, 3])``. Other Parameters ---------------- @@ -828,6 +837,15 @@ def axvline(self, x=0, ymin=0, ymax=1, **kwargs): Returns ------- `~matplotlib.lines.Line2D` + A `.Line2D` specified via two points ``(x, ymin)``, ``(x, ymax)``. + Its transform is set such that *x* is in + :ref:`data coordinates ` and *y* is in + :ref:`axes coordinates `. + + This is still a generic line and the vertical character is only + realized through using identical *x* values for both points. Thus, + if you want to change the *x* value later, you have to provide two + values ``line.set_xdata([3, 3])``. Other Parameters ---------------- From 2e323c91137698d83008346ba530fc9515be29d8 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 8 Oct 2024 07:04:34 -0400 Subject: [PATCH 0339/1230] BLD: update trove metadata to support py3.13 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b706d86cb7b2..5b5c60d60d54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ classifiers=[ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Visualization", ] From 97ea6ef94d0e67271fda0853b55aaf8c077ae4df Mon Sep 17 00:00:00 2001 From: "Lumberbot (aka Jack)" <39504233+meeseeksmachine@users.noreply.github.com> Date: Wed, 9 Oct 2024 12:07:04 -0700 Subject: [PATCH 0340/1230] Backport PR #28952: BLD: update trove metadata to support py3.13 (#28954) Co-authored-by: Greg Lucas --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 891ef87e4342..c0237c7df5c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers=[ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Visualization", ] From 5db55de2e18cdba4a7c7f83212ca40fb6d710b5c Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 20 Sep 2024 22:14:21 -0400 Subject: [PATCH 0341/1230] Remove old-style Python-C++ converters And merge the two files now that there are no old ones. --- src/_backend_agg_wrapper.cpp | 1 - src/_image_wrapper.cpp | 2 +- src/_path_wrapper.cpp | 1 - src/meson.build | 7 +- src/py_converters.cpp | 549 +---------------------------------- src/py_converters.h | 187 +++++++++--- src/py_converters_11.cpp | 22 -- src/py_converters_11.h | 153 ---------- 8 files changed, 166 insertions(+), 756 deletions(-) delete mode 100644 src/py_converters_11.cpp delete mode 100644 src/py_converters_11.h diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index fb241b217fe9..3e41eed7452d 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -5,7 +5,6 @@ #include "numpy_cpp.h" #include "py_converters.h" #include "_backend_agg.h" -#include "py_converters_11.h" namespace py = pybind11; using namespace pybind11::literals; diff --git a/src/_image_wrapper.cpp b/src/_image_wrapper.cpp index 856dcf4ea3ce..0095f52e5997 100644 --- a/src/_image_wrapper.cpp +++ b/src/_image_wrapper.cpp @@ -2,7 +2,7 @@ #include #include "_image_resample.h" -#include "py_converters_11.h" +#include "py_converters.h" namespace py = pybind11; using namespace pybind11::literals; diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index 13431601e5af..958cb6f5a2b6 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -14,7 +14,6 @@ #include "_backend_agg_basic_types.h" #include "py_adaptors.h" #include "py_converters.h" -#include "py_converters_11.h" namespace py = pybind11; using namespace pybind11::literals; diff --git a/src/meson.build b/src/meson.build index 6f1869cc6ca4..ebd02a5f66c6 100644 --- a/src/meson.build +++ b/src/meson.build @@ -73,8 +73,6 @@ extension_data = { '_backend_agg': { 'subdir': 'matplotlib/backends', 'sources': files( - 'py_converters.cpp', - 'py_converters_11.cpp', '_backend_agg.cpp', '_backend_agg_wrapper.cpp', ), @@ -92,7 +90,6 @@ extension_data = { 'sources': files( 'ft2font.cpp', 'ft2font_wrapper.cpp', - 'py_converters.cpp', ), 'dependencies': [ freetype_dep, pybind11_dep, numpy_dep, agg_dep.partial_dependency(includes: true), @@ -107,7 +104,7 @@ extension_data = { 'subdir': 'matplotlib', 'sources': files( '_image_wrapper.cpp', - 'py_converters_11.cpp', + 'py_converters.cpp', ), 'dependencies': [ pybind11_dep, @@ -118,8 +115,6 @@ extension_data = { '_path': { 'subdir': 'matplotlib', 'sources': files( - 'py_converters.cpp', - 'py_converters_11.cpp', '_path_wrapper.cpp', ), 'dependencies': [numpy_dep, agg_dep, pybind11_dep], diff --git a/src/py_converters.cpp b/src/py_converters.cpp index dee4b0abfd31..1506bcfcf5b7 100644 --- a/src/py_converters.cpp +++ b/src/py_converters.cpp @@ -1,543 +1,22 @@ -#define NO_IMPORT_ARRAY -#define PY_SSIZE_T_CLEAN #include "py_converters.h" -#include "numpy_cpp.h" -#include "agg_basics.h" -#include "agg_color_rgba.h" -#include "agg_math_stroke.h" - -extern "C" { - -static int convert_string_enum(PyObject *obj, const char *name, const char **names, int *values, int *result) -{ - PyObject *bytesobj; - char *str; - - if (obj == NULL || obj == Py_None) { - return 1; - } - - if (PyUnicode_Check(obj)) { - bytesobj = PyUnicode_AsASCIIString(obj); - if (bytesobj == NULL) { - return 0; - } - } else if (PyBytes_Check(obj)) { - Py_INCREF(obj); - bytesobj = obj; - } else { - PyErr_Format(PyExc_TypeError, "%s must be str or bytes", name); - return 0; - } - - str = PyBytes_AsString(bytesobj); - if (str == NULL) { - Py_DECREF(bytesobj); - return 0; - } - - for ( ; *names != NULL; names++, values++) { - if (strncmp(str, *names, 64) == 0) { - *result = *values; - Py_DECREF(bytesobj); - return 1; - } - } - - PyErr_Format(PyExc_ValueError, "invalid %s value", name); - Py_DECREF(bytesobj); - return 0; -} - -int convert_from_method(PyObject *obj, const char *name, converter func, void *p) -{ - PyObject *value; - - value = PyObject_CallMethod(obj, name, NULL); - if (value == NULL) { - if (!PyObject_HasAttrString(obj, name)) { - PyErr_Clear(); - return 1; - } - return 0; - } - - if (!func(value, p)) { - Py_DECREF(value); - return 0; - } - - Py_DECREF(value); - return 1; -} - -int convert_from_attr(PyObject *obj, const char *name, converter func, void *p) -{ - PyObject *value; - - value = PyObject_GetAttrString(obj, name); - if (value == NULL) { - if (!PyObject_HasAttrString(obj, name)) { - PyErr_Clear(); - return 1; - } - return 0; - } - - if (!func(value, p)) { - Py_DECREF(value); - return 0; - } - - Py_DECREF(value); - return 1; -} - -int convert_double(PyObject *obj, void *p) -{ - double *val = (double *)p; - - *val = PyFloat_AsDouble(obj); - if (PyErr_Occurred()) { - return 0; - } - - return 1; -} - -int convert_bool(PyObject *obj, void *p) -{ - bool *val = (bool *)p; - switch (PyObject_IsTrue(obj)) { - case 0: *val = false; break; - case 1: *val = true; break; - default: return 0; // errored. - } - return 1; -} - -int convert_cap(PyObject *capobj, void *capp) -{ - const char *names[] = {"butt", "round", "projecting", NULL}; - int values[] = {agg::butt_cap, agg::round_cap, agg::square_cap}; - int result = agg::butt_cap; - - if (!convert_string_enum(capobj, "capstyle", names, values, &result)) { - return 0; - } - - *(agg::line_cap_e *)capp = (agg::line_cap_e)result; - return 1; -} - -int convert_join(PyObject *joinobj, void *joinp) -{ - const char *names[] = {"miter", "round", "bevel", NULL}; - int values[] = {agg::miter_join_revert, agg::round_join, agg::bevel_join}; - int result = agg::miter_join_revert; - - if (!convert_string_enum(joinobj, "joinstyle", names, values, &result)) { - return 0; - } - - *(agg::line_join_e *)joinp = (agg::line_join_e)result; - return 1; -} - -int convert_rect(PyObject *rectobj, void *rectp) -{ - agg::rect_d *rect = (agg::rect_d *)rectp; - - if (rectobj == NULL || rectobj == Py_None) { - rect->x1 = 0.0; - rect->y1 = 0.0; - rect->x2 = 0.0; - rect->y2 = 0.0; - } else { - PyArrayObject *rect_arr = (PyArrayObject *)PyArray_ContiguousFromAny( - rectobj, NPY_DOUBLE, 1, 2); - if (rect_arr == NULL) { - return 0; - } - - if (PyArray_NDIM(rect_arr) == 2) { - if (PyArray_DIM(rect_arr, 0) != 2 || - PyArray_DIM(rect_arr, 1) != 2) { - PyErr_SetString(PyExc_ValueError, "Invalid bounding box"); - Py_DECREF(rect_arr); - return 0; - } - - } else { // PyArray_NDIM(rect_arr) == 1 - if (PyArray_DIM(rect_arr, 0) != 4) { - PyErr_SetString(PyExc_ValueError, "Invalid bounding box"); - Py_DECREF(rect_arr); - return 0; - } - } - - double *buff = (double *)PyArray_DATA(rect_arr); - rect->x1 = buff[0]; - rect->y1 = buff[1]; - rect->x2 = buff[2]; - rect->y2 = buff[3]; - - Py_DECREF(rect_arr); - } - return 1; -} - -int convert_rgba(PyObject *rgbaobj, void *rgbap) -{ - agg::rgba *rgba = (agg::rgba *)rgbap; - PyObject *rgbatuple = NULL; - int success = 1; - if (rgbaobj == NULL || rgbaobj == Py_None) { - rgba->r = 0.0; - rgba->g = 0.0; - rgba->b = 0.0; - rgba->a = 0.0; - } else { - if (!(rgbatuple = PySequence_Tuple(rgbaobj))) { - success = 0; - goto exit; - } - rgba->a = 1.0; - if (!PyArg_ParseTuple( - rgbatuple, "ddd|d:rgba", &(rgba->r), &(rgba->g), &(rgba->b), &(rgba->a))) { - success = 0; - goto exit; - } - } -exit: - Py_XDECREF(rgbatuple); - return success; -} - -int convert_dashes(PyObject *dashobj, void *dashesp) -{ - Dashes *dashes = (Dashes *)dashesp; - - double dash_offset = 0.0; - PyObject *dashes_seq = NULL; - - if (!PyArg_ParseTuple(dashobj, "dO:dashes", &dash_offset, &dashes_seq)) { - return 0; - } - - if (dashes_seq == Py_None) { - return 1; - } - - if (!PySequence_Check(dashes_seq)) { - PyErr_SetString(PyExc_TypeError, "Invalid dashes sequence"); - return 0; - } - - Py_ssize_t nentries = PySequence_Size(dashes_seq); - // If the dashpattern has odd length, iterate through it twice (in - // accordance with the pdf/ps/svg specs). - Py_ssize_t dash_pattern_length = (nentries % 2) ? 2 * nentries : nentries; - - for (Py_ssize_t i = 0; i < dash_pattern_length; ++i) { - PyObject *item; - double length; - double skip; - - item = PySequence_GetItem(dashes_seq, i % nentries); - if (item == NULL) { - return 0; - } - length = PyFloat_AsDouble(item); - if (PyErr_Occurred()) { - Py_DECREF(item); - return 0; - } - Py_DECREF(item); - - ++i; - - item = PySequence_GetItem(dashes_seq, i % nentries); - if (item == NULL) { - return 0; - } - skip = PyFloat_AsDouble(item); - if (PyErr_Occurred()) { - Py_DECREF(item); - return 0; - } - Py_DECREF(item); - - dashes->add_dash_pair(length, skip); - } - - dashes->set_dash_offset(dash_offset); - - return 1; -} - -int convert_dashes_vector(PyObject *obj, void *dashesp) -{ - DashesVector *dashes = (DashesVector *)dashesp; - - if (!PySequence_Check(obj)) { - return 0; - } - - Py_ssize_t n = PySequence_Size(obj); - - for (Py_ssize_t i = 0; i < n; ++i) { - PyObject *item; - Dashes subdashes; - - item = PySequence_GetItem(obj, i); - if (item == NULL) { - return 0; - } - - if (!convert_dashes(item, &subdashes)) { - Py_DECREF(item); - return 0; - } - Py_DECREF(item); - - dashes->push_back(subdashes); - } - - return 1; -} - -int convert_trans_affine(PyObject *obj, void *transp) +void convert_trans_affine(const py::object& transform, agg::trans_affine& affine) { - agg::trans_affine *trans = (agg::trans_affine *)transp; - - /** If None assume identity transform. */ - if (obj == NULL || obj == Py_None) { - return 1; - } - - PyArrayObject *array = (PyArrayObject *)PyArray_ContiguousFromAny(obj, NPY_DOUBLE, 2, 2); - if (array == NULL) { - return 0; + // If None assume identity transform so leave affine unchanged + if (transform.is_none()) { + return; } - if (PyArray_DIM(array, 0) == 3 && PyArray_DIM(array, 1) == 3) { - double *buffer = (double *)PyArray_DATA(array); - trans->sx = buffer[0]; - trans->shx = buffer[1]; - trans->tx = buffer[2]; - - trans->shy = buffer[3]; - trans->sy = buffer[4]; - trans->ty = buffer[5]; - - Py_DECREF(array); - return 1; + auto array = py::array_t::ensure(transform); + if (!array || array.ndim() != 2 || array.shape(0) != 3 || array.shape(1) != 3) { + throw std::invalid_argument("Invalid affine transformation matrix"); } - Py_DECREF(array); - PyErr_SetString(PyExc_ValueError, "Invalid affine transformation matrix"); - return 0; -} - -int convert_path(PyObject *obj, void *pathp) -{ - mpl::PathIterator *path = (mpl::PathIterator *)pathp; - - PyObject *vertices_obj = NULL; - PyObject *codes_obj = NULL; - PyObject *should_simplify_obj = NULL; - PyObject *simplify_threshold_obj = NULL; - bool should_simplify; - double simplify_threshold; - - int status = 0; - - if (obj == NULL || obj == Py_None) { - return 1; - } - - vertices_obj = PyObject_GetAttrString(obj, "vertices"); - if (vertices_obj == NULL) { - goto exit; - } - - codes_obj = PyObject_GetAttrString(obj, "codes"); - if (codes_obj == NULL) { - goto exit; - } - - should_simplify_obj = PyObject_GetAttrString(obj, "should_simplify"); - if (should_simplify_obj == NULL) { - goto exit; - } - switch (PyObject_IsTrue(should_simplify_obj)) { - case 0: should_simplify = 0; break; - case 1: should_simplify = 1; break; - default: goto exit; // errored. - } - - simplify_threshold_obj = PyObject_GetAttrString(obj, "simplify_threshold"); - if (simplify_threshold_obj == NULL) { - goto exit; - } - simplify_threshold = PyFloat_AsDouble(simplify_threshold_obj); - if (PyErr_Occurred()) { - goto exit; - } - - if (!path->set(vertices_obj, codes_obj, should_simplify, simplify_threshold)) { - goto exit; - } - - status = 1; - -exit: - Py_XDECREF(vertices_obj); - Py_XDECREF(codes_obj); - Py_XDECREF(should_simplify_obj); - Py_XDECREF(simplify_threshold_obj); - - return status; -} - -int convert_pathgen(PyObject *obj, void *pathgenp) -{ - mpl::PathGenerator *paths = (mpl::PathGenerator *)pathgenp; - if (!paths->set(obj)) { - PyErr_SetString(PyExc_TypeError, "Not an iterable of paths"); - return 0; - } - return 1; -} - -int convert_clippath(PyObject *clippath_tuple, void *clippathp) -{ - ClipPath *clippath = (ClipPath *)clippathp; - - if (clippath_tuple != NULL && clippath_tuple != Py_None) { - if (!PyArg_ParseTuple(clippath_tuple, - "O&O&:clippath", - &convert_path, - &clippath->path, - &convert_trans_affine, - &clippath->trans)) { - return 0; - } - } - - return 1; -} - -int convert_snap(PyObject *obj, void *snapp) -{ - e_snap_mode *snap = (e_snap_mode *)snapp; - if (obj == NULL || obj == Py_None) { - *snap = SNAP_AUTO; - } else { - switch (PyObject_IsTrue(obj)) { - case 0: *snap = SNAP_FALSE; break; - case 1: *snap = SNAP_TRUE; break; - default: return 0; // errored. - } - } - return 1; -} - -int convert_sketch_params(PyObject *obj, void *sketchp) -{ - SketchParams *sketch = (SketchParams *)sketchp; - - if (obj == NULL || obj == Py_None) { - sketch->scale = 0.0; - sketch->length = 0.0; - sketch->randomness = 0.0; - } else if (!PyArg_ParseTuple(obj, - "ddd:sketch_params", - &sketch->scale, - &sketch->length, - &sketch->randomness)) { - return 0; - } - - return 1; -} - -int convert_gcagg(PyObject *pygc, void *gcp) -{ - GCAgg *gc = (GCAgg *)gcp; - - if (!(convert_from_attr(pygc, "_linewidth", &convert_double, &gc->linewidth) && - convert_from_attr(pygc, "_alpha", &convert_double, &gc->alpha) && - convert_from_attr(pygc, "_forced_alpha", &convert_bool, &gc->forced_alpha) && - convert_from_attr(pygc, "_rgb", &convert_rgba, &gc->color) && - convert_from_attr(pygc, "_antialiased", &convert_bool, &gc->isaa) && - convert_from_attr(pygc, "_capstyle", &convert_cap, &gc->cap) && - convert_from_attr(pygc, "_joinstyle", &convert_join, &gc->join) && - convert_from_method(pygc, "get_dashes", &convert_dashes, &gc->dashes) && - convert_from_attr(pygc, "_cliprect", &convert_rect, &gc->cliprect) && - convert_from_method(pygc, "get_clip_path", &convert_clippath, &gc->clippath) && - convert_from_method(pygc, "get_snap", &convert_snap, &gc->snap_mode) && - convert_from_method(pygc, "get_hatch_path", &convert_path, &gc->hatchpath) && - convert_from_method(pygc, "get_hatch_color", &convert_rgba, &gc->hatch_color) && - convert_from_method(pygc, "get_hatch_linewidth", &convert_double, &gc->hatch_linewidth) && - convert_from_method(pygc, "get_sketch_params", &convert_sketch_params, &gc->sketch))) { - return 0; - } - - return 1; -} - -int convert_points(PyObject *obj, void *pointsp) -{ - numpy::array_view *points = (numpy::array_view *)pointsp; - if (obj == NULL || obj == Py_None) { - return 1; - } - if (!points->set(obj) - || (points->size() && !check_trailing_shape(*points, "points", 2))) { - return 0; - } - return 1; -} - -int convert_transforms(PyObject *obj, void *transp) -{ - numpy::array_view *trans = (numpy::array_view *)transp; - if (obj == NULL || obj == Py_None) { - return 1; - } - if (!trans->set(obj) - || (trans->size() && !check_trailing_shape(*trans, "transforms", 3, 3))) { - return 0; - } - return 1; -} - -int convert_bboxes(PyObject *obj, void *bboxp) -{ - numpy::array_view *bbox = (numpy::array_view *)bboxp; - if (obj == NULL || obj == Py_None) { - return 1; - } - if (!bbox->set(obj) - || (bbox->size() && !check_trailing_shape(*bbox, "bbox array", 2, 2))) { - return 0; - } - return 1; -} - -int convert_colors(PyObject *obj, void *colorsp) -{ - numpy::array_view *colors = (numpy::array_view *)colorsp; - if (obj == NULL || obj == Py_None) { - return 1; - } - if (!colors->set(obj) - || (colors->size() && !check_trailing_shape(*colors, "colors", 4))) { - return 0; - } - return 1; -} + auto buffer = array.data(); + affine.sx = buffer[0]; + affine.shx = buffer[1]; + affine.tx = buffer[2]; + affine.shy = buffer[3]; + affine.sy = buffer[4]; + affine.ty = buffer[5]; } diff --git a/src/py_converters.h b/src/py_converters.h index b514efdf5d47..360d1e01d075 100644 --- a/src/py_converters.h +++ b/src/py_converters.h @@ -3,44 +3,157 @@ #ifndef MPL_PY_CONVERTERS_H #define MPL_PY_CONVERTERS_H -/*************************************************************************** - * This module contains a number of conversion functions from Python types - * to C++ types. Most of them meet the Python "converter" signature: - * - * typedef int (*converter)(PyObject *, void *); - * - * and thus can be passed as conversion functions to PyArg_ParseTuple - * and friends. +/*************************************************************************************** + * This module contains a number of conversion functions from Python types to C++ types. + * Most of them meet the pybind11 type casters, and thus will automatically be applied + * when a C++ function parameter uses their type. */ -#include -#include "_backend_agg_basic_types.h" - -extern "C" { -typedef int (*converter)(PyObject *, void *); - -int convert_from_attr(PyObject *obj, const char *name, converter func, void *p); -int convert_from_method(PyObject *obj, const char *name, converter func, void *p); - -int convert_double(PyObject *obj, void *p); -int convert_bool(PyObject *obj, void *p); -int convert_cap(PyObject *capobj, void *capp); -int convert_join(PyObject *joinobj, void *joinp); -int convert_rect(PyObject *rectobj, void *rectp); -int convert_rgba(PyObject *rgbaocj, void *rgbap); -int convert_dashes(PyObject *dashobj, void *gcp); -int convert_dashes_vector(PyObject *obj, void *dashesp); -int convert_trans_affine(PyObject *obj, void *transp); -int convert_path(PyObject *obj, void *pathp); -int convert_pathgen(PyObject *obj, void *pathgenp); -int convert_clippath(PyObject *clippath_tuple, void *clippathp); -int convert_snap(PyObject *obj, void *snapp); -int convert_sketch_params(PyObject *obj, void *sketchp); -int convert_gcagg(PyObject *pygc, void *gcp); -int convert_points(PyObject *pygc, void *pointsp); -int convert_transforms(PyObject *pygc, void *transp); -int convert_bboxes(PyObject *pygc, void *bboxp); -int convert_colors(PyObject *pygc, void *colorsp); +#include +#include + +namespace py = pybind11; + +#include "agg_basics.h" +#include "agg_color_rgba.h" +#include "agg_trans_affine.h" +#include "mplutils.h" + +void convert_trans_affine(const py::object& transform, agg::trans_affine& affine); + +inline auto convert_points(py::array_t obj) +{ + if (!check_trailing_shape(obj, "points", 2)) { + throw py::error_already_set(); + } + return obj.unchecked<2>(); } -#endif +inline auto convert_transforms(py::array_t obj) +{ + if (!check_trailing_shape(obj, "transforms", 3, 3)) { + throw py::error_already_set(); + } + return obj.unchecked<3>(); +} + +inline auto convert_bboxes(py::array_t obj) +{ + if (!check_trailing_shape(obj, "bbox array", 2, 2)) { + throw py::error_already_set(); + } + return obj.unchecked<3>(); +} + +inline auto convert_colors(py::array_t obj) +{ + if (!check_trailing_shape(obj, "colors", 4)) { + throw py::error_already_set(); + } + return obj.unchecked<2>(); +} + +namespace PYBIND11_NAMESPACE { namespace detail { + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::rect_d, const_name("rect_d")); + + bool load(handle src, bool) { + if (src.is_none()) { + value.x1 = 0.0; + value.y1 = 0.0; + value.x2 = 0.0; + value.y2 = 0.0; + return true; + } + + auto rect_arr = py::array_t::ensure(src); + + if (rect_arr.ndim() == 2) { + if (rect_arr.shape(0) != 2 || rect_arr.shape(1) != 2) { + throw py::value_error("Invalid bounding box"); + } + + value.x1 = *rect_arr.data(0, 0); + value.y1 = *rect_arr.data(0, 1); + value.x2 = *rect_arr.data(1, 0); + value.y2 = *rect_arr.data(1, 1); + + } else if (rect_arr.ndim() == 1) { + if (rect_arr.shape(0) != 4) { + throw py::value_error("Invalid bounding box"); + } + + value.x1 = *rect_arr.data(0); + value.y1 = *rect_arr.data(1); + value.x2 = *rect_arr.data(2); + value.y2 = *rect_arr.data(3); + + } else { + throw py::value_error("Invalid bounding box"); + } + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::rgba, const_name("rgba")); + + bool load(handle src, bool) { + if (src.is_none()) { + value.r = 0.0; + value.g = 0.0; + value.b = 0.0; + value.a = 0.0; + } else { + auto rgbatuple = src.cast(); + value.r = rgbatuple[0].cast(); + value.g = rgbatuple[1].cast(); + value.b = rgbatuple[2].cast(); + switch (rgbatuple.size()) { + case 4: + value.a = rgbatuple[3].cast(); + break; + case 3: + value.a = 1.0; + break; + default: + throw py::value_error("RGBA value must be 3- or 4-tuple"); + } + } + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::trans_affine, const_name("trans_affine")); + + bool load(handle src, bool) { + // If None assume identity transform so leave affine unchanged + if (src.is_none()) { + return true; + } + + auto array = py::array_t::ensure(src); + if (!array || array.ndim() != 2 || + array.shape(0) != 3 || array.shape(1) != 3) { + throw std::invalid_argument("Invalid affine transformation matrix"); + } + + auto buffer = array.data(); + value.sx = buffer[0]; + value.shx = buffer[1]; + value.tx = buffer[2]; + value.shy = buffer[3]; + value.sy = buffer[4]; + value.ty = buffer[5]; + + return true; + } + }; +}} // namespace PYBIND11_NAMESPACE::detail + +#endif /* MPL_PY_CONVERTERS_H */ diff --git a/src/py_converters_11.cpp b/src/py_converters_11.cpp deleted file mode 100644 index 830ee6336fb4..000000000000 --- a/src/py_converters_11.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "py_converters_11.h" - -void convert_trans_affine(const py::object& transform, agg::trans_affine& affine) -{ - // If None assume identity transform so leave affine unchanged - if (transform.is_none()) { - return; - } - - auto array = py::array_t::ensure(transform); - if (!array || array.ndim() != 2 || array.shape(0) != 3 || array.shape(1) != 3) { - throw std::invalid_argument("Invalid affine transformation matrix"); - } - - auto buffer = array.data(); - affine.sx = buffer[0]; - affine.shx = buffer[1]; - affine.tx = buffer[2]; - affine.shy = buffer[3]; - affine.sy = buffer[4]; - affine.ty = buffer[5]; -} diff --git a/src/py_converters_11.h b/src/py_converters_11.h deleted file mode 100644 index b093f71b181a..000000000000 --- a/src/py_converters_11.h +++ /dev/null @@ -1,153 +0,0 @@ -#ifndef MPL_PY_CONVERTERS_11_H -#define MPL_PY_CONVERTERS_11_H - -// pybind11 equivalent of py_converters.h - -#include -#include - -namespace py = pybind11; - -#include "agg_basics.h" -#include "agg_color_rgba.h" -#include "agg_trans_affine.h" -#include "mplutils.h" - -void convert_trans_affine(const py::object& transform, agg::trans_affine& affine); - -inline auto convert_points(py::array_t obj) -{ - if (!check_trailing_shape(obj, "points", 2)) { - throw py::error_already_set(); - } - return obj.unchecked<2>(); -} - -inline auto convert_transforms(py::array_t obj) -{ - if (!check_trailing_shape(obj, "transforms", 3, 3)) { - throw py::error_already_set(); - } - return obj.unchecked<3>(); -} - -inline auto convert_bboxes(py::array_t obj) -{ - if (!check_trailing_shape(obj, "bbox array", 2, 2)) { - throw py::error_already_set(); - } - return obj.unchecked<3>(); -} - -inline auto convert_colors(py::array_t obj) -{ - if (!check_trailing_shape(obj, "colors", 4)) { - throw py::error_already_set(); - } - return obj.unchecked<2>(); -} - -namespace PYBIND11_NAMESPACE { namespace detail { - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(agg::rect_d, const_name("rect_d")); - - bool load(handle src, bool) { - if (src.is_none()) { - value.x1 = 0.0; - value.y1 = 0.0; - value.x2 = 0.0; - value.y2 = 0.0; - return true; - } - - auto rect_arr = py::array_t::ensure(src); - - if (rect_arr.ndim() == 2) { - if (rect_arr.shape(0) != 2 || rect_arr.shape(1) != 2) { - throw py::value_error("Invalid bounding box"); - } - - value.x1 = *rect_arr.data(0, 0); - value.y1 = *rect_arr.data(0, 1); - value.x2 = *rect_arr.data(1, 0); - value.y2 = *rect_arr.data(1, 1); - - } else if (rect_arr.ndim() == 1) { - if (rect_arr.shape(0) != 4) { - throw py::value_error("Invalid bounding box"); - } - - value.x1 = *rect_arr.data(0); - value.y1 = *rect_arr.data(1); - value.x2 = *rect_arr.data(2); - value.y2 = *rect_arr.data(3); - - } else { - throw py::value_error("Invalid bounding box"); - } - - return true; - } - }; - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(agg::rgba, const_name("rgba")); - - bool load(handle src, bool) { - if (src.is_none()) { - value.r = 0.0; - value.g = 0.0; - value.b = 0.0; - value.a = 0.0; - } else { - auto rgbatuple = src.cast(); - value.r = rgbatuple[0].cast(); - value.g = rgbatuple[1].cast(); - value.b = rgbatuple[2].cast(); - switch (rgbatuple.size()) { - case 4: - value.a = rgbatuple[3].cast(); - break; - case 3: - value.a = 1.0; - break; - default: - throw py::value_error("RGBA value must be 3- or 4-tuple"); - } - } - return true; - } - }; - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(agg::trans_affine, const_name("trans_affine")); - - bool load(handle src, bool) { - // If None assume identity transform so leave affine unchanged - if (src.is_none()) { - return true; - } - - auto array = py::array_t::ensure(src); - if (!array || array.ndim() != 2 || - array.shape(0) != 3 || array.shape(1) != 3) { - throw std::invalid_argument("Invalid affine transformation matrix"); - } - - auto buffer = array.data(); - value.sx = buffer[0]; - value.shx = buffer[1]; - value.tx = buffer[2]; - value.shy = buffer[3]; - value.sy = buffer[4]; - value.ty = buffer[5]; - - return true; - } - }; -}} // namespace PYBIND11_NAMESPACE::detail - -#endif /* MPL_PY_CONVERTERS_11_H */ From 56936346bf2027dea2d620653a33a24f6698ec8c Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 20 Sep 2024 22:46:39 -0400 Subject: [PATCH 0342/1230] Remove NumPy from extensions entirely This should be replaced by pybind11 in all cases now. --- .github/workflows/tests.yml | 2 +- pyproject.toml | 13 - requirements/dev/build-requirements.txt | 1 - requirements/testing/mypy.txt | 1 - src/_backend_agg_wrapper.cpp | 9 - src/_path.h | 5 +- src/_path_wrapper.cpp | 12 +- src/ft2font_wrapper.cpp | 9 - src/meson.build | 56 +-- src/numpy_cpp.h | 582 ------------------------ 10 files changed, 8 insertions(+), 682 deletions(-) delete mode 100644 src/numpy_cpp.h diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 062d742b81d9..df73fe1d2169 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -261,7 +261,7 @@ jobs: # Preinstall build requirements to enable no-build-isolation builds. python -m pip install --upgrade $PRE \ 'contourpy>=1.0.1' cycler fonttools kiwisolver importlib_resources \ - numpy packaging pillow 'pyparsing!=3.1.0' python-dateutil setuptools-scm \ + packaging pillow 'pyparsing!=3.1.0' python-dateutil setuptools-scm \ 'meson-python>=0.13.1' 'pybind11>=2.6' \ -r requirements/testing/all.txt \ ${{ matrix.extra-requirements }} diff --git a/pyproject.toml b/pyproject.toml index 5b5c60d60d54..cd0a5c039758 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,6 @@ requires-python = ">=3.10" # Should be a copy of the build dependencies below. dev = [ "meson-python>=0.13.1", - "numpy>=1.25", "pybind11>=2.6,!=2.13.3", "setuptools_scm>=7", # Not required by us but setuptools_scm without a version, cso _if_ @@ -74,18 +73,6 @@ requires = [ "meson-python>=0.13.1", "pybind11>=2.6,!=2.13.3", "setuptools_scm>=7", - - # Comments on numpy build requirement range: - # - # 1. >=2.0.x is the numpy requirement for wheel builds for distribution - # on PyPI - building against 2.x yields wheels that are also - # ABI-compatible with numpy 1.x at runtime. - # 2. Note that building against numpy 1.x works fine too - users and - # redistributors can do this by installing the numpy version they like - # and disabling build isolation. - # 3. The <2.3 upper bound is for matching the numpy deprecation policy, - # it should not be loosened. - "numpy>=2.0.0rc1,<2.3", ] [tool.meson-python.args] diff --git a/requirements/dev/build-requirements.txt b/requirements/dev/build-requirements.txt index 0861a11c9ee5..b5cb6acdb279 100644 --- a/requirements/dev/build-requirements.txt +++ b/requirements/dev/build-requirements.txt @@ -1,4 +1,3 @@ pybind11!=2.13.3 meson-python -numpy<2.1.0 setuptools-scm diff --git a/requirements/testing/mypy.txt b/requirements/testing/mypy.txt index 0b65050b52de..aa20581ee69b 100644 --- a/requirements/testing/mypy.txt +++ b/requirements/testing/mypy.txt @@ -18,7 +18,6 @@ contourpy>=1.0.1 cycler>=0.10 fonttools>=4.22.0 kiwisolver>=1.3.1 -numpy>=1.19 packaging>=20.0 pillow>=8 pyparsing>=2.3.1 diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 3e41eed7452d..bfc8584d688d 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -2,7 +2,6 @@ #include #include #include "mplutils.h" -#include "numpy_cpp.h" #include "py_converters.h" #include "_backend_agg.h" @@ -188,14 +187,6 @@ PyRendererAgg_draw_gouraud_triangles(RendererAgg *self, PYBIND11_MODULE(_backend_agg, m) { - auto ia = [m]() -> const void* { - import_array(); - return &m; - }; - if (ia() == NULL) { - throw py::error_already_set(); - } - py::class_(m, "RendererAgg", py::buffer_protocol()) .def(py::init(), "width"_a, "height"_a, "dpi"_a) diff --git a/src/_path.h b/src/_path.h index 693862c7a829..f5c06e4a6a15 100644 --- a/src/_path.h +++ b/src/_path.h @@ -18,7 +18,6 @@ #include "path_converters.h" #include "_backend_agg_basic_types.h" -#include "numpy_cpp.h" const size_t NUM_VERTICES[] = { 1, 1, 1, 2, 3 }; @@ -1004,7 +1003,7 @@ void convert_path_to_polygons(PathIterator &path, template void -__cleanup_path(VertexSource &source, std::vector &vertices, std::vector &codes) +__cleanup_path(VertexSource &source, std::vector &vertices, std::vector &codes) { unsigned code; double x, y; @@ -1012,7 +1011,7 @@ __cleanup_path(VertexSource &source, std::vector &vertices, std::vector< code = source.vertex(&x, &y); vertices.push_back(x); vertices.push_back(y); - codes.push_back((npy_uint8)code); + codes.push_back(static_cast(code)); } while (code != agg::path_cmd_stop); } diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index 958cb6f5a2b6..77c1b40f9514 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -7,8 +7,6 @@ #include #include -#include "numpy_cpp.h" - #include "_path.h" #include "_backend_agg_basic_types.h" @@ -266,7 +264,7 @@ Py_cleanup_path(mpl::PathIterator path, agg::trans_affine trans, bool remove_nan bool do_clip = (clip_rect.x1 < clip_rect.x2 && clip_rect.y1 < clip_rect.y2); std::vector vertices; - std::vector codes; + std::vector codes; cleanup_path(path, trans, remove_nans, do_clip, clip_rect, snap_mode, stroke_width, *simplify, return_curves, sketch, vertices, codes); @@ -374,14 +372,6 @@ Py_is_sorted_and_has_non_nan(py::object obj) PYBIND11_MODULE(_path, m) { - auto ia = [m]() -> const void* { - import_array(); - return &m; - }; - if (ia() == NULL) { - throw py::error_already_set(); - } - m.def("point_in_path", &Py_point_in_path, "x"_a, "y"_a, "radius"_a, "path"_a, "trans"_a); m.def("points_in_path", &Py_points_in_path, diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 9791dc7e2e06..4358646beede 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -4,7 +4,6 @@ #include #include "ft2font.h" -#include "numpy/arrayobject.h" #include #include @@ -955,14 +954,6 @@ PyFT2Font_fname(PyFT2Font *self) PYBIND11_MODULE(ft2font, m) { - auto ia = [m]() -> const void* { - import_array(); - return &m; - }; - if (ia() == NULL) { - throw py::error_already_set(); - } - if (FT_Init_FreeType(&_ft2Library)) { // initialize library throw std::runtime_error("Could not initialize the freetype2 library"); } diff --git a/src/meson.build b/src/meson.build index ebd02a5f66c6..d2bc9e4afccd 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,42 +1,3 @@ -# NumPy include directory - needed in all submodules -# The try-except is needed because when things are split across drives on Windows, there -# is no relative path and an exception gets raised. There may be other such cases, so add -# a catch-all and switch to an absolute path. Relative paths are needed when for example -# a virtualenv is placed inside the source tree; Meson rejects absolute paths to places -# inside the source tree. -# For cross-compilation it is often not possible to run the Python interpreter in order -# to retrieve numpy's include directory. It can be specified in the cross file instead: -# -# [properties] -# numpy-include-dir = /abspath/to/host-pythons/site-packages/numpy/core/include -# -# This uses the path as is, and avoids running the interpreter. -incdir_numpy = meson.get_external_property('numpy-include-dir', 'not-given') -if incdir_numpy == 'not-given' - incdir_numpy = run_command(py3, - [ - '-c', - '''import os -import numpy as np -try: - incdir = os.path.relpath(np.get_include()) -except Exception: - incdir = np.get_include() -print(incdir)''' - ], - check: true - ).stdout().strip() -endif -numpy_dep = declare_dependency( - compile_args: [ - '-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION', - # Allow NumPy's printf format specifiers in C++. - '-D__STDC_FORMAT_MACROS=1', - ], - include_directories: include_directories(incdir_numpy), - dependencies: py3_dep, -) - # For cross-compilation it is often not possible to run the Python interpreter in order # to retrieve the platform-specific /dev/null. It can be specified in the cross file # instead: @@ -76,7 +37,7 @@ extension_data = { '_backend_agg.cpp', '_backend_agg_wrapper.cpp', ), - 'dependencies': [agg_dep, numpy_dep, freetype_dep, pybind11_dep], + 'dependencies': [agg_dep, freetype_dep, pybind11_dep], }, '_c_internal_utils': { 'subdir': 'matplotlib', @@ -92,7 +53,7 @@ extension_data = { 'ft2font_wrapper.cpp', ), 'dependencies': [ - freetype_dep, pybind11_dep, numpy_dep, agg_dep.partial_dependency(includes: true), + freetype_dep, pybind11_dep, agg_dep.partial_dependency(includes: true), ], 'cpp_args': [ '-DFREETYPE_BUILD_TYPE="@0@"'.format( @@ -117,7 +78,7 @@ extension_data = { 'sources': files( '_path_wrapper.cpp', ), - 'dependencies': [numpy_dep, agg_dep, pybind11_dep], + 'dependencies': [agg_dep, pybind11_dep], }, '_qhull': { 'subdir': 'matplotlib', @@ -157,16 +118,7 @@ extension_data = { } foreach ext, kwargs : extension_data - # Ensure that PY_ARRAY_UNIQUE_SYMBOL is uniquely defined for each extension. - unique_array_api = '-DPY_ARRAY_UNIQUE_SYMBOL=MPL_@0@_ARRAY_API'.format(ext.replace('.', '_')) - additions = { - 'c_args': [unique_array_api] + kwargs.get('c_args', []), - 'cpp_args': [unique_array_api] + kwargs.get('cpp_args', []), - } - py3.extension_module( - ext, - install: true, - kwargs: kwargs + additions) + py3.extension_module(ext, install: true, kwargs: kwargs) endforeach if get_option('macosx') and host_machine.system() == 'darwin' diff --git a/src/numpy_cpp.h b/src/numpy_cpp.h deleted file mode 100644 index 6b7446337bb7..000000000000 --- a/src/numpy_cpp.h +++ /dev/null @@ -1,582 +0,0 @@ -/* -*- mode: c++; c-basic-offset: 4 -*- */ - -#ifndef MPL_NUMPY_CPP_H -#define MPL_NUMPY_CPP_H -#define PY_SSIZE_T_CLEAN -/*************************************************************************** - * This file is based on original work by Mark Wiebe, available at: - * - * http://github.com/mwiebe/numpy-cpp - * - * However, the needs of matplotlib wrappers, such as treating an - * empty array as having the correct dimensions, have made this rather - * matplotlib-specific, so it's no longer compatible with the - * original. - */ - -#ifdef _POSIX_C_SOURCE -# undef _POSIX_C_SOURCE -#endif -#ifndef _AIX -#ifdef _XOPEN_SOURCE -# undef _XOPEN_SOURCE -#endif -#endif - -// Prevent multiple conflicting definitions of swab from stdlib.h and unistd.h -#if defined(__sun) || defined(sun) -#if defined(_XPG4) -#undef _XPG4 -#endif -#if defined(_XPG3) -#undef _XPG3 -#endif -#endif - -#include -#include - -#include "py_exceptions.h" - -#include - -namespace numpy -{ - -// Type traits for the NumPy types -template -struct type_num_of; - -/* Be careful with bool arrays as python has sizeof(npy_bool) == 1, but it is - * not always the case that sizeof(bool) == 1. Using the array_view_accessors - * is always fine regardless of sizeof(bool), so do this rather than using - * array.data() and pointer arithmetic which will not work correctly if - * sizeof(bool) != 1. */ -template <> struct type_num_of -{ - enum { - value = NPY_BOOL - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_BYTE - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_UBYTE - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_SHORT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_USHORT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_INT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_UINT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_LONG - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_ULONG - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_LONGLONG - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_ULONGLONG - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_FLOAT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_DOUBLE - }; -}; -#if NPY_LONGDOUBLE != NPY_DOUBLE -template <> -struct type_num_of -{ - enum { - value = NPY_LONGDOUBLE - }; -}; -#endif -template <> -struct type_num_of -{ - enum { - value = NPY_CFLOAT - }; -}; -template <> -struct type_num_of > -{ - enum { - value = NPY_CFLOAT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_CDOUBLE - }; -}; -template <> -struct type_num_of > -{ - enum { - value = NPY_CDOUBLE - }; -}; -#if NPY_CLONGDOUBLE != NPY_CDOUBLE -template <> -struct type_num_of -{ - enum { - value = NPY_CLONGDOUBLE - }; -}; -template <> -struct type_num_of > -{ - enum { - value = NPY_CLONGDOUBLE - }; -}; -#endif -template <> -struct type_num_of -{ - enum { - value = NPY_OBJECT - }; -}; -template -struct type_num_of -{ - enum { - value = type_num_of::value - }; -}; -template -struct type_num_of -{ - enum { - value = type_num_of::value - }; -}; - -template -struct is_const -{ - enum { - value = false - }; -}; -template -struct is_const -{ - enum { - value = true - }; -}; - -namespace detail -{ -template