From 615017a31e49cfbe542d11c49428822f46ae1232 Mon Sep 17 00:00:00 2001
From: Aaron Meurer
Date: Wed, 8 May 2024 14:28:21 -0600
Subject: [PATCH 001/627] 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 002/627] 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 426abc7ec938f8de77eb5eb0760d262bfe3f5491 Mon Sep 17 00:00:00 2001
From: Aaron Meurer
Date: Fri, 11 Oct 2024 20:56:44 +0200
Subject: [PATCH 003/627] Remove {counter} from output-base-name and remove the
global config
The idea of allowing the use of the counter for custom base names is flawed
because the counter would always be incremented even for custom names that
don't use it. Also, the global base name is difficult to get working because
we don't have a good global view of the plots to create a counter, especially
in the case of partial rebuilds.
Instead, we just simplify things by just allowing setting a custom-base-name.
If two plot directives use the same custom-base-name, then one will end up
overwriting the other (I'm still looking into whether it's possible to at
least detect this and give an error).
---
lib/matplotlib/sphinxext/plot_directive.py | 18 +++++-------------
lib/matplotlib/tests/test_sphinxext.py | 6 +++---
lib/matplotlib/tests/tinypages/some_plots.rst | 6 +++---
3 files changed, 11 insertions(+), 19 deletions(-)
diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py
index 249979c942ad..4e7b0ffd20c7 100644
--- a/lib/matplotlib/sphinxext/plot_directive.py
+++ b/lib/matplotlib/sphinxext/plot_directive.py
@@ -50,10 +50,7 @@
``: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
+ the RST document if no script is provided. Note: two plots with the same
output-base-name may overwrite each other.
``:format:`` : {'python', 'doctest'}
@@ -97,10 +94,6 @@
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).
@@ -313,7 +306,6 @@ 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)
@@ -749,7 +741,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)
+ options.setdefault('output-base-name', None)
if 'class' in options:
# classes are parsed into a list of string, and output by simply
@@ -798,12 +790,12 @@ def run(arguments, content, options, state_machine, state, lineno):
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))
if options['output-base-name']:
- output_base = options['output-base-name'].format(counter=counter)
+ output_base = options['output-base-name']
else:
+ counter = document.attributes.get('_plot_counter', 0) + 1
+ document.attributes['_plot_counter'] = counter
output_base = '%s-%d.py' % (base, counter)
function_name = None
caption = options.get('caption', '')
diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py
index 29d6f3168621..3555ab3462c3 100644
--- a/lib/matplotlib/tests/test_sphinxext.py
+++ b/lib/matplotlib/tests/test_sphinxext.py
@@ -97,9 +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')
+ # plots 23 and 24 use a custom base name
+ assert filecmp.cmp(range_4, img_dir / 'custom-base-name-1.png')
+ assert filecmp.cmp(range_6, img_dir / 'custom-base-name-2.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 b484d705ae1c..05a7fc34c92a 100644
--- a/lib/matplotlib/tests/tinypages/some_plots.rst
+++ b/lib/matplotlib/tests/tinypages/some_plots.rst
@@ -175,14 +175,14 @@ 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}.
+Plots 23 and 24 use output-base-name.
.. plot::
- :output-base-name: custom-base-name-{counter}
+ :output-base-name: custom-base-name-1
plt.plot(range(4))
.. plot::
- :output-base-name: custom-base-name-{counter}
+ :output-base-name: custom-base-name-2
plt.plot(range(6))
From 8485bfd9f1dcd1feabefb8cda0595a58e73321be Mon Sep 17 00:00:00 2001
From: Aaron Meurer
Date: Fri, 11 Oct 2024 21:41:39 +0200
Subject: [PATCH 004/627] Check for duplicate output-base-name in the Sphinx
extension
Previously it would just overwrite one plot with the other.
I did not add tests for this because I would have to create a whole separate
docs build just to test the error (but I have tested it manually).
---
lib/matplotlib/sphinxext/plot_directive.py | 45 +++++++++++++++++++++-
1 file changed, 43 insertions(+), 2 deletions(-)
diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py
index 4e7b0ffd20c7..7b3383e34840 100644
--- a/lib/matplotlib/sphinxext/plot_directive.py
+++ b/lib/matplotlib/sphinxext/plot_directive.py
@@ -50,8 +50,8 @@
``: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. Note: two plots with the same
- output-base-name may overwrite each other.
+ the RST document if no script is provided. The output-base-name for each
+ plot directive must be unique.
``:format:`` : {'python', 'doctest'}
The format of the input. If unset, the format is auto-detected.
@@ -171,6 +171,7 @@
and *TEMPLATE_SRCSET*.
"""
+from collections import defaultdict
import contextlib
import doctest
from io import StringIO
@@ -188,6 +189,7 @@
from docutils.parsers.rst.directives.images import Image
import jinja2 # Sphinx dependency.
+from sphinx.environment.collectors import EnvironmentCollector
from sphinx.errors import ExtensionError
import matplotlib
@@ -319,9 +321,35 @@ def setup(app):
app.connect('build-finished', _copy_css_file)
metadata = {'parallel_read_safe': True, 'parallel_write_safe': True,
'version': matplotlib.__version__}
+ app.connect('builder-inited', init_filename_registry)
+ app.add_env_collector(FilenameCollector)
return metadata
+# -----------------------------------------------------------------------------
+# Handle Duplicate Filenames
+# -----------------------------------------------------------------------------
+
+def init_filename_registry(app):
+ env = app.builder.env
+ if not hasattr(env, 'mpl_custom_base_names'):
+ env.mpl_custom_base_names = defaultdict(set)
+
+class FilenameCollector(EnvironmentCollector):
+ def process_doc(self, app, doctree):
+ pass
+
+ def clear_doc(self, app, env, docname):
+ if docname in env.mpl_custom_base_names:
+ del env.mpl_custom_base_names[docname]
+
+ def merge_other(self, app, env, docnames, other):
+ for docname in docnames:
+ if docname in other.mpl_custom_base_names:
+ if docname not in env.mpl_custom_base_names:
+ env.mpl_custom_base_names[docname] = set()
+ env.mpl_custom_base_names[docname].update(other.mpl_custom_base_names[docname])
+
# -----------------------------------------------------------------------------
# Doctest handling
# -----------------------------------------------------------------------------
@@ -606,6 +634,16 @@ def _parse_srcset(entries):
raise ExtensionError(f'srcset argument {entry!r} is invalid.')
return srcset
+def check_output_base_name(env, output_base):
+ docname = env.docname
+
+ for d in env.mpl_custom_base_names:
+ if output_base in env.mpl_custom_base_names[d]:
+ if d == docname:
+ raise PlotError(f"The output-base-name '{output_base}' is used multiple times.")
+ raise PlotError(f"The output-base-name '{output_base}' is used multiple times (it is also used in {env.doc2path(d)}).")
+
+ env.mpl_custom_base_names[docname].add(output_base)
def render_figures(code, code_path, output_dir, output_base, context,
function_name, config, context_reset=False,
@@ -730,6 +768,7 @@ def render_figures(code, code_path, output_dir, output_base, context,
def run(arguments, content, options, state_machine, state, lineno):
document = state_machine.document
config = document.settings.env.config
+ env = document.settings.env
nofigs = 'nofigs' in options
if config.plot_srcset and setup.app.builder.name == 'singlehtml':
@@ -785,6 +824,7 @@ def run(arguments, content, options, state_machine, state, lineno):
code = Path(source_file_name).read_text(encoding='utf-8')
if options['output-base-name']:
output_base = options['output-base-name']
+ check_output_base_name(env, output_base)
else:
output_base = os.path.basename(source_file_name)
else:
@@ -793,6 +833,7 @@ def run(arguments, content, options, state_machine, state, lineno):
base, ext = os.path.splitext(os.path.basename(source_file_name))
if options['output-base-name']:
output_base = options['output-base-name']
+ check_output_base_name(env, output_base)
else:
counter = document.attributes.get('_plot_counter', 0) + 1
document.attributes['_plot_counter'] = counter
From f94a9324036a8da1141e4c275eb231c26c20cccc Mon Sep 17 00:00:00 2001
From: Aaron Meurer
Date: Fri, 11 Oct 2024 19:04:02 -0500
Subject: [PATCH 005/627] Fix flake8 errors
---
lib/matplotlib/sphinxext/plot_directive.py | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py
index 7b3383e34840..abce90dfc0e3 100644
--- a/lib/matplotlib/sphinxext/plot_directive.py
+++ b/lib/matplotlib/sphinxext/plot_directive.py
@@ -335,6 +335,7 @@ def init_filename_registry(app):
if not hasattr(env, 'mpl_custom_base_names'):
env.mpl_custom_base_names = defaultdict(set)
+
class FilenameCollector(EnvironmentCollector):
def process_doc(self, app, doctree):
pass
@@ -348,7 +349,8 @@ def merge_other(self, app, env, docnames, other):
if docname in other.mpl_custom_base_names:
if docname not in env.mpl_custom_base_names:
env.mpl_custom_base_names[docname] = set()
- env.mpl_custom_base_names[docname].update(other.mpl_custom_base_names[docname])
+ env.mpl_custom_base_names[docname].update(
+ other.mpl_custom_base_names[docname])
# -----------------------------------------------------------------------------
# Doctest handling
@@ -634,17 +636,23 @@ def _parse_srcset(entries):
raise ExtensionError(f'srcset argument {entry!r} is invalid.')
return srcset
+
def check_output_base_name(env, output_base):
docname = env.docname
for d in env.mpl_custom_base_names:
if output_base in env.mpl_custom_base_names[d]:
if d == docname:
- raise PlotError(f"The output-base-name '{output_base}' is used multiple times.")
- raise PlotError(f"The output-base-name '{output_base}' is used multiple times (it is also used in {env.doc2path(d)}).")
+ raise PlotError(
+ f"The output-base-name "
+ f"{output_base}' is used multiple times.")
+ raise PlotError(f"The output-base-name "
+ f"'{output_base}' is used multiple times "
+ f"(it is also used in {env.doc2path(d)}).")
env.mpl_custom_base_names[docname].add(output_base)
+
def render_figures(code, code_path, output_dir, output_base, context,
function_name, config, context_reset=False,
close_figs=False,
From 8f05ba6eb541cb7f19ec74f8c6e934a32fdd9380 Mon Sep 17 00:00:00 2001
From: Aaron Meurer
Date: Sat, 12 Oct 2024 16:31:22 -0600
Subject: [PATCH 006/627] Make an internal class private
This will hopefully fix Sphinx trying and failing to include it.
---
lib/matplotlib/sphinxext/plot_directive.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py
index abce90dfc0e3..71fe0e7d7850 100644
--- a/lib/matplotlib/sphinxext/plot_directive.py
+++ b/lib/matplotlib/sphinxext/plot_directive.py
@@ -322,7 +322,7 @@ def setup(app):
metadata = {'parallel_read_safe': True, 'parallel_write_safe': True,
'version': matplotlib.__version__}
app.connect('builder-inited', init_filename_registry)
- app.add_env_collector(FilenameCollector)
+ app.add_env_collector(_FilenameCollector)
return metadata
@@ -336,7 +336,7 @@ def init_filename_registry(app):
env.mpl_custom_base_names = defaultdict(set)
-class FilenameCollector(EnvironmentCollector):
+class _FilenameCollector(EnvironmentCollector):
def process_doc(self, app, doctree):
pass
From 1fa88dd1e3936f3fe2847b1b532f8cae214428d1 Mon Sep 17 00:00:00 2001
From: Aaron Meurer
Date: Mon, 14 Oct 2024 10:32:31 -0600
Subject: [PATCH 007/627] Fix small code nit
---
lib/matplotlib/sphinxext/plot_directive.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py
index 71fe0e7d7850..16866459e784 100644
--- a/lib/matplotlib/sphinxext/plot_directive.py
+++ b/lib/matplotlib/sphinxext/plot_directive.py
@@ -838,11 +838,11 @@ def run(arguments, content, options, state_machine, state, lineno):
else:
source_file_name = rst_file
code = textwrap.dedent("\n".join(map(str, content)))
- base, ext = os.path.splitext(os.path.basename(source_file_name))
if options['output-base-name']:
output_base = options['output-base-name']
check_output_base_name(env, output_base)
else:
+ base, ext = os.path.splitext(os.path.basename(source_file_name))
counter = document.attributes.get('_plot_counter', 0) + 1
document.attributes['_plot_counter'] = counter
output_base = '%s-%d.py' % (base, counter)
From a22fcc304e251b28b46f9f69884fe053a7790e81 Mon Sep 17 00:00:00 2001
From: Aaron Meurer
Date: Mon, 14 Oct 2024 10:32:41 -0600
Subject: [PATCH 008/627] Add a test for output-base-name with a .py file
---
lib/matplotlib/tests/test_sphinxext.py | 7 ++++---
lib/matplotlib/tests/tinypages/some_plots.rst | 13 ++++++++-----
2 files changed, 12 insertions(+), 8 deletions(-)
diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py
index 758b440d4dc0..4370d9cea47d 100644
--- a/lib/matplotlib/tests/test_sphinxext.py
+++ b/lib/matplotlib/tests/test_sphinxext.py
@@ -96,9 +96,10 @@ 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
- assert filecmp.cmp(range_4, img_dir / 'custom-base-name-1.png')
- assert filecmp.cmp(range_6, img_dir / 'custom-base-name-2.png')
+ # plots 23 through 25 use a custom base name
+ assert filecmp.cmp(range_6, img_dir / 'custom-base-name-6.png')
+ assert filecmp.cmp(range_10, img_dir / 'custom-base-name-10.png')
+ assert filecmp.cmp(range_4, img_dir / 'custom-base-name-4.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 05a7fc34c92a..7652c7301563 100644
--- a/lib/matplotlib/tests/tinypages/some_plots.rst
+++ b/lib/matplotlib/tests/tinypages/some_plots.rst
@@ -175,14 +175,17 @@ 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.
+Plots 23 through 25 use output-base-name.
.. plot::
- :output-base-name: custom-base-name-1
+ :output-base-name: custom-base-name-6
- plt.plot(range(4))
+ plt.plot(range(6))
.. plot::
- :output-base-name: custom-base-name-2
+ :output-base-name: custom-base-name-10
- plt.plot(range(6))
+ plt.plot(range(10))
+
+.. plot:: range4.py
+ :output-base-name: custom-base-name-4
From 86fb16710c43a5e6c2354371ff3124d39178b876 Mon Sep 17 00:00:00 2001
From: Aaron Meurer
Date: Thu, 17 Oct 2024 12:37:25 -0600
Subject: [PATCH 009/627] Remove a redundant test
---
lib/matplotlib/tests/test_sphinxext.py | 3 +--
lib/matplotlib/tests/tinypages/some_plots.rst | 7 +------
2 files changed, 2 insertions(+), 8 deletions(-)
diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py
index 4370d9cea47d..64759f650b06 100644
--- a/lib/matplotlib/tests/test_sphinxext.py
+++ b/lib/matplotlib/tests/test_sphinxext.py
@@ -96,9 +96,8 @@ 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 through 25 use a custom base name
+ # plots 23 and 24 use a custom base name
assert filecmp.cmp(range_6, img_dir / 'custom-base-name-6.png')
- assert filecmp.cmp(range_10, img_dir / 'custom-base-name-10.png')
assert filecmp.cmp(range_4, img_dir / 'custom-base-name-4.png')
# Modify the included plot
diff --git a/lib/matplotlib/tests/tinypages/some_plots.rst b/lib/matplotlib/tests/tinypages/some_plots.rst
index 7652c7301563..51348415f128 100644
--- a/lib/matplotlib/tests/tinypages/some_plots.rst
+++ b/lib/matplotlib/tests/tinypages/some_plots.rst
@@ -175,17 +175,12 @@ Plot 22 uses a different specific function in a file with plot commands:
.. plot:: range6.py range10
-Plots 23 through 25 use output-base-name.
+Plots 23 and 24 use output-base-name.
.. plot::
:output-base-name: custom-base-name-6
plt.plot(range(6))
-.. plot::
- :output-base-name: custom-base-name-10
-
- plt.plot(range(10))
-
.. plot:: range4.py
:output-base-name: custom-base-name-4
From e0be21e57d68e49d1380bae4af22a44575abca55 Mon Sep 17 00:00:00 2001
From: Aaron Meurer
Date: Thu, 17 Oct 2024 12:37:35 -0600
Subject: [PATCH 010/627] Disallow / or . in output-base-name
---
lib/matplotlib/sphinxext/plot_directive.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py
index 16866459e784..3e10aac176c7 100644
--- a/lib/matplotlib/sphinxext/plot_directive.py
+++ b/lib/matplotlib/sphinxext/plot_directive.py
@@ -640,6 +640,11 @@ def _parse_srcset(entries):
def check_output_base_name(env, output_base):
docname = env.docname
+ if '.' in output_base or '/' in output_base:
+ raise PlotError(
+ f"The output-base-name '{output_base}' is invalid. "
+ f"It must not contain dots or slashes.")
+
for d in env.mpl_custom_base_names:
if output_base in env.mpl_custom_base_names[d]:
if d == docname:
From f322125f9fdd2734831130de9d975d73bbff7a92 Mon Sep 17 00:00:00 2001
From: Aaron Meurer
Date: Fri, 18 Oct 2024 12:27:34 -0600
Subject: [PATCH 011/627] Rename output-base-name to image-basename
---
lib/matplotlib/sphinxext/plot_directive.py | 22 +++++++++----------
lib/matplotlib/tests/test_sphinxext.py | 6 ++---
lib/matplotlib/tests/tinypages/some_plots.rst | 6 ++---
3 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py
index 3e10aac176c7..d4b547686283 100644
--- a/lib/matplotlib/sphinxext/plot_directive.py
+++ b/lib/matplotlib/sphinxext/plot_directive.py
@@ -47,10 +47,10 @@
The ``.. plot::`` directive supports the following options:
-``:output-base-name:`` : str
+``:image-basename:`` : 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 output-base-name for each
+ the RST document if no script is provided. The image-basename for each
plot directive must be unique.
``:format:`` : {'python', 'doctest'}
@@ -273,7 +273,7 @@ class PlotDirective(Directive):
'scale': directives.nonnegative_int,
'align': Image.align,
'class': directives.class_option,
- 'output-base-name': directives.unchanged,
+ 'image-basename': directives.unchanged,
'include-source': _option_boolean,
'show-source-link': _option_boolean,
'format': _option_format,
@@ -642,16 +642,16 @@ def check_output_base_name(env, output_base):
if '.' in output_base or '/' in output_base:
raise PlotError(
- f"The output-base-name '{output_base}' is invalid. "
+ f"The image-basename '{output_base}' is invalid. "
f"It must not contain dots or slashes.")
for d in env.mpl_custom_base_names:
if output_base in env.mpl_custom_base_names[d]:
if d == docname:
raise PlotError(
- f"The output-base-name "
+ f"The image-basename "
f"{output_base}' is used multiple times.")
- raise PlotError(f"The output-base-name "
+ raise PlotError(f"The image-basename "
f"'{output_base}' is used multiple times "
f"(it is also used in {env.doc2path(d)}).")
@@ -793,7 +793,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', None)
+ options.setdefault('image-basename', None)
if 'class' in options:
# classes are parsed into a list of string, and output by simply
@@ -835,16 +835,16 @@ def run(arguments, content, options, state_machine, state, lineno):
function_name = None
code = Path(source_file_name).read_text(encoding='utf-8')
- if options['output-base-name']:
- output_base = options['output-base-name']
+ if options['image-basename']:
+ output_base = options['image-basename']
check_output_base_name(env, output_base)
else:
output_base = os.path.basename(source_file_name)
else:
source_file_name = rst_file
code = textwrap.dedent("\n".join(map(str, content)))
- if options['output-base-name']:
- output_base = options['output-base-name']
+ if options['image-basename']:
+ output_base = options['image-basename']
check_output_base_name(env, output_base)
else:
base, ext = os.path.splitext(os.path.basename(source_file_name))
diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py
index 64759f650b06..139bd56d8fe3 100644
--- a/lib/matplotlib/tests/test_sphinxext.py
+++ b/lib/matplotlib/tests/test_sphinxext.py
@@ -96,9 +96,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
- assert filecmp.cmp(range_6, img_dir / 'custom-base-name-6.png')
- assert filecmp.cmp(range_4, img_dir / 'custom-base-name-4.png')
+ # plots 23 and 24 use a custom basename
+ assert filecmp.cmp(range_6, img_dir / 'custom-basename-6.png')
+ assert filecmp.cmp(range_4, img_dir / 'custom-basename-4.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 51348415f128..85e53086e792 100644
--- a/lib/matplotlib/tests/tinypages/some_plots.rst
+++ b/lib/matplotlib/tests/tinypages/some_plots.rst
@@ -175,12 +175,12 @@ 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.
+Plots 23 and 24 use image-basename.
.. plot::
- :output-base-name: custom-base-name-6
+ :image-basename: custom-basename-6
plt.plot(range(6))
.. plot:: range4.py
- :output-base-name: custom-base-name-4
+ :image-basename: custom-basename-4
From fc33c3887dd5a8455d06b329a4a32c302771f267 Mon Sep 17 00:00:00 2001
From: Aaron Meurer
Date: Mon, 21 Oct 2024 12:30:41 -0600
Subject: [PATCH 012/627] Use a better variable name
---
lib/matplotlib/sphinxext/plot_directive.py | 24 +++++++++++-----------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py
index d4b547686283..535b4b5b5fd9 100644
--- a/lib/matplotlib/sphinxext/plot_directive.py
+++ b/lib/matplotlib/sphinxext/plot_directive.py
@@ -332,8 +332,8 @@ def setup(app):
def init_filename_registry(app):
env = app.builder.env
- if not hasattr(env, 'mpl_custom_base_names'):
- env.mpl_custom_base_names = defaultdict(set)
+ if not hasattr(env, 'mpl_plot_image_basenames'):
+ env.mpl_plot_image_basenames = defaultdict(set)
class _FilenameCollector(EnvironmentCollector):
@@ -341,16 +341,16 @@ def process_doc(self, app, doctree):
pass
def clear_doc(self, app, env, docname):
- if docname in env.mpl_custom_base_names:
- del env.mpl_custom_base_names[docname]
+ if docname in env.mpl_plot_image_basenames:
+ del env.mpl_plot_image_basenames[docname]
def merge_other(self, app, env, docnames, other):
for docname in docnames:
- if docname in other.mpl_custom_base_names:
- if docname not in env.mpl_custom_base_names:
- env.mpl_custom_base_names[docname] = set()
- env.mpl_custom_base_names[docname].update(
- other.mpl_custom_base_names[docname])
+ if docname in other.mpl_plot_image_basenames:
+ if docname not in env.mpl_plot_image_basenames:
+ env.mpl_plot_image_basenames[docname] = set()
+ env.mpl_plot_image_basenames[docname].update(
+ other.mpl_plot_image_basenames[docname])
# -----------------------------------------------------------------------------
# Doctest handling
@@ -645,8 +645,8 @@ def check_output_base_name(env, output_base):
f"The image-basename '{output_base}' is invalid. "
f"It must not contain dots or slashes.")
- for d in env.mpl_custom_base_names:
- if output_base in env.mpl_custom_base_names[d]:
+ for d in env.mpl_plot_image_basenames:
+ if output_base in env.mpl_plot_image_basenames[d]:
if d == docname:
raise PlotError(
f"The image-basename "
@@ -655,7 +655,7 @@ def check_output_base_name(env, output_base):
f"'{output_base}' is used multiple times "
f"(it is also used in {env.doc2path(d)}).")
- env.mpl_custom_base_names[docname].add(output_base)
+ env.mpl_plot_image_basenames[docname].add(output_base)
def render_figures(code, code_path, output_dir, output_base, context,
From 7d416cf7cc90151c80435df583bf16f3524c565f Mon Sep 17 00:00:00 2001
From: Aaron Meurer
Date: Mon, 21 Oct 2024 12:34:48 -0600
Subject: [PATCH 013/627] Simplify logic in merge_other
---
lib/matplotlib/sphinxext/plot_directive.py | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py
index 535b4b5b5fd9..d6034ac9cd9a 100644
--- a/lib/matplotlib/sphinxext/plot_directive.py
+++ b/lib/matplotlib/sphinxext/plot_directive.py
@@ -345,12 +345,10 @@ def clear_doc(self, app, env, docname):
del env.mpl_plot_image_basenames[docname]
def merge_other(self, app, env, docnames, other):
- for docname in docnames:
- if docname in other.mpl_plot_image_basenames:
- if docname not in env.mpl_plot_image_basenames:
- env.mpl_plot_image_basenames[docname] = set()
- env.mpl_plot_image_basenames[docname].update(
- other.mpl_plot_image_basenames[docname])
+ for docname in other.mpl_plot_image_basenames:
+ env.mpl_plot_image_basenames[docname].update(
+ other.mpl_plot_image_basenames[docname])
+
# -----------------------------------------------------------------------------
# Doctest handling
From 74af1bbb2bf36092ef7edf1a2850face9bb06b24 Mon Sep 17 00:00:00 2001
From: DerWeh
Date: Wed, 29 Jan 2025 22:03:16 +0100
Subject: [PATCH 014/627] DOC: correctly specify return type of `figaspect`
---
lib/matplotlib/figure.py | 4 ++--
lib/matplotlib/figure.pyi | 4 +++-
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py
index 76b2df563ade..759e9f22dc9b 100644
--- a/lib/matplotlib/figure.py
+++ b/lib/matplotlib/figure.py
@@ -3655,8 +3655,8 @@ def figaspect(arg):
Returns
-------
- width, height : float
- The figure size in inches.
+ size : (2,) array
+ The width and height of the figure in inches.
Notes
-----
diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi
index 08bf1505532b..064503c91384 100644
--- a/lib/matplotlib/figure.pyi
+++ b/lib/matplotlib/figure.pyi
@@ -418,4 +418,6 @@ class Figure(FigureBase):
rect: tuple[float, float, float, float] | None = ...
) -> None: ...
-def figaspect(arg: float | ArrayLike) -> tuple[float, float]: ...
+def figaspect(
+ arg: float | ArrayLike,
+) -> np.ndarray[tuple[Literal[2]], np.dtype[np.float64]]: ...
From 59b6d9cd2e1caa41cd9274ed2394a5176c7d77b3 Mon Sep 17 00:00:00 2001
From: Weh Andreas
Date: Mon, 3 Feb 2025 11:23:11 +0100
Subject: [PATCH 015/627] TYP: relax type hints for figure's figsize
---
lib/matplotlib/pyplot.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py
index 43c4ac70bff0..3068185d2cc8 100644
--- a/lib/matplotlib/pyplot.py
+++ b/lib/matplotlib/pyplot.py
@@ -876,7 +876,7 @@ def figure(
# autoincrement if None, else integer from 1-N
num: int | str | Figure | SubFigure | None = None,
# defaults to rc figure.figsize
- figsize: tuple[float, float] | None = None,
+ figsize: ArrayLike | None = None,
# defaults to rc figure.dpi
dpi: float | None = None,
*,
From d7da1fce96e11e31c24700fe798fcaabe193f109 Mon Sep 17 00:00:00 2001
From: anTon <138380708+r3kste@users.noreply.github.com>
Date: Fri, 3 Jan 2025 17:39:44 +0530
Subject: [PATCH 016/627] add hatchcolors param for collections
---
lib/matplotlib/backend_bases.py | 32 +++++++----
lib/matplotlib/backend_bases.pyi | 2 +
lib/matplotlib/backends/backend_pdf.py | 9 ++--
lib/matplotlib/backends/backend_ps.py | 8 +--
lib/matplotlib/backends/backend_svg.py | 8 +--
lib/matplotlib/collections.py | 54 +++++++++++++------
lib/matplotlib/collections.pyi | 5 ++
lib/matplotlib/legend_handler.py | 3 +-
lib/matplotlib/tests/test_collections.py | 69 ++++++++++++++++++++++++
src/_backend_agg.h | 32 +++++++----
src/_backend_agg_wrapper.cpp | 19 ++++---
11 files changed, 190 insertions(+), 51 deletions(-)
diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py
index 62c26a90a91c..494f40b1ff06 100644
--- a/lib/matplotlib/backend_bases.py
+++ b/lib/matplotlib/backend_bases.py
@@ -208,7 +208,7 @@ def draw_markers(self, gc, marker_path, marker_trans, path,
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
- offset_position):
+ offset_position, hatchcolors=None):
"""
Draw a collection of *paths*.
@@ -217,8 +217,8 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
*master_transform*. They are then translated by the corresponding
entry in *offsets*, which has been first transformed by *offset_trans*.
- *facecolors*, *edgecolors*, *linewidths*, *linestyles*, and
- *antialiased* are lists that set the corresponding properties.
+ *facecolors*, *edgecolors*, *linewidths*, *linestyles*, *antialiased*
+ and *hatchcolors* are lists that set the corresponding properties.
*offset_position* is unused now, but the argument is kept for
backwards compatibility.
@@ -235,10 +235,13 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
path_ids = self._iter_collection_raw_paths(master_transform,
paths, all_transforms)
+ if hatchcolors is None:
+ hatchcolors = []
+
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
gc, list(path_ids), offsets, offset_trans,
facecolors, edgecolors, linewidths, linestyles,
- antialiaseds, urls, offset_position):
+ antialiaseds, urls, offset_position, hatchcolors):
path, transform = path_id
# Only apply another translation if we have an offset, else we
# reuse the initial transform.
@@ -252,7 +255,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
coordinates, offsets, offsetTrans, facecolors,
- antialiased, edgecolors):
+ antialiased, edgecolors, hatchcolors=None):
"""
Draw a quadmesh.
@@ -265,11 +268,13 @@ def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
if edgecolors is None:
edgecolors = facecolors
+ if hatchcolors is None:
+ hatchcolors = []
linewidths = np.array([gc.get_linewidth()], float)
return self.draw_path_collection(
gc, master_transform, paths, [], offsets, offsetTrans, facecolors,
- edgecolors, linewidths, [], [antialiased], [None], 'screen')
+ edgecolors, linewidths, [], [antialiased], [None], 'screen', hatchcolors)
def draw_gouraud_triangles(self, gc, triangles_array, colors_array,
transform):
@@ -337,7 +342,7 @@ def _iter_collection_uses_per_path(self, paths, all_transforms,
def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors,
edgecolors, linewidths, linestyles,
- antialiaseds, urls, offset_position):
+ antialiaseds, urls, offset_position, hatchcolors=None):
"""
Helper method (along with `_iter_collection_raw_paths`) to implement
`draw_path_collection` in a memory-efficient manner.
@@ -360,16 +365,20 @@ def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors,
*path_ids*; *gc* is a graphics context and *rgbFace* is a color to
use for filling the path.
"""
+ if hatchcolors is None:
+ hatchcolors = []
+
Npaths = len(path_ids)
Noffsets = len(offsets)
N = max(Npaths, Noffsets)
Nfacecolors = len(facecolors)
Nedgecolors = len(edgecolors)
+ Nhatchcolors = len(hatchcolors)
Nlinewidths = len(linewidths)
Nlinestyles = len(linestyles)
Nurls = len(urls)
- if (Nfacecolors == 0 and Nedgecolors == 0) or Npaths == 0:
+ if (Nfacecolors == 0 and Nedgecolors == 0 and Nhatchcolors == 0) or Npaths == 0:
return
gc0 = self.new_gc()
@@ -384,6 +393,7 @@ def cycle_or_default(seq, default=None):
toffsets = cycle_or_default(offset_trans.transform(offsets), (0, 0))
fcs = cycle_or_default(facecolors)
ecs = cycle_or_default(edgecolors)
+ hcs = cycle_or_default(hatchcolors)
lws = cycle_or_default(linewidths)
lss = cycle_or_default(linestyles)
aas = cycle_or_default(antialiaseds)
@@ -392,8 +402,8 @@ def cycle_or_default(seq, default=None):
if Nedgecolors == 0:
gc0.set_linewidth(0.0)
- for pathid, (xo, yo), fc, ec, lw, ls, aa, url in itertools.islice(
- zip(pathids, toffsets, fcs, ecs, lws, lss, aas, urls), N):
+ for pathid, (xo, yo), fc, ec, hc, lw, ls, aa, url in itertools.islice(
+ zip(pathids, toffsets, fcs, ecs, hcs, lws, lss, aas, urls), N):
if not (np.isfinite(xo) and np.isfinite(yo)):
continue
if Nedgecolors:
@@ -405,6 +415,8 @@ def cycle_or_default(seq, default=None):
gc0.set_linewidth(0)
else:
gc0.set_foreground(ec)
+ if Nhatchcolors:
+ gc0.set_hatch_color(hc)
if fc is not None and len(fc) == 4 and fc[3] == 0:
fc = None
gc0.set_antialiased(aa)
diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi
index 898d6fb0e7f5..33c10adf9c2a 100644
--- a/lib/matplotlib/backend_bases.pyi
+++ b/lib/matplotlib/backend_bases.pyi
@@ -63,6 +63,7 @@ class RendererBase:
antialiaseds: bool | Sequence[bool],
urls: str | Sequence[str],
offset_position: Any,
+ hatchcolors: ColorType | Sequence[ColorType] | None = None,
) -> None: ...
def draw_quad_mesh(
self,
@@ -76,6 +77,7 @@ class RendererBase:
facecolors: Sequence[ColorType],
antialiased: bool,
edgecolors: Sequence[ColorType] | ColorType | None,
+ hatchcolors: Sequence[ColorType] | ColorType | None = None,
) -> None: ...
def draw_gouraud_triangles(
self,
diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py
index c1c5eb8819be..0607b5455414 100644
--- a/lib/matplotlib/backends/backend_pdf.py
+++ b/lib/matplotlib/backends/backend_pdf.py
@@ -2030,7 +2030,7 @@ def draw_path(self, gc, path, transform, rgbFace=None):
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
- offset_position):
+ offset_position, hatchcolors=None):
# We can only reuse the objects if the presence of fill and
# stroke (and the amount of alpha for each) is the same for
# all of them
@@ -2038,6 +2038,9 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
facecolors = np.asarray(facecolors)
edgecolors = np.asarray(edgecolors)
+ if hatchcolors is None:
+ hatchcolors = []
+
if not len(facecolors):
filled = False
can_do_optimization = not gc.get_hatch()
@@ -2072,7 +2075,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
self, gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
- offset_position)
+ offset_position, hatchcolors)
padding = np.max(linewidths)
path_codes = []
@@ -2088,7 +2091,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
gc, path_codes, offsets, offset_trans,
facecolors, edgecolors, linewidths, linestyles,
- antialiaseds, urls, offset_position):
+ antialiaseds, urls, offset_position, hatchcolors):
self.check_gc(gc0, rgbFace)
dx, dy = xo - lastx, yo - lasty
diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py
index f1f914ae5420..18d69c8684b8 100644
--- a/lib/matplotlib/backends/backend_ps.py
+++ b/lib/matplotlib/backends/backend_ps.py
@@ -674,7 +674,9 @@ def draw_markers(
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
- offset_position):
+ offset_position, hatchcolors=None):
+ if hatchcolors is None:
+ hatchcolors = []
# Is the optimization worth it? Rough calculation:
# cost of emitting a path in-line is
# (len_path + 2) * uses_per_path
@@ -690,7 +692,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
self, gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
- offset_position)
+ offset_position, hatchcolors)
path_codes = []
for i, (path, transform) in enumerate(self._iter_collection_raw_paths(
@@ -709,7 +711,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
gc, path_codes, offsets, offset_trans,
facecolors, edgecolors, linewidths, linestyles,
- antialiaseds, urls, offset_position):
+ antialiaseds, urls, offset_position, hatchcolors):
ps = f"{xo:g} {yo:g} {path_id}"
self._draw_ps(ps, gc0, rgbFace)
diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py
index 2193dc6b6cdc..9ae079dd2260 100644
--- a/lib/matplotlib/backends/backend_svg.py
+++ b/lib/matplotlib/backends/backend_svg.py
@@ -736,7 +736,9 @@ def draw_markers(
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
- offset_position):
+ offset_position, hatchcolors=None):
+ if hatchcolors is None:
+ hatchcolors = []
# Is the optimization worth it? Rough calculation:
# cost of emitting a path in-line is
# (len_path + 5) * uses_per_path
@@ -752,7 +754,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
gc, master_transform, paths, all_transforms,
offsets, offset_trans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
- offset_position)
+ offset_position, hatchcolors)
writer = self.writer
path_codes = []
@@ -770,7 +772,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
gc, path_codes, offsets, offset_trans,
facecolors, edgecolors, linewidths, linestyles,
- antialiaseds, urls, offset_position):
+ antialiaseds, urls, offset_position, hatchcolors):
url = gc0.get_url()
if url is not None:
writer.start('a', attrib={'xlink:href': url})
diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py
index a3f245bbc2c8..a29ec87f07fd 100644
--- a/lib/matplotlib/collections.py
+++ b/lib/matplotlib/collections.py
@@ -79,6 +79,7 @@ class Collection(mcolorizer.ColorizingArtist):
def __init__(self, *,
edgecolors=None,
facecolors=None,
+ hatchcolors=None,
linewidths=None,
linestyles='solid',
capstyle=None,
@@ -174,13 +175,6 @@ def __init__(self, *,
self._face_is_mapped = None
self._edge_is_mapped = None
self._mapped_colors = None # calculated in update_scalarmappable
-
- # Temporary logic to set hatchcolor. This eager resolution is temporary
- # and will be replaced by a proper mechanism in a follow-up PR.
- hatch_color = mpl.rcParams['hatch.color']
- if hatch_color == 'edge':
- hatch_color = mpl.rcParams['patch.edgecolor']
- self._hatch_color = mcolors.to_rgba(hatch_color)
self._hatch_linewidth = mpl.rcParams['hatch.linewidth']
self.set_facecolor(facecolors)
self.set_edgecolor(edgecolors)
@@ -190,6 +184,7 @@ def __init__(self, *,
self.set_pickradius(pickradius)
self.set_urls(urls)
self.set_hatch(hatch)
+ self.set_hatchcolor(hatchcolors)
self.set_zorder(zorder)
if capstyle:
@@ -371,7 +366,6 @@ def draw(self, renderer):
if self._hatch:
gc.set_hatch(self._hatch)
- gc.set_hatch_color(self._hatch_color)
gc.set_hatch_linewidth(self._hatch_linewidth)
if self.get_sketch_params() is not None:
@@ -431,7 +425,7 @@ def draw(self, renderer):
[mcolors.to_rgba("none")], self._gapcolor,
self._linewidths, ilinestyles,
self._antialiaseds, self._urls,
- "screen")
+ "screen", self.get_hatchcolor())
renderer.draw_path_collection(
gc, transform.frozen(), paths,
@@ -439,7 +433,8 @@ def draw(self, renderer):
self.get_facecolor(), self.get_edgecolor(),
self._linewidths, self._linestyles,
self._antialiaseds, self._urls,
- "screen") # offset_position, kept for backcompat.
+ "screen", # offset_position, kept for backcompat.
+ self.get_hatchcolor())
gc.restore()
renderer.close_group(self.__class__.__name__)
@@ -814,8 +809,15 @@ def _get_default_edgecolor(self):
# This may be overridden in a subclass.
return mpl.rcParams['patch.edgecolor']
+ def get_hatchcolor(self):
+ if cbook._str_equal(self._hatchcolors, 'edge'):
+ if self.get_edgecolor().size == 0:
+ return mpl.colors.to_rgba_array(self._get_default_edgecolor(),
+ self._alpha)
+ return self.get_edgecolor()
+ return self._hatchcolors
+
def _set_edgecolor(self, c):
- set_hatch_color = True
if c is None:
if (mpl.rcParams['patch.force_edgecolor']
or self._edge_default
@@ -823,14 +825,11 @@ def _set_edgecolor(self, c):
c = self._get_default_edgecolor()
else:
c = 'none'
- set_hatch_color = False
if cbook._str_lower_equal(c, 'face'):
self._edgecolors = 'face'
self.stale = True
return
self._edgecolors = mcolors.to_rgba_array(c, self._alpha)
- if set_hatch_color and len(self._edgecolors):
- self._hatch_color = tuple(self._edgecolors[0])
self.stale = True
def set_edgecolor(self, c):
@@ -851,6 +850,29 @@ def set_edgecolor(self, c):
self._original_edgecolor = c
self._set_edgecolor(c)
+ def _set_hatchcolor(self, c):
+ c = mpl._val_or_rc(c, 'hatch.color')
+ if c == 'edge':
+ self._hatchcolors = 'edge'
+ else:
+ self._hatchcolors = mcolors.to_rgba_array(c, self._alpha)
+ self.stale = True
+
+ def set_hatchcolor(self, c):
+ """
+ Set the hatchcolor(s) of the collection.
+
+ Parameters
+ ----------
+ c : :mpltype:`color` or list of :mpltype:`color` or 'edge'
+ The collection hatchcolor(s). If a sequence, the patches cycle
+ through it.
+ """
+ if cbook._str_equal(c, 'edge'):
+ c = 'edge'
+ self._original_hatchcolor = c
+ self._set_hatchcolor(c)
+
def set_alpha(self, alpha):
"""
Set the transparency of the collection.
@@ -968,6 +990,7 @@ def update_from(self, other):
self._us_linestyles = other._us_linestyles
self._pickradius = other._pickradius
self._hatch = other._hatch
+ self._hatchcolors = other._hatchcolors
# update_from for scalarmappable
self._A = other._A
@@ -2465,7 +2488,8 @@ def draw(self, renderer):
coordinates, offsets, offset_trf,
# Backends expect flattened rgba arrays (n*m, 4) for fc and ec
self.get_facecolor().reshape((-1, 4)),
- self._antialiased, self.get_edgecolors().reshape((-1, 4)))
+ self._antialiased, self.get_edgecolors().reshape((-1, 4)),
+ self.get_hatchcolor().reshape((-1, 4)))
gc.restore()
renderer.close_group(self.__class__.__name__)
self.stale = False
diff --git a/lib/matplotlib/collections.pyi b/lib/matplotlib/collections.pyi
index 0805adef4293..a7ad264fb59d 100644
--- a/lib/matplotlib/collections.pyi
+++ b/lib/matplotlib/collections.pyi
@@ -21,6 +21,7 @@ class Collection(colorizer.ColorizingArtist):
*,
edgecolors: ColorType | Sequence[ColorType] | None = ...,
facecolors: ColorType | Sequence[ColorType] | None = ...,
+ hatchcolors: ColorType | Sequence[ColorType] | None = ...,
linewidths: float | Sequence[float] | None = ...,
linestyles: LineStyleType | Sequence[LineStyleType] = ...,
capstyle: CapStyleType | None = ...,
@@ -66,6 +67,10 @@ class Collection(colorizer.ColorizingArtist):
def get_facecolor(self) -> ColorType | Sequence[ColorType]: ...
def get_edgecolor(self) -> ColorType | Sequence[ColorType]: ...
def set_edgecolor(self, c: ColorType | Sequence[ColorType]) -> None: ...
+ def get_hatchcolor(self) -> ColorType | Sequence[ColorType]: ...
+ def set_hatchcolor(
+ self, c: ColorType | Sequence[ColorType] | Literal["edge"]
+ ) -> None: ...
def set_alpha(self, alpha: float | Sequence[float] | None) -> None: ...
def get_linewidth(self) -> float | Sequence[float]: ...
def get_linestyle(self) -> LineStyleType | Sequence[LineStyleType]: ...
diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py
index 97076ad09cb8..263945b050d0 100644
--- a/lib/matplotlib/legend_handler.py
+++ b/lib/matplotlib/legend_handler.py
@@ -790,12 +790,11 @@ def get_first(prop_array):
# Directly set Patch color attributes (must be RGBA tuples).
legend_handle._facecolor = first_color(orig_handle.get_facecolor())
legend_handle._edgecolor = first_color(orig_handle.get_edgecolor())
+ legend_handle._hatch_color = first_color(orig_handle.get_hatchcolor())
legend_handle._original_facecolor = orig_handle._original_facecolor
legend_handle._original_edgecolor = orig_handle._original_edgecolor
legend_handle._fill = orig_handle.get_fill()
legend_handle._hatch = orig_handle.get_hatch()
- # Hatch color is anomalous in having no getters and setters.
- legend_handle._hatch_color = orig_handle._hatch_color
# Setters are fine for the remaining attributes.
legend_handle.set_linewidth(get_first(orig_handle.get_linewidths()))
legend_handle.set_linestyle(get_first(orig_handle.get_linestyles()))
diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py
index db9c4be81a44..17d935536cfd 100644
--- a/lib/matplotlib/tests/test_collections.py
+++ b/lib/matplotlib/tests/test_collections.py
@@ -1384,3 +1384,72 @@ def test_hatch_linewidth(fig_test, fig_ref):
ax_test.add_collection(test)
assert test.get_hatch_linewidth() == ref.get_hatch_linewidth() == lw
+
+
+def test_collection_hatchcolor_inherit_logic():
+ from matplotlib.collections import PathCollection
+ path = mpath.Path.unit_rectangle()
+
+ colors_1 = ['purple', 'red', 'green', 'yellow']
+ colors_2 = ['orange', 'cyan', 'blue', 'magenta']
+ with mpl.rc_context({'hatch.color': 'edge'}):
+ # edgecolor and hatchcolor is set
+ col = PathCollection([path], hatch='//',
+ edgecolor=colors_1, hatchcolor=colors_2)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_2))
+
+ # explicitly setting edgecolor and then hatchcolor
+ col = PathCollection([path], hatch='//')
+ col.set_edgecolor(colors_1)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1))
+ col.set_hatchcolor(colors_2)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_2))
+
+ # explicitly setting hatchcolor and then edgecolor
+ col = PathCollection([path], hatch='//')
+ col.set_hatchcolor(colors_1)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1))
+ col.set_edgecolor(colors_2)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1))
+
+
+def test_collection_hatchcolor_fallback_logic():
+ from matplotlib.collections import PathCollection
+ path = mpath.Path.unit_rectangle()
+
+ colors_1 = ['purple', 'red', 'green', 'yellow']
+ colors_2 = ['orange', 'cyan', 'blue', 'magenta']
+
+ # hatchcolor parameter should take precedence over rcParam
+ # When edgecolor is not set
+ with mpl.rc_context({'hatch.color': 'green'}):
+ col = PathCollection([path], hatch='//', hatchcolor=colors_1)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1))
+ # When edgecolor is set
+ with mpl.rc_context({'hatch.color': 'green'}):
+ col = PathCollection([path], hatch='//',
+ edgecolor=colors_2, hatchcolor=colors_1)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1))
+
+ # hatchcolor should not be overridden by edgecolor when
+ # hatchcolor parameter is not passed and hatch.color rcParam is set to a color
+ with mpl.rc_context({'hatch.color': 'green'}):
+ col = PathCollection([path], hatch='//')
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array('green'))
+ col.set_edgecolor(colors_1)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array('green'))
+
+ # hatchcolor should match edgecolor when
+ # hatchcolor parameter is not passed and hatch.color rcParam is set to 'edge'
+ with mpl.rc_context({'hatch.color': 'edge'}):
+ col = PathCollection([path], hatch='//', edgecolor=colors_1)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1))
+ # hatchcolor parameter is set to 'edge'
+ col = PathCollection([path], hatch='//', edgecolor=colors_1, hatchcolor='edge')
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1))
+
+ # default hatchcolor should be used when hatchcolor parameter is not passed and
+ # hatch.color rcParam is set to 'edge' and edgecolor is not set
+ col = PathCollection([path], hatch='//')
+ assert_array_equal(col.get_hatchcolor(),
+ mpl.colors.to_rgba_array(mpl.rcParams['patch.edgecolor']))
diff --git a/src/_backend_agg.h b/src/_backend_agg.h
index f1fbf11ea4e5..0e33d38dfd93 100644
--- a/src/_backend_agg.h
+++ b/src/_backend_agg.h
@@ -177,7 +177,8 @@ class RendererAgg
ColorArray &edgecolors,
LineWidthArray &linewidths,
DashesVector &linestyles,
- AntialiasedArray &antialiaseds);
+ AntialiasedArray &antialiaseds,
+ ColorArray &hatchcolors);
template
void draw_quad_mesh(GCAgg &gc,
@@ -189,7 +190,8 @@ class RendererAgg
agg::trans_affine &offset_trans,
ColorArray &facecolors,
bool antialiased,
- ColorArray &edgecolors);
+ ColorArray &edgecolors,
+ ColorArray &hatchcolors);
template
void draw_gouraud_triangles(GCAgg &gc,
@@ -272,7 +274,8 @@ class RendererAgg
DashesVector &linestyles,
AntialiasedArray &antialiaseds,
bool check_snap,
- bool has_codes);
+ bool has_codes,
+ ColorArray &hatchcolors);
template
void _draw_gouraud_triangle(PointArray &points,
@@ -917,7 +920,8 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc,
DashesVector &linestyles,
AntialiasedArray &antialiaseds,
bool check_snap,
- bool has_codes)
+ bool has_codes,
+ ColorArray &hatchcolors)
{
typedef agg::conv_transform transformed_path_t;
typedef PathNanRemover nan_removed_t;
@@ -937,11 +941,12 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc,
size_t Ntransforms = safe_first_shape(transforms);
size_t Nfacecolors = safe_first_shape(facecolors);
size_t Nedgecolors = safe_first_shape(edgecolors);
+ size_t Nhatchcolors = safe_first_shape(hatchcolors);
size_t Nlinewidths = safe_first_shape(linewidths);
size_t Nlinestyles = std::min(linestyles.size(), N);
size_t Naa = safe_first_shape(antialiaseds);
- if ((Nfacecolors == 0 && Nedgecolors == 0) || Npaths == 0) {
+ if ((Nfacecolors == 0 && Nedgecolors == 0 && Nhatchcolors == 0) || Npaths == 0) {
return;
}
@@ -1004,6 +1009,11 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc,
}
}
+ if(Nhatchcolors) {
+ int ic = i % Nhatchcolors;
+ gc.hatch_color = agg::rgba(hatchcolors(ic, 0), hatchcolors(ic, 1), hatchcolors(ic, 2), hatchcolors(ic, 3));
+ }
+
gc.isaa = antialiaseds(i % Naa);
transformed_path_t tpath(path, trans);
nan_removed_t nan_removed(tpath, true, has_codes);
@@ -1048,7 +1058,8 @@ inline void RendererAgg::draw_path_collection(GCAgg &gc,
ColorArray &edgecolors,
LineWidthArray &linewidths,
DashesVector &linestyles,
- AntialiasedArray &antialiaseds)
+ AntialiasedArray &antialiaseds,
+ ColorArray &hatchcolors)
{
_draw_path_collection_generic(gc,
master_transform,
@@ -1065,7 +1076,8 @@ inline void RendererAgg::draw_path_collection(GCAgg &gc,
linestyles,
antialiaseds,
true,
- true);
+ true,
+ hatchcolors);
}
template
@@ -1151,7 +1163,8 @@ inline void RendererAgg::draw_quad_mesh(GCAgg &gc,
agg::trans_affine &offset_trans,
ColorArray &facecolors,
bool antialiased,
- ColorArray &edgecolors)
+ ColorArray &edgecolors,
+ ColorArray &hatchcolors)
{
QuadMeshGenerator path_generator(mesh_width, mesh_height, coordinates);
@@ -1175,7 +1188,8 @@ inline void RendererAgg::draw_quad_mesh(GCAgg &gc,
linestyles,
antialiaseds,
true, // check_snap
- false);
+ false,
+ hatchcolors);
}
template
diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp
index 269e2aaa9ee5..39d8009748d3 100644
--- a/src/_backend_agg_wrapper.cpp
+++ b/src/_backend_agg_wrapper.cpp
@@ -146,12 +146,14 @@ PyRendererAgg_draw_path_collection(RendererAgg *self,
py::array_t antialiaseds_obj,
py::object Py_UNUSED(ignored_obj),
// offset position is no longer used
- py::object Py_UNUSED(offset_position_obj))
+ py::object Py_UNUSED(offset_position_obj),
+ py::array_t hatchcolors_obj)
{
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 hatchcolors = convert_colors(hatchcolors_obj);
auto linewidths = linewidths_obj.unchecked<1>();
auto antialiaseds = antialiaseds_obj.unchecked<1>();
@@ -165,7 +167,8 @@ PyRendererAgg_draw_path_collection(RendererAgg *self,
edgecolors,
linewidths,
dashes,
- antialiaseds);
+ antialiaseds,
+ hatchcolors);
}
static void
@@ -179,12 +182,14 @@ PyRendererAgg_draw_quad_mesh(RendererAgg *self,
agg::trans_affine offset_trans,
py::array_t facecolors_obj,
bool antialiased,
- py::array_t edgecolors_obj)
+ py::array_t edgecolors_obj,
+ py::array_t hatchcolors_obj)
{
auto coordinates = coordinates_obj.mutable_unchecked<3>();
auto offsets = convert_points(offsets_obj);
auto facecolors = convert_colors(facecolors_obj);
auto edgecolors = convert_colors(edgecolors_obj);
+ auto hatchcolors = convert_colors(hatchcolors_obj);
self->draw_quad_mesh(gc,
master_transform,
@@ -195,7 +200,8 @@ PyRendererAgg_draw_quad_mesh(RendererAgg *self,
offset_trans,
facecolors,
antialiased,
- edgecolors);
+ edgecolors,
+ hatchcolors);
}
static void
@@ -229,11 +235,12 @@ PYBIND11_MODULE(_backend_agg, m, py::mod_gil_not_used())
.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)
+ "dashes"_a, "antialiaseds"_a, "ignored"_a, "offset_position"_a,
+ "hatchcolors"_a = nullptr)
.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)
+ "antialiased"_a, "edgecolors"_a, "hatchcolors"_a = nullptr)
.def("draw_gouraud_triangles", &PyRendererAgg_draw_gouraud_triangles,
"gc"_a, "points"_a, "colors"_a, "trans"_a = nullptr)
From 2fd17a09b16a58188efc692e4fbc7a13cb6c4ac5 Mon Sep 17 00:00:00 2001
From: anTon <138380708+r3kste@users.noreply.github.com>
Date: Fri, 3 Jan 2025 19:52:13 +0530
Subject: [PATCH 017/627] minor fixes
---
lib/matplotlib/collections.py | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py
index a29ec87f07fd..2ba655aa99bb 100644
--- a/lib/matplotlib/collections.py
+++ b/lib/matplotlib/collections.py
@@ -105,6 +105,10 @@ def __init__(self, *,
facecolor.
facecolors : :mpltype:`color` or list of colors, default: :rc:`patch.facecolor`
Face color for each patch making up the collection.
+ hatchcolors : :mpltype:`color` or list of colors, default: :rc:`hatch.color`
+ Hatch color for each patch making up the collection. The special
+ value 'edge' can be passed to make the hatchcolor match the
+ edgecolor.
linewidths : float or list of floats, default: :rc:`patch.linewidth`
Line width for each patch making up the collection.
linestyles : str or tuple or list thereof, default: 'solid'
@@ -754,7 +758,7 @@ def _get_default_antialiased(self):
def set_color(self, c):
"""
- Set both the edgecolor and the facecolor.
+ Sets the edgecolor, facecolor and hatchcolor.
Parameters
----------
@@ -767,6 +771,7 @@ def set_color(self, c):
"""
self.set_facecolor(c)
self.set_edgecolor(c)
+ self.set_hatchcolor(c)
def _get_default_facecolor(self):
# This may be overridden in a subclass.
@@ -811,7 +816,7 @@ def _get_default_edgecolor(self):
def get_hatchcolor(self):
if cbook._str_equal(self._hatchcolors, 'edge'):
- if self.get_edgecolor().size == 0:
+ if len(self.get_edgecolor()) == 0:
return mpl.colors.to_rgba_array(self._get_default_edgecolor(),
self._alpha)
return self.get_edgecolor()
@@ -852,7 +857,7 @@ def set_edgecolor(self, c):
def _set_hatchcolor(self, c):
c = mpl._val_or_rc(c, 'hatch.color')
- if c == 'edge':
+ if cbook._str_equal(c, 'edge'):
self._hatchcolors = 'edge'
else:
self._hatchcolors = mcolors.to_rgba_array(c, self._alpha)
@@ -868,8 +873,6 @@ def set_hatchcolor(self, c):
The collection hatchcolor(s). If a sequence, the patches cycle
through it.
"""
- if cbook._str_equal(c, 'edge'):
- c = 'edge'
self._original_hatchcolor = c
self._set_hatchcolor(c)
@@ -888,6 +891,7 @@ def set_alpha(self, alpha):
artist.Artist._set_alpha_for_array(self, alpha)
self._set_facecolor(self._original_facecolor)
self._set_edgecolor(self._original_edgecolor)
+ self._set_hatchcolor(self._original_hatchcolor)
set_alpha.__doc__ = artist.Artist._set_alpha_for_array.__doc__
From f5978ceb30016af0feb46a0860958850501e9579 Mon Sep 17 00:00:00 2001
From: anTon <138380708+r3kste@users.noreply.github.com>
Date: Wed, 8 Jan 2025 18:47:41 +0530
Subject: [PATCH 018/627] added gallery example
---
.../shapes_and_collections/hatchcolor_demo.py | 55 ++++++++++++++++++-
1 file changed, 52 insertions(+), 3 deletions(-)
diff --git a/galleries/examples/shapes_and_collections/hatchcolor_demo.py b/galleries/examples/shapes_and_collections/hatchcolor_demo.py
index 7125ddb57fe7..0b50b5ad825e 100644
--- a/galleries/examples/shapes_and_collections/hatchcolor_demo.py
+++ b/galleries/examples/shapes_and_collections/hatchcolor_demo.py
@@ -1,7 +1,10 @@
"""
-================
-Patch hatchcolor
-================
+===============
+Hatchcolor Demo
+===============
+
+Patch Hatchcolor
+----------------
This example shows how to use the *hatchcolor* parameter to set the color of
the hatch. The *hatchcolor* parameter is available for `~.patches.Patch`,
@@ -11,6 +14,7 @@
import matplotlib.pyplot as plt
import numpy as np
+import matplotlib.cm as cm
from matplotlib.patches import Rectangle
fig, (ax1, ax2) = plt.subplots(1, 2)
@@ -28,6 +32,49 @@
ax2.set_xlim(0, 5)
ax2.set_ylim(0, 5)
+# %%
+# Collection Hatchcolor
+# ---------------------
+#
+# The following example shows how to use the *hatchcolor* parameter to set the color of
+# the hatch in a scatter plot. The *hatchcolor* parameter can also be passed to
+# `~.collections.Collection`, child classes of Collection, and methods that pass
+# through to Collection.
+
+fig, ax = plt.subplots()
+
+num_points_x = 10
+num_points_y = 9
+x = np.linspace(0, 1, num_points_x)
+y = np.linspace(0, 1, num_points_y)
+
+X, Y = np.meshgrid(x, y)
+X[1::2, :] += (x[1] - x[0]) / 2 # stagger every alternate row
+
+# As ax.scatter (PathCollection) is drawn row by row, setting hatchcolors to the
+# first row is enough, as the colors will be cycled through for the next rows.
+colors = [cm.rainbow(val) for val in x]
+
+ax.scatter(
+ X.ravel(),
+ Y.ravel(),
+ s=1700,
+ facecolor="none",
+ edgecolor="gray",
+ linewidth=2,
+ marker="h", # Use hexagon as marker
+ hatch="xxx",
+ hatchcolor=colors,
+)
+ax.set_xlim(0, 1)
+ax.set_ylim(0, 1)
+
+# Remove ticks and labels
+ax.set_xticks([])
+ax.set_yticks([])
+ax.set_xticklabels([])
+ax.set_yticklabels([])
+
plt.show()
# %%
@@ -41,3 +88,5 @@
# - `matplotlib.patches.Polygon`
# - `matplotlib.axes.Axes.add_patch`
# - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar`
+# - `matplotlib.collections`
+# - `matplotlib.axes.Axes.scatter` / `matplotlib.pyplot.scatter`
From 5377127fd920130f5969ed4f1c9a063a60fc68d1 Mon Sep 17 00:00:00 2001
From: anTon <138380708+r3kste@users.noreply.github.com>
Date: Mon, 20 Jan 2025 22:50:16 +0530
Subject: [PATCH 019/627] documented hatchcolor parameter for collections in
next whats new entry
---
.../next_whats_new/separated_hatchcolor.rst | 27 +++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/doc/users/next_whats_new/separated_hatchcolor.rst b/doc/users/next_whats_new/separated_hatchcolor.rst
index f3932cf876f8..b8c3f7d36aa8 100644
--- a/doc/users/next_whats_new/separated_hatchcolor.rst
+++ b/doc/users/next_whats_new/separated_hatchcolor.rst
@@ -57,3 +57,30 @@ Previously, hatch colors were the same as edge colors, with a fallback to
xy=(.5, 1.03), xycoords=patch4, ha='center', va='bottom')
plt.show()
+
+For collections, a sequence of colors can be passed to the *hatchcolor* parameter
+which will be cycled through for each hatch, similar to *facecolor* and *edgecolor*.
+
+.. plot::
+ :include-source: true
+ :alt: A scatter plot of a quadratic function with hatches on the markers. The hatches are colored in blue, orange, and green, respectively. After the first three markers, the colors are cycled through again.
+
+ import matplotlib.pyplot as plt
+ import numpy as np
+
+ fig, ax = plt.subplots()
+
+ x = np.linspace(0, 1, 10)
+ y = x**2
+ colors = ["blue", "orange", "green"]
+
+ ax.scatter(
+ x,
+ y,
+ s=800,
+ hatch="xxxx",
+ hatchcolor=colors,
+ facecolor="none",
+ edgecolor="black",
+ )
+ plt.show()
From 6f18c499c1288d18200779229b1a2681efd11e52 Mon Sep 17 00:00:00 2001
From: r3kste <138380708+r3kste@users.noreply.github.com>
Date: Mon, 27 Jan 2025 20:48:11 +0530
Subject: [PATCH 020/627] update whats new for hatchcolor in collections
---
doc/users/next_whats_new/separated_hatchcolor.rst | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/doc/users/next_whats_new/separated_hatchcolor.rst b/doc/users/next_whats_new/separated_hatchcolor.rst
index b8c3f7d36aa8..4652458083e3 100644
--- a/doc/users/next_whats_new/separated_hatchcolor.rst
+++ b/doc/users/next_whats_new/separated_hatchcolor.rst
@@ -61,17 +61,25 @@ Previously, hatch colors were the same as edge colors, with a fallback to
For collections, a sequence of colors can be passed to the *hatchcolor* parameter
which will be cycled through for each hatch, similar to *facecolor* and *edgecolor*.
+Previously, if *edgecolor* was not specified, the hatch color would fall back to
+:rc:`patch.edgecolor`, but the alpha value would default to **1.0**, regardless of the
+alpha value of the collection. This behavior has been changed such that, if both
+*hatchcolor* and *edgecolor* are not specified, the hatch color will fall back
+to 'patch.edgecolor' with the alpha value of the collection.
+
.. plot::
:include-source: true
- :alt: A scatter plot of a quadratic function with hatches on the markers. The hatches are colored in blue, orange, and green, respectively. After the first three markers, the colors are cycled through again.
+ :alt: A random scatter plot with hatches on the markers. The hatches are colored in blue, orange, and green, respectively. After the first three markers, the colors are cycled through again.
import matplotlib.pyplot as plt
import numpy as np
+ np.random.seed(19680801)
+
fig, ax = plt.subplots()
- x = np.linspace(0, 1, 10)
- y = x**2
+ x = np.random.rand(20)
+ y = np.random.rand(20)
colors = ["blue", "orange", "green"]
ax.scatter(
@@ -83,4 +91,5 @@ which will be cycled through for each hatch, similar to *facecolor* and *edgecol
facecolor="none",
edgecolor="black",
)
+
plt.show()
From 8a7cd656ef1ce15678b5b5f3cf3e4e0c3d9e710e Mon Sep 17 00:00:00 2001
From: r3kste <138380708+r3kste@users.noreply.github.com>
Date: Wed, 29 Jan 2025 22:07:06 +0530
Subject: [PATCH 021/627] update contourf hatch test with cmap.with_alpha()
---
lib/matplotlib/tests/test_axes.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py
index 10b6af8d5883..9dd50e436ea3 100644
--- a/lib/matplotlib/tests/test_axes.py
+++ b/lib/matplotlib/tests/test_axes.py
@@ -2649,8 +2649,8 @@ def test_contour_hatching():
x, y, z = contour_dat()
fig, ax = plt.subplots()
ax.contourf(x, y, z, 7, hatches=['/', '\\', '//', '-'],
- cmap=mpl.colormaps['gray'],
- extend='both', alpha=0.5)
+ cmap=mpl.colormaps['gray'].with_alpha(0.5),
+ extend='both')
@image_comparison(['contour_colorbar'], style='mpl20',
From e3b03fcb5b068e7f2e9442106d5d0494606bab42 Mon Sep 17 00:00:00 2001
From: Pranav
Date: Wed, 5 Feb 2025 16:05:20 +0530
Subject: [PATCH 022/627] grammar nits
---
lib/matplotlib/backend_bases.py | 3 +++
lib/matplotlib/collections.py | 6 +++---
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py
index 494f40b1ff06..68bc0441950f 100644
--- a/lib/matplotlib/backend_bases.py
+++ b/lib/matplotlib/backend_bases.py
@@ -220,6 +220,9 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
*facecolors*, *edgecolors*, *linewidths*, *linestyles*, *antialiased*
and *hatchcolors* are lists that set the corresponding properties.
+ .. versionadded:: 3.11
+ Allowing *hatchcolors* to be specified.
+
*offset_position* is unused now, but the argument is kept for
backwards compatibility.
diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py
index 2ba655aa99bb..46c288be9b58 100644
--- a/lib/matplotlib/collections.py
+++ b/lib/matplotlib/collections.py
@@ -106,8 +106,8 @@ def __init__(self, *,
facecolors : :mpltype:`color` or list of colors, default: :rc:`patch.facecolor`
Face color for each patch making up the collection.
hatchcolors : :mpltype:`color` or list of colors, default: :rc:`hatch.color`
- Hatch color for each patch making up the collection. The special
- value 'edge' can be passed to make the hatchcolor match the
+ Hatch color for each patch making up the collection. The color
+ can be set to the special value 'edge' to make the hatchcolor match the
edgecolor.
linewidths : float or list of floats, default: :rc:`patch.linewidth`
Line width for each patch making up the collection.
@@ -758,7 +758,7 @@ def _get_default_antialiased(self):
def set_color(self, c):
"""
- Sets the edgecolor, facecolor and hatchcolor.
+ Set the edgecolor, facecolor and hatchcolor.
Parameters
----------
From 7668535a90725522e33763d812fa17b541d23750 Mon Sep 17 00:00:00 2001
From: r3kste <138380708+r3kste@users.noreply.github.com>
Date: Wed, 5 Feb 2025 21:45:56 +0530
Subject: [PATCH 023/627] enhanced tests and made hatchcolors param required in
_iter_collection()
---
lib/matplotlib/backend_bases.py | 5 +--
lib/matplotlib/tests/test_backend_bases.py | 2 +-
lib/matplotlib/tests/test_collections.py | 46 +++++++++++-----------
3 files changed, 25 insertions(+), 28 deletions(-)
diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py
index 68bc0441950f..995278019868 100644
--- a/lib/matplotlib/backend_bases.py
+++ b/lib/matplotlib/backend_bases.py
@@ -345,7 +345,7 @@ def _iter_collection_uses_per_path(self, paths, all_transforms,
def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors,
edgecolors, linewidths, linestyles,
- antialiaseds, urls, offset_position, hatchcolors=None):
+ antialiaseds, urls, offset_position, hatchcolors):
"""
Helper method (along with `_iter_collection_raw_paths`) to implement
`draw_path_collection` in a memory-efficient manner.
@@ -368,9 +368,6 @@ def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors,
*path_ids*; *gc* is a graphics context and *rgbFace* is a color to
use for filling the path.
"""
- if hatchcolors is None:
- hatchcolors = []
-
Npaths = len(path_ids)
Noffsets = len(offsets)
N = max(Npaths, Noffsets)
diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py
index ef5a52d988bb..b1ac109a913d 100644
--- a/lib/matplotlib/tests/test_backend_bases.py
+++ b/lib/matplotlib/tests/test_backend_bases.py
@@ -38,7 +38,7 @@ def check(master_transform, paths, all_transforms,
gc, range(len(raw_paths)), offsets,
transforms.AffineDeltaTransform(master_transform),
facecolors, edgecolors, [], [], [False],
- [], 'screen')]
+ [], 'screen', [])]
uses = rb._iter_collection_uses_per_path(
paths, all_transforms, offsets, facecolors, edgecolors)
if raw_paths:
diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py
index 17d935536cfd..dc6e4e32b342 100644
--- a/lib/matplotlib/tests/test_collections.py
+++ b/lib/matplotlib/tests/test_collections.py
@@ -1390,63 +1390,63 @@ def test_collection_hatchcolor_inherit_logic():
from matplotlib.collections import PathCollection
path = mpath.Path.unit_rectangle()
- colors_1 = ['purple', 'red', 'green', 'yellow']
- colors_2 = ['orange', 'cyan', 'blue', 'magenta']
+ edgecolors = ['purple', 'red', 'green', 'yellow']
+ hatchcolors = ['orange', 'cyan', 'blue', 'magenta']
with mpl.rc_context({'hatch.color': 'edge'}):
# edgecolor and hatchcolor is set
col = PathCollection([path], hatch='//',
- edgecolor=colors_1, hatchcolor=colors_2)
- assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_2))
+ edgecolor=edgecolors, hatchcolor=hatchcolors)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(hatchcolors))
# explicitly setting edgecolor and then hatchcolor
col = PathCollection([path], hatch='//')
- col.set_edgecolor(colors_1)
- assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1))
- col.set_hatchcolor(colors_2)
- assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_2))
+ col.set_edgecolor(edgecolors)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(edgecolors))
+ col.set_hatchcolor(hatchcolors)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(hatchcolors))
# explicitly setting hatchcolor and then edgecolor
col = PathCollection([path], hatch='//')
- col.set_hatchcolor(colors_1)
- assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1))
- col.set_edgecolor(colors_2)
- assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1))
+ col.set_hatchcolor(hatchcolors)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(hatchcolors))
+ col.set_edgecolor(edgecolors)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(hatchcolors))
def test_collection_hatchcolor_fallback_logic():
from matplotlib.collections import PathCollection
path = mpath.Path.unit_rectangle()
- colors_1 = ['purple', 'red', 'green', 'yellow']
- colors_2 = ['orange', 'cyan', 'blue', 'magenta']
+ edgecolors = ['purple', 'red', 'green', 'yellow']
+ hatchcolors = ['orange', 'cyan', 'blue', 'magenta']
# hatchcolor parameter should take precedence over rcParam
# When edgecolor is not set
with mpl.rc_context({'hatch.color': 'green'}):
- col = PathCollection([path], hatch='//', hatchcolor=colors_1)
- assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1))
+ col = PathCollection([path], hatch='//', hatchcolor=hatchcolors)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(hatchcolors))
# When edgecolor is set
with mpl.rc_context({'hatch.color': 'green'}):
col = PathCollection([path], hatch='//',
- edgecolor=colors_2, hatchcolor=colors_1)
- assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1))
+ edgecolor=edgecolors, hatchcolor=hatchcolors)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(hatchcolors))
# hatchcolor should not be overridden by edgecolor when
# hatchcolor parameter is not passed and hatch.color rcParam is set to a color
with mpl.rc_context({'hatch.color': 'green'}):
col = PathCollection([path], hatch='//')
assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array('green'))
- col.set_edgecolor(colors_1)
+ col.set_edgecolor(edgecolors)
assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array('green'))
# hatchcolor should match edgecolor when
# hatchcolor parameter is not passed and hatch.color rcParam is set to 'edge'
with mpl.rc_context({'hatch.color': 'edge'}):
- col = PathCollection([path], hatch='//', edgecolor=colors_1)
- assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1))
+ col = PathCollection([path], hatch='//', edgecolor=edgecolors)
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(edgecolors))
# hatchcolor parameter is set to 'edge'
- col = PathCollection([path], hatch='//', edgecolor=colors_1, hatchcolor='edge')
- assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1))
+ col = PathCollection([path], hatch='//', edgecolor=edgecolors, hatchcolor='edge')
+ assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(edgecolors))
# default hatchcolor should be used when hatchcolor parameter is not passed and
# hatch.color rcParam is set to 'edge' and edgecolor is not set
From 9d0ec232bfea76929351a7749ef7aa023b6bfc3a Mon Sep 17 00:00:00 2001
From: r3kste <138380708+r3kste@users.noreply.github.com>
Date: Sat, 8 Feb 2025 08:37:40 +0530
Subject: [PATCH 024/627] smoke test for hatchcolors coercion
---
lib/matplotlib/tests/test_collections.py | 33 ++++++++++++++++++++++++
src/_backend_agg_wrapper.cpp | 4 +--
2 files changed, 35 insertions(+), 2 deletions(-)
diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py
index dc6e4e32b342..cb29508ec010 100644
--- a/lib/matplotlib/tests/test_collections.py
+++ b/lib/matplotlib/tests/test_collections.py
@@ -1453,3 +1453,36 @@ def test_collection_hatchcolor_fallback_logic():
col = PathCollection([path], hatch='//')
assert_array_equal(col.get_hatchcolor(),
mpl.colors.to_rgba_array(mpl.rcParams['patch.edgecolor']))
+
+
+@pytest.mark.parametrize('backend', ['agg', 'pdf', 'svg', 'ps'])
+def test_draw_path_collection_no_hatchcolor(backend):
+ from matplotlib.collections import PathCollection
+ path = mpath.Path.unit_rectangle()
+
+ plt.switch_backend(backend)
+ fig, ax = plt.subplots()
+ renderer = fig._get_renderer()
+
+ col = PathCollection([path], hatch='//')
+ ax.add_collection(col)
+
+ gc = renderer.new_gc()
+ transform = mtransforms.IdentityTransform()
+ paths = col.get_paths()
+ transforms = col.get_transforms()
+ offsets = col.get_offsets()
+ offset_trf = col.get_offset_transform()
+ facecolors = col.get_facecolor()
+ edgecolors = col.get_edgecolor()
+ linewidths = col.get_linewidth()
+ linestyles = col.get_linestyle()
+ antialiaseds = col.get_antialiased()
+ urls = col.get_urls()
+ offset_position = "screen"
+
+ renderer.draw_path_collection(
+ gc, transform, paths, transforms, offsets, offset_trf,
+ facecolors, edgecolors, linewidths, linestyles,
+ antialiaseds, urls, offset_position
+ )
diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp
index 39d8009748d3..f0c029b96b98 100644
--- a/src/_backend_agg_wrapper.cpp
+++ b/src/_backend_agg_wrapper.cpp
@@ -236,11 +236,11 @@ PYBIND11_MODULE(_backend_agg, m, py::mod_gil_not_used())
"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,
- "hatchcolors"_a = nullptr)
+ "hatchcolors"_a = py::array_t().reshape({0, 4}))
.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, "hatchcolors"_a = nullptr)
+ "antialiased"_a, "edgecolors"_a, "hatchcolors"_a = py::array_t().reshape({0, 4}))
.def("draw_gouraud_triangles", &PyRendererAgg_draw_gouraud_triangles,
"gc"_a, "points"_a, "colors"_a, "trans"_a = nullptr)
From 5e6fca88ac6d9d79e75813d7cf0e99725850ef58 Mon Sep 17 00:00:00 2001
From: marbled-toast <69227427+marbled-toast@users.noreply.github.com>
Date: Tue, 11 Feb 2025 17:56:51 +0000
Subject: [PATCH 025/627] add detail to doc string in Line3DCollection
---
lib/mpl_toolkits/mplot3d/art3d.py | 32 +++++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py
index 2571e6447f5c..ac78ff8b754f 100644
--- a/lib/mpl_toolkits/mplot3d/art3d.py
+++ b/lib/mpl_toolkits/mplot3d/art3d.py
@@ -444,6 +444,38 @@ class Line3DCollection(LineCollection):
def __init__(self, lines, axlim_clip=False, **kwargs):
super().__init__(lines, **kwargs)
self._axlim_clip = axlim_clip
+ """
+ Parameters
+ ----------
+ segments : list of (N, 3) array-like
+ A sequence ``[line0, line1, ...]`` where each line is a (N, 3)-shape
+ array-like containing points::
+
+ line0 = [(x0, y0, z0), (x1, y1, z1), ...]
+
+ Each line can contain a different number of points.
+ linewidths : float or list of float, default: :rc:`lines.linewidth`
+ The width of each line in points.
+ colors : :mpltype:`color` or list of color, default: :rc:`lines.color`
+ A sequence of RGBA tuples (e.g., arbitrary color strings, etc, not
+ allowed).
+ antialiaseds : bool or list of bool, default: :rc:`lines.antialiased`
+ Whether to use antialiasing for each line.
+ zorder : float, default: 2
+ zorder of the lines once drawn.
+
+ facecolors : :mpltype:`color` or list of :mpltype:`color`, default: 'none'
+ When setting *facecolors*, each line is interpreted as a boundary
+ for an area, implicitly closing the path from the last point to the
+ first point. The enclosed area is filled with *facecolor*.
+ In order to manually specify what should count as the "interior" of
+ each line, please use `.PathCollection` instead, where the
+ "interior" can be specified by appropriate usage of
+ `~.path.Path.CLOSEPOLY`.
+
+ **kwargs
+ Forwarded to `.Collection`.
+ """
def set_sort_zpos(self, val):
"""Set the position to use for z-sorting."""
From 7e46707c29c2c9b9918fab25acb89d0566f89162 Mon Sep 17 00:00:00 2001
From: r3kste <138380708+r3kste@users.noreply.github.com>
Date: Sun, 16 Feb 2025 15:00:10 +0530
Subject: [PATCH 026/627] pass hatchcolor arg only if it is supported by the
renderer. and made suggested changes.
---
lib/matplotlib/backends/backend_pdf.py | 5 ++-
lib/matplotlib/collections.py | 47 +++++++++++++++++---------
2 files changed, 35 insertions(+), 17 deletions(-)
diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py
index 0607b5455414..a370330d43eb 100644
--- a/lib/matplotlib/backends/backend_pdf.py
+++ b/lib/matplotlib/backends/backend_pdf.py
@@ -2606,7 +2606,10 @@ def delta(self, other):
different = ours is not theirs
else:
different = bool(ours != theirs)
- except ValueError:
+ except (ValueError, DeprecationWarning):
+ # numpy version < 1.25 raises DeprecationWarning when array shapes
+ # mismatch, unlike numpy >= 1.25 which raises ValueError.
+ # This should be removed when numpy < 1.25 is no longer supported.
ours = np.asarray(ours)
theirs = np.asarray(theirs)
different = (ours.shape != theirs.shape or
diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py
index 46c288be9b58..9deba8b741a6 100644
--- a/lib/matplotlib/collections.py
+++ b/lib/matplotlib/collections.py
@@ -420,25 +420,40 @@ def draw(self, renderer):
gc, paths[0], combined_transform.frozen(),
mpath.Path(offsets), offset_trf, tuple(facecolors[0]))
else:
- if self._gapcolor is not None:
- # First draw paths within the gaps.
- ipaths, ilinestyles = self._get_inverse_paths_linestyles()
+ # Find whether renderer.draw_path_collection() takes hatchcolor parameter
+ hatchcolors_arg_supported = True
+ try:
renderer.draw_path_collection(
- gc, transform.frozen(), ipaths,
+ gc, transform.frozen(), [],
self.get_transforms(), offsets, offset_trf,
- [mcolors.to_rgba("none")], self._gapcolor,
- self._linewidths, ilinestyles,
+ self.get_facecolor(), self.get_edgecolor(),
+ self._linewidths, self._linestyles,
self._antialiaseds, self._urls,
- "screen", self.get_hatchcolor())
-
- renderer.draw_path_collection(
- gc, transform.frozen(), paths,
- self.get_transforms(), offsets, offset_trf,
- self.get_facecolor(), self.get_edgecolor(),
- self._linewidths, self._linestyles,
- self._antialiaseds, self._urls,
- "screen", # offset_position, kept for backcompat.
- self.get_hatchcolor())
+ "screen", self.get_hatchcolor()
+ )
+ except TypeError:
+ hatchcolors_arg_supported = False
+
+ if self._gapcolor is not None:
+ # First draw paths within the gaps.
+ ipaths, ilinestyles = self._get_inverse_paths_linestyles()
+ args = [gc, transform.frozen(), ipaths, self.get_transforms(),
+ offsets, offset_trf, [mcolors.to_rgba("none")],
+ self._gapcolor, self._linewidths, ilinestyles,
+ self._antialiaseds, self._urls, "screen"]
+ if hatchcolors_arg_supported:
+ args.append(self.get_hatchcolor())
+
+ renderer.draw_path_collection(*args)
+
+ args = [gc, transform.frozen(), paths, self.get_transforms(),
+ offsets, offset_trf, self.get_facecolor(),
+ self.get_edgecolor(), self._linewidths, self._linestyles,
+ self._antialiaseds, self._urls, "screen"]
+ if hatchcolors_arg_supported:
+ args.append(self.get_hatchcolor())
+
+ renderer.draw_path_collection(*args)
gc.restore()
renderer.close_group(self.__class__.__name__)
From c5e9583a5e78bb1b1af1fefffc33c13a4831aa59 Mon Sep 17 00:00:00 2001
From: r3kste <138380708+r3kste@users.noreply.github.com>
Date: Sat, 22 Feb 2025 16:18:10 +0530
Subject: [PATCH 027/627] Fix hatchcolors argument support in third-party
backends
---
lib/matplotlib/collections.py | 50 +++++++++++++++++++++++++----------
1 file changed, 36 insertions(+), 14 deletions(-)
diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py
index 9deba8b741a6..ff3fe4427f15 100644
--- a/lib/matplotlib/collections.py
+++ b/lib/matplotlib/collections.py
@@ -437,23 +437,45 @@ def draw(self, renderer):
if self._gapcolor is not None:
# First draw paths within the gaps.
ipaths, ilinestyles = self._get_inverse_paths_linestyles()
- args = [gc, transform.frozen(), ipaths, self.get_transforms(),
- offsets, offset_trf, [mcolors.to_rgba("none")],
- self._gapcolor, self._linewidths, ilinestyles,
- self._antialiaseds, self._urls, "screen"]
- if hatchcolors_arg_supported:
- args.append(self.get_hatchcolor())
+ args = [offsets, offset_trf, [mcolors.to_rgba("none")], self._gapcolor,
+ self._linewidths, ilinestyles, self._antialiaseds, self._urls,
+ "screen", self.get_hatchcolor()]
- renderer.draw_path_collection(*args)
+ if hatchcolors_arg_supported:
+ renderer.draw_path_collection(gc, transform.frozen(), ipaths,
+ self.get_transforms(), *args)
+ else:
+ # If the renderer does not support the hatchcolors argument,
+ # iterate over the paths and draw them one by one.
+ path_ids = renderer._iter_collection_raw_paths(
+ transform.frozen(), ipaths, self.get_transforms())
+ for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection(
+ gc, list(path_ids), *args
+ ):
+ path, transform = path_id
+ if xo != 0 or yo != 0:
+ transform = transform.frozen()
+ transform.translate(xo, yo)
+ renderer.draw_path(gc0, path, transform, rgbFace)
+
+ args = [offsets, offset_trf, self.get_facecolor(), self.get_edgecolor(),
+ self._linewidths, self._linestyles, self._antialiaseds, self._urls,
+ "screen", self.get_hatchcolor()]
- args = [gc, transform.frozen(), paths, self.get_transforms(),
- offsets, offset_trf, self.get_facecolor(),
- self.get_edgecolor(), self._linewidths, self._linestyles,
- self._antialiaseds, self._urls, "screen"]
if hatchcolors_arg_supported:
- args.append(self.get_hatchcolor())
-
- renderer.draw_path_collection(*args)
+ renderer.draw_path_collection(gc, transform.frozen(), paths,
+ self.get_transforms(), *args)
+ else:
+ path_ids = renderer._iter_collection_raw_paths(
+ transform.frozen(), paths, self.get_transforms())
+ for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection(
+ gc, list(path_ids), *args
+ ):
+ path, transform = path_id
+ if xo != 0 or yo != 0:
+ transform = transform.frozen()
+ transform.translate(xo, yo)
+ renderer.draw_path(gc0, path, transform, rgbFace)
gc.restore()
renderer.close_group(self.__class__.__name__)
From d2b626802e9971aa1b704b61ee79f5c9a707ea09 Mon Sep 17 00:00:00 2001
From: r3kste <138380708+r3kste@users.noreply.github.com>
Date: Sun, 23 Feb 2025 12:54:03 +0530
Subject: [PATCH 028/627] Add note about provisional API for
draw_path_collection()
---
lib/matplotlib/collections.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py
index ff3fe4427f15..7306e85586ef 100644
--- a/lib/matplotlib/collections.py
+++ b/lib/matplotlib/collections.py
@@ -420,6 +420,9 @@ def draw(self, renderer):
gc, paths[0], combined_transform.frozen(),
mpath.Path(offsets), offset_trf, tuple(facecolors[0]))
else:
+ # The current new API of draw_path_collection() is provisional
+ # and will be changed in a future PR.
+
# Find whether renderer.draw_path_collection() takes hatchcolor parameter
hatchcolors_arg_supported = True
try:
From 4f87488d4fc24a7ec255c11f2c6f863b7e51d0d3 Mon Sep 17 00:00:00 2001
From: Thomas A Caswell
Date: Mon, 24 Feb 2025 14:01:50 -0500
Subject: [PATCH 029/627] DOC: document the issues with overlaying new mpl on
old mpl
closes #26827
---
.../api_changes_3.8.0/behaviour.rst | 15 +++++++++++++++
1 file changed, 15 insertions(+)
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 0b598723e26c..43200757947c 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
@@ -171,3 +171,18 @@ saved.
Previously, *mincnt* was inclusive with no *C* provided but exclusive when *C* is provided.
It is now inclusive of *mincnt* in both cases.
+
+
+``matplotlib.mpl_toolkits`` is now an implicit namespace package
+----------------------------------------------------------------
+
+Following the deprecation of ``pkg_resources.declare_namespace`` in ``setuptools`` 67.3.0,
+``matplotlib.mpl_toolkits`` is now implemented as an implicit namespace, following
+`PEP 420 `_.
+
+As a consequence using ``pip`` to install a version of Matplotlib >= 3.8 on top
+of a version of Matplotlib < 3.8 (e.g. via ``pip install --local`` or
+``python -m venv --system-site-packages ...``) will fail because the old
+`matplotlib.mpl_toolkits` files will be found where as the newer files will be
+found for all other modules. This will result in errors due to the version
+miss-match.
From 96c5403f251954233750788e9f85a8dfd091de7e Mon Sep 17 00:00:00 2001
From: Thomas A Caswell
Date: Mon, 24 Feb 2025 17:14:17 -0500
Subject: [PATCH 030/627] DOC: add note about how to avoid overlaying multiple
versions of mpl
---
doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
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 43200757947c..ccba751f975d 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
@@ -183,6 +183,10 @@ Following the deprecation of ``pkg_resources.declare_namespace`` in ``setuptools
As a consequence using ``pip`` to install a version of Matplotlib >= 3.8 on top
of a version of Matplotlib < 3.8 (e.g. via ``pip install --local`` or
``python -m venv --system-site-packages ...``) will fail because the old
-`matplotlib.mpl_toolkits` files will be found where as the newer files will be
+``matplotlib.mpl_toolkits`` files will be found where as the newer files will be
found for all other modules. This will result in errors due to the version
miss-match.
+
+To avoid this issue you need to avoid having multiple versions of Matplotlib
+in different entries of ``sys.path``. Either uninstall Matplotlib from
+at the system level or use a more isolated virtual environment.
From fc00227b2856cf1fbc148f31fbf32bf32a96595b Mon Sep 17 00:00:00 2001
From: Thomas A Caswell
Date: Mon, 24 Feb 2025 17:53:24 -0500
Subject: [PATCH 031/627] Update
doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst
Co-authored-by: Jody Klymak
---
doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 ccba751f975d..b6fbcca5735d 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
@@ -188,5 +188,5 @@ found for all other modules. This will result in errors due to the version
miss-match.
To avoid this issue you need to avoid having multiple versions of Matplotlib
-in different entries of ``sys.path``. Either uninstall Matplotlib from
+in different entries of ``sys.path``. Either uninstall Matplotlib
at the system level or use a more isolated virtual environment.
From 50cad2a347ff0beb4e2d322b6d54ff4cc650348f Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Fri, 28 Feb 2025 12:51:08 +0100
Subject: [PATCH 032/627] Backport PR #29584: DOC: Recommend constrained_layout
over tight_layout
---
galleries/users_explain/axes/tight_layout_guide.py | 10 ++++++----
lib/matplotlib/layout_engine.py | 11 ++++++++---
2 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/galleries/users_explain/axes/tight_layout_guide.py b/galleries/users_explain/axes/tight_layout_guide.py
index 455bb57de126..672704bb9726 100644
--- a/galleries/users_explain/axes/tight_layout_guide.py
+++ b/galleries/users_explain/axes/tight_layout_guide.py
@@ -9,15 +9,17 @@
How to use tight-layout to fit plots within your figure cleanly.
+.. tip::
+
+ *tight_layout* was the first layout engine in Matplotlib. The more modern
+ and more capable :ref:`Constrained Layout ` should
+ typically be used instead.
+
*tight_layout* automatically adjusts subplot params so that the
subplot(s) fits in to the figure area. This is an experimental
feature and may not work for some cases. It only checks the extents
of ticklabels, axis labels, and titles.
-An alternative to *tight_layout* is :ref:`constrained_layout
-`.
-
-
Simple example
==============
diff --git a/lib/matplotlib/layout_engine.py b/lib/matplotlib/layout_engine.py
index 5a96745d0697..8a3276b53371 100644
--- a/lib/matplotlib/layout_engine.py
+++ b/lib/matplotlib/layout_engine.py
@@ -10,9 +10,14 @@
layout engine while the figure is being created. In particular, colorbars are
made differently with different layout engines (for historical reasons).
-Matplotlib supplies two layout engines, `.TightLayoutEngine` and
-`.ConstrainedLayoutEngine`. Third parties can create their own layout engine
-by subclassing `.LayoutEngine`.
+Matplotlib has two built-in layout engines:
+
+- `.TightLayoutEngine` was the first layout engine added to Matplotlib.
+ See also :ref:`tight_layout_guide`.
+- `.ConstrainedLayoutEngine` is more modern and generally gives better results.
+ See also :ref:`constrainedlayout_guide`.
+
+Third parties can create their own layout engine by subclassing `.LayoutEngine`.
"""
from contextlib import nullcontext
From 1bdc36aea428696118679f3d1d1a86180cddef5f Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Fri, 28 Feb 2025 16:03:49 +0100
Subject: [PATCH 033/627] Backport PR #29689: Fix alt and caption handling in
Sphinx directives
---
lib/matplotlib/sphinxext/figmpl_directive.py | 78 +++++++++----------
lib/matplotlib/sphinxext/plot_directive.py | 5 +-
lib/matplotlib/tests/test_sphinxext.py | 19 +++--
lib/matplotlib/tests/tinypages/some_plots.rst | 7 +-
4 files changed, 59 insertions(+), 50 deletions(-)
diff --git a/lib/matplotlib/sphinxext/figmpl_directive.py b/lib/matplotlib/sphinxext/figmpl_directive.py
index 5ef34f4dd0b1..7cb9c6c04e8a 100644
--- a/lib/matplotlib/sphinxext/figmpl_directive.py
+++ b/lib/matplotlib/sphinxext/figmpl_directive.py
@@ -12,16 +12,14 @@
See the *FigureMpl* documentation below.
"""
-from docutils import nodes
-
-from docutils.parsers.rst import directives
-from docutils.parsers.rst.directives.images import Figure, Image
-
import os
from os.path import relpath
from pathlib import PurePath, Path
import shutil
+from docutils import nodes
+from docutils.parsers.rst import directives
+from docutils.parsers.rst.directives.images import Figure, Image
from sphinx.errors import ExtensionError
import matplotlib
@@ -193,12 +191,13 @@ def visit_figmpl_html(self, node):
# make uri also be relative...
nm = PurePath(node['uri'][1:]).name
uri = f'{imagerel}/{rel}{nm}'
+ img_attrs = {'src': uri, 'alt': node['alt']}
# make srcset str. Need to change all the prefixes!
maxsrc = uri
- srcsetst = ''
if srcset:
maxmult = -1
+ srcsetst = ''
for mult, src in srcset.items():
nm = PurePath(src[1:]).name
# ../../_images/plot_1_2_0x.png
@@ -214,44 +213,43 @@ def visit_figmpl_html(self, node):
maxsrc = path
# trim trailing comma and space...
- srcsetst = srcsetst[:-2]
+ img_attrs['srcset'] = srcsetst[:-2]
- alt = node['alt']
if node['class'] is not None:
- classst = ' '.join(node['class'])
- classst = f'class="{classst}"'
-
- else:
- classst = ''
-
- stylers = ['width', 'height', 'scale']
- stylest = ''
- for style in stylers:
+ img_attrs['class'] = ' '.join(node['class'])
+ for style in ['width', 'height', 'scale']:
if node[style]:
- stylest += f'{style}: {node[style]};'
-
- figalign = node['align'] if node['align'] else 'center'
-
-#
-#
-#
-#
-#
-#