"
+ assert "image/png" in image
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_backend_ps.py b/lib/python3.12/site-packages/matplotlib/tests/test_backend_ps.py
new file mode 100644
index 0000000000000000000000000000000000000000..cc968795802e47e326dfdcfeda5d6133bcbf6149
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_backend_ps.py
@@ -0,0 +1,380 @@
+from collections import Counter
+from pathlib import Path
+import io
+import re
+import tempfile
+
+import numpy as np
+import pytest
+
+from matplotlib import cbook, path, patheffects, font_manager as fm
+from matplotlib.figure import Figure
+from matplotlib.patches import Ellipse
+from matplotlib.testing._markers import needs_ghostscript, needs_usetex
+from matplotlib.testing.decorators import check_figures_equal, image_comparison
+import matplotlib as mpl
+import matplotlib.collections as mcollections
+import matplotlib.colors as mcolors
+import matplotlib.pyplot as plt
+
+
+# This tests tends to hit a TeX cache lock on AppVeyor.
+@pytest.mark.flaky(reruns=3)
+@pytest.mark.parametrize('papersize', ['letter', 'figure'])
+@pytest.mark.parametrize('orientation', ['portrait', 'landscape'])
+@pytest.mark.parametrize('format, use_log, rcParams', [
+ ('ps', False, {}),
+ ('ps', False, {'ps.usedistiller': 'ghostscript'}),
+ ('ps', False, {'ps.usedistiller': 'xpdf'}),
+ ('ps', False, {'text.usetex': True}),
+ ('eps', False, {}),
+ ('eps', True, {'ps.useafm': True}),
+ ('eps', False, {'text.usetex': True}),
+], ids=[
+ 'ps',
+ 'ps with distiller=ghostscript',
+ 'ps with distiller=xpdf',
+ 'ps with usetex',
+ 'eps',
+ 'eps afm',
+ 'eps with usetex'
+])
+def test_savefig_to_stringio(format, use_log, rcParams, orientation, papersize):
+ mpl.rcParams.update(rcParams)
+ if mpl.rcParams["ps.usedistiller"] == "ghostscript":
+ try:
+ mpl._get_executable_info("gs")
+ except mpl.ExecutableNotFoundError as exc:
+ pytest.skip(str(exc))
+ elif mpl.rcParams["ps.usedistiller"] == "xpdf":
+ try:
+ mpl._get_executable_info("gs") # Effectively checks for ps2pdf.
+ mpl._get_executable_info("pdftops")
+ except mpl.ExecutableNotFoundError as exc:
+ pytest.skip(str(exc))
+
+ fig, ax = plt.subplots()
+
+ with io.StringIO() as s_buf, io.BytesIO() as b_buf:
+
+ if use_log:
+ ax.set_yscale('log')
+
+ ax.plot([1, 2], [1, 2])
+ title = "Déjà vu"
+ if not mpl.rcParams["text.usetex"]:
+ title += " \N{MINUS SIGN}\N{EURO SIGN}"
+ ax.set_title(title)
+ allowable_exceptions = []
+ if mpl.rcParams["text.usetex"]:
+ allowable_exceptions.append(RuntimeError)
+ if mpl.rcParams["ps.useafm"]:
+ allowable_exceptions.append(mpl.MatplotlibDeprecationWarning)
+ try:
+ fig.savefig(s_buf, format=format, orientation=orientation,
+ papertype=papersize)
+ fig.savefig(b_buf, format=format, orientation=orientation,
+ papertype=papersize)
+ except tuple(allowable_exceptions) as exc:
+ pytest.skip(str(exc))
+
+ assert not s_buf.closed
+ assert not b_buf.closed
+ s_val = s_buf.getvalue().encode('ascii')
+ b_val = b_buf.getvalue()
+
+ if format == 'ps':
+ # Default figsize = (8, 6) inches = (576, 432) points = (203.2, 152.4) mm.
+ # Landscape orientation will swap dimensions.
+ if mpl.rcParams["ps.usedistiller"] == "xpdf":
+ # Some versions specifically show letter/203x152, but not all,
+ # so we can only use this simpler test.
+ if papersize == 'figure':
+ assert b'letter' not in s_val.lower()
+ else:
+ assert b'letter' in s_val.lower()
+ elif mpl.rcParams["ps.usedistiller"] or mpl.rcParams["text.usetex"]:
+ width = b'432.0' if orientation == 'landscape' else b'576.0'
+ wanted = (b'-dDEVICEWIDTHPOINTS=' + width if papersize == 'figure'
+ else b'-sPAPERSIZE')
+ assert wanted in s_val
+ else:
+ if papersize == 'figure':
+ assert b'%%DocumentPaperSizes' not in s_val
+ else:
+ assert b'%%DocumentPaperSizes' in s_val
+
+ # Strip out CreationDate: ghostscript and cairo don't obey
+ # SOURCE_DATE_EPOCH, and that environment variable is already tested in
+ # test_determinism.
+ s_val = re.sub(b"(?<=\n%%CreationDate: ).*", b"", s_val)
+ b_val = re.sub(b"(?<=\n%%CreationDate: ).*", b"", b_val)
+
+ assert s_val == b_val.replace(b'\r\n', b'\n')
+
+
+def test_patheffects():
+ mpl.rcParams['path.effects'] = [
+ patheffects.withStroke(linewidth=4, foreground='w')]
+ fig, ax = plt.subplots()
+ ax.plot([1, 2, 3])
+ with io.BytesIO() as ps:
+ fig.savefig(ps, format='ps')
+
+
+@needs_usetex
+@needs_ghostscript
+def test_tilde_in_tempfilename(tmp_path):
+ # Tilde ~ in the tempdir path (e.g. TMPDIR, TMP or TEMP on windows
+ # when the username is very long and windows uses a short name) breaks
+ # latex before https://github.com/matplotlib/matplotlib/pull/5928
+ base_tempdir = tmp_path / "short-1"
+ base_tempdir.mkdir()
+ # Change the path for new tempdirs, which is used internally by the ps
+ # backend to write a file.
+ with cbook._setattr_cm(tempfile, tempdir=str(base_tempdir)):
+ # usetex results in the latex call, which does not like the ~
+ mpl.rcParams['text.usetex'] = True
+ plt.plot([1, 2, 3, 4])
+ plt.xlabel(r'\textbf{time} (s)')
+ # use the PS backend to write the file...
+ plt.savefig(base_tempdir / 'tex_demo.eps', format="ps")
+
+
+@image_comparison(["empty.eps"])
+def test_transparency():
+ fig, ax = plt.subplots()
+ ax.set_axis_off()
+ ax.plot([0, 1], color="r", alpha=0)
+ ax.text(.5, .5, "foo", color="r", alpha=0)
+
+
+@needs_usetex
+@image_comparison(["empty.eps"])
+def test_transparency_tex():
+ mpl.rcParams['text.usetex'] = True
+ fig, ax = plt.subplots()
+ ax.set_axis_off()
+ ax.plot([0, 1], color="r", alpha=0)
+ ax.text(.5, .5, "foo", color="r", alpha=0)
+
+
+def test_bbox():
+ fig, ax = plt.subplots()
+ with io.BytesIO() as buf:
+ fig.savefig(buf, format='eps')
+ buf = buf.getvalue()
+
+ bb = re.search(b'^%%BoundingBox: (.+) (.+) (.+) (.+)$', buf, re.MULTILINE)
+ assert bb
+ hibb = re.search(b'^%%HiResBoundingBox: (.+) (.+) (.+) (.+)$', buf,
+ re.MULTILINE)
+ assert hibb
+
+ for i in range(1, 5):
+ # BoundingBox must use integers, and be ceil/floor of the hi res.
+ assert b'.' not in bb.group(i)
+ assert int(bb.group(i)) == pytest.approx(float(hibb.group(i)), 1)
+
+
+@needs_usetex
+def test_failing_latex():
+ """Test failing latex subprocess call"""
+ mpl.rcParams['text.usetex'] = True
+ # This fails with "Double subscript"
+ plt.xlabel("$22_2_2$")
+ with pytest.raises(RuntimeError):
+ plt.savefig(io.BytesIO(), format="ps")
+
+
+@needs_usetex
+def test_partial_usetex(caplog):
+ caplog.set_level("WARNING")
+ plt.figtext(.1, .1, "foo", usetex=True)
+ plt.figtext(.2, .2, "bar", usetex=True)
+ plt.savefig(io.BytesIO(), format="ps")
+ record, = caplog.records # asserts there's a single record.
+ assert "as if usetex=False" in record.getMessage()
+
+
+@needs_usetex
+def test_usetex_preamble(caplog):
+ mpl.rcParams.update({
+ "text.usetex": True,
+ # Check that these don't conflict with the packages loaded by default.
+ "text.latex.preamble": r"\usepackage{color,graphicx,textcomp}",
+ })
+ plt.figtext(.5, .5, "foo")
+ plt.savefig(io.BytesIO(), format="ps")
+
+
+@image_comparison(["useafm.eps"])
+def test_useafm():
+ mpl.rcParams["ps.useafm"] = True
+ fig, ax = plt.subplots()
+ ax.set_axis_off()
+ ax.axhline(.5)
+ ax.text(.5, .5, "qk")
+
+
+@image_comparison(["type3.eps"])
+def test_type3_font():
+ plt.figtext(.5, .5, "I/J")
+
+
+@image_comparison(["coloredhatcheszerolw.eps"])
+def test_colored_hatch_zero_linewidth():
+ ax = plt.gca()
+ ax.add_patch(Ellipse((0, 0), 1, 1, hatch='/', facecolor='none',
+ edgecolor='r', linewidth=0))
+ ax.add_patch(Ellipse((0.5, 0.5), 0.5, 0.5, hatch='+', facecolor='none',
+ edgecolor='g', linewidth=0.2))
+ ax.add_patch(Ellipse((1, 1), 0.3, 0.8, hatch='\\', facecolor='none',
+ edgecolor='b', linewidth=0))
+ ax.set_axis_off()
+
+
+@check_figures_equal(extensions=["eps"])
+def test_text_clip(fig_test, fig_ref):
+ ax = fig_test.add_subplot()
+ # Fully clipped-out text should not appear.
+ ax.text(0, 0, "hello", transform=fig_test.transFigure, clip_on=True)
+ fig_ref.add_subplot()
+
+
+@needs_ghostscript
+def test_d_glyph(tmp_path):
+ # Ensure that we don't have a procedure defined as /d, which would be
+ # overwritten by the glyph definition for "d".
+ fig = plt.figure()
+ fig.text(.5, .5, "def")
+ out = tmp_path / "test.eps"
+ fig.savefig(out)
+ mpl.testing.compare.convert(out, cache=False) # Should not raise.
+
+
+@image_comparison(["type42_without_prep.eps"], style='mpl20')
+def test_type42_font_without_prep():
+ # Test whether Type 42 fonts without prep table are properly embedded
+ mpl.rcParams["ps.fonttype"] = 42
+ mpl.rcParams["mathtext.fontset"] = "stix"
+
+ plt.figtext(0.5, 0.5, "Mass $m$")
+
+
+@pytest.mark.parametrize('fonttype', ["3", "42"])
+def test_fonttype(fonttype):
+ mpl.rcParams["ps.fonttype"] = fonttype
+ fig, ax = plt.subplots()
+
+ ax.text(0.25, 0.5, "Forty-two is the answer to everything!")
+
+ buf = io.BytesIO()
+ fig.savefig(buf, format="ps")
+
+ test = b'/FontType ' + bytes(f"{fonttype}", encoding='utf-8') + b' def'
+
+ assert re.search(test, buf.getvalue(), re.MULTILINE)
+
+
+def test_linedash():
+ """Test that dashed lines do not break PS output"""
+ fig, ax = plt.subplots()
+
+ ax.plot([0, 1], linestyle="--")
+
+ buf = io.BytesIO()
+ fig.savefig(buf, format="ps")
+
+ assert buf.tell() > 0
+
+
+def test_empty_line():
+ # Smoke-test for gh#23954
+ figure = Figure()
+ figure.text(0.5, 0.5, "\nfoo\n\n")
+ buf = io.BytesIO()
+ figure.savefig(buf, format='eps')
+ figure.savefig(buf, format='ps')
+
+
+def test_no_duplicate_definition():
+
+ fig = Figure()
+ axs = fig.subplots(4, 4, subplot_kw=dict(projection="polar"))
+ for ax in axs.flat:
+ ax.set(xticks=[], yticks=[])
+ ax.plot([1, 2])
+ fig.suptitle("hello, world")
+
+ buf = io.StringIO()
+ fig.savefig(buf, format='eps')
+ buf.seek(0)
+
+ wds = [ln.partition(' ')[0] for
+ ln in buf.readlines()
+ if ln.startswith('/')]
+
+ assert max(Counter(wds).values()) == 1
+
+
+@image_comparison(["multi_font_type3.eps"], tol=0.51)
+def test_multi_font_type3():
+ fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
+ if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
+ pytest.skip("Font may be missing")
+
+ plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27)
+ plt.rc('ps', fonttype=3)
+
+ fig = plt.figure()
+ fig.text(0.15, 0.475, "There are å‡ ä¸ªæ±‰å— in between!")
+
+
+@image_comparison(["multi_font_type42.eps"], tol=1.6)
+def test_multi_font_type42():
+ fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
+ if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
+ pytest.skip("Font may be missing")
+
+ plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27)
+ plt.rc('ps', fonttype=42)
+
+ fig = plt.figure()
+ fig.text(0.15, 0.475, "There are å‡ ä¸ªæ±‰å— in between!")
+
+
+@image_comparison(["scatter.eps"])
+def test_path_collection():
+ rng = np.random.default_rng(19680801)
+ xvals = rng.uniform(0, 1, 10)
+ yvals = rng.uniform(0, 1, 10)
+ sizes = rng.uniform(30, 100, 10)
+ fig, ax = plt.subplots()
+ ax.scatter(xvals, yvals, sizes, edgecolor=[0.9, 0.2, 0.1], marker='<')
+ ax.set_axis_off()
+ paths = [path.Path.unit_regular_polygon(i) for i in range(3, 7)]
+ offsets = rng.uniform(0, 200, 20).reshape(10, 2)
+ sizes = [0.02, 0.04]
+ pc = mcollections.PathCollection(paths, sizes, zorder=-1,
+ facecolors='yellow', offsets=offsets)
+ ax.add_collection(pc)
+ ax.set_xlim(0, 1)
+
+
+@image_comparison(["colorbar_shift.eps"], savefig_kwarg={"bbox_inches": "tight"},
+ style="mpl20")
+def test_colorbar_shift(tmp_path):
+ cmap = mcolors.ListedColormap(["r", "g", "b"])
+ norm = mcolors.BoundaryNorm([-1, -0.5, 0.5, 1], cmap.N)
+ plt.scatter([0, 1], [1, 1], c=[0, 1], cmap=cmap, norm=norm)
+ plt.colorbar()
+
+
+def test_auto_papersize_removal():
+ fig = plt.figure()
+ with pytest.raises(ValueError, match="'auto' is not a valid value"):
+ fig.savefig(io.BytesIO(), format='eps', papertype='auto')
+
+ with pytest.raises(ValueError, match="'auto' is not a valid value"):
+ mpl.rcParams['ps.papersize'] = 'auto'
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_backend_registry.py b/lib/python3.12/site-packages/matplotlib/tests/test_backend_registry.py
new file mode 100644
index 0000000000000000000000000000000000000000..80c2ce4fc51a6869315a84a91fcced18a7a0443f
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_backend_registry.py
@@ -0,0 +1,180 @@
+from collections.abc import Sequence
+from typing import Any
+
+import pytest
+
+import matplotlib as mpl
+from matplotlib.backends import BackendFilter, backend_registry
+
+
+@pytest.fixture
+def clear_backend_registry():
+ # Fixture that clears the singleton backend_registry before and after use
+ # so that the test state remains isolated.
+ backend_registry._clear()
+ yield
+ backend_registry._clear()
+
+
+def has_duplicates(seq: Sequence[Any]) -> bool:
+ return len(seq) > len(set(seq))
+
+
+@pytest.mark.parametrize(
+ 'framework,expected',
+ [
+ ('qt', 'qtagg'),
+ ('gtk3', 'gtk3agg'),
+ ('gtk4', 'gtk4agg'),
+ ('wx', 'wxagg'),
+ ('tk', 'tkagg'),
+ ('macosx', 'macosx'),
+ ('headless', 'agg'),
+ ('does not exist', None),
+ ]
+)
+def test_backend_for_gui_framework(framework, expected):
+ assert backend_registry.backend_for_gui_framework(framework) == expected
+
+
+def test_list_builtin():
+ backends = backend_registry.list_builtin()
+ assert not has_duplicates(backends)
+ # Compare using sets as order is not important
+ assert {*backends} == {
+ 'gtk3agg', 'gtk3cairo', 'gtk4agg', 'gtk4cairo', 'macosx', 'nbagg', 'notebook',
+ 'qtagg', 'qtcairo', 'qt5agg', 'qt5cairo', 'tkagg',
+ 'tkcairo', 'webagg', 'wx', 'wxagg', 'wxcairo', 'agg', 'cairo', 'pdf', 'pgf',
+ 'ps', 'svg', 'template',
+ }
+
+
+@pytest.mark.parametrize(
+ 'filter,expected',
+ [
+ (BackendFilter.INTERACTIVE,
+ ['gtk3agg', 'gtk3cairo', 'gtk4agg', 'gtk4cairo', 'macosx', 'nbagg', 'notebook',
+ 'qtagg', 'qtcairo', 'qt5agg', 'qt5cairo', 'tkagg',
+ 'tkcairo', 'webagg', 'wx', 'wxagg', 'wxcairo']),
+ (BackendFilter.NON_INTERACTIVE,
+ ['agg', 'cairo', 'pdf', 'pgf', 'ps', 'svg', 'template']),
+ ]
+)
+def test_list_builtin_with_filter(filter, expected):
+ backends = backend_registry.list_builtin(filter)
+ assert not has_duplicates(backends)
+ # Compare using sets as order is not important
+ assert {*backends} == {*expected}
+
+
+def test_list_gui_frameworks():
+ frameworks = backend_registry.list_gui_frameworks()
+ assert not has_duplicates(frameworks)
+ # Compare using sets as order is not important
+ assert {*frameworks} == {
+ "gtk3", "gtk4", "macosx", "qt", "qt5", "qt6", "tk", "wx",
+ }
+
+
+@pytest.mark.parametrize("backend, is_valid", [
+ ("agg", True),
+ ("QtAgg", True),
+ ("module://anything", True),
+ ("made-up-name", False),
+])
+def test_is_valid_backend(backend, is_valid):
+ assert backend_registry.is_valid_backend(backend) == is_valid
+
+
+@pytest.mark.parametrize("backend, normalized", [
+ ("agg", "matplotlib.backends.backend_agg"),
+ ("QtAgg", "matplotlib.backends.backend_qtagg"),
+ ("module://Anything", "Anything"),
+])
+def test_backend_normalization(backend, normalized):
+ assert backend_registry._backend_module_name(backend) == normalized
+
+
+def test_deprecated_rcsetup_attributes():
+ match = "was deprecated in Matplotlib 3.9"
+ with pytest.warns(mpl.MatplotlibDeprecationWarning, match=match):
+ mpl.rcsetup.interactive_bk
+ with pytest.warns(mpl.MatplotlibDeprecationWarning, match=match):
+ mpl.rcsetup.non_interactive_bk
+ with pytest.warns(mpl.MatplotlibDeprecationWarning, match=match):
+ mpl.rcsetup.all_backends
+
+
+def test_entry_points_inline():
+ pytest.importorskip('matplotlib_inline')
+ backends = backend_registry.list_all()
+ assert 'inline' in backends
+
+
+def test_entry_points_ipympl():
+ pytest.importorskip('ipympl')
+ backends = backend_registry.list_all()
+ assert 'ipympl' in backends
+ assert 'widget' in backends
+
+
+def test_entry_point_name_shadows_builtin(clear_backend_registry):
+ with pytest.raises(RuntimeError):
+ backend_registry._validate_and_store_entry_points(
+ [('qtagg', 'module1')])
+
+
+def test_entry_point_name_duplicate(clear_backend_registry):
+ with pytest.raises(RuntimeError):
+ backend_registry._validate_and_store_entry_points(
+ [('some_name', 'module1'), ('some_name', 'module2')])
+
+
+def test_entry_point_identical(clear_backend_registry):
+ # Issue https://github.com/matplotlib/matplotlib/issues/28367
+ # Multiple entry points with the same name and value (value is the module)
+ # are acceptable.
+ n = len(backend_registry._name_to_module)
+ backend_registry._validate_and_store_entry_points(
+ [('some_name', 'some.module'), ('some_name', 'some.module')])
+ assert len(backend_registry._name_to_module) == n+1
+ assert backend_registry._name_to_module['some_name'] == 'module://some.module'
+
+
+def test_entry_point_name_is_module(clear_backend_registry):
+ with pytest.raises(RuntimeError):
+ backend_registry._validate_and_store_entry_points(
+ [('module://backend.something', 'module1')])
+
+
+@pytest.mark.parametrize('backend', [
+ 'agg',
+ 'module://matplotlib.backends.backend_agg',
+])
+def test_load_entry_points_only_if_needed(clear_backend_registry, backend):
+ assert not backend_registry._loaded_entry_points
+ check = backend_registry.resolve_backend(backend)
+ assert check == (backend, None)
+ assert not backend_registry._loaded_entry_points
+ backend_registry.list_all() # Force load of entry points
+ assert backend_registry._loaded_entry_points
+
+
+@pytest.mark.parametrize(
+ 'gui_or_backend, expected_backend, expected_gui',
+ [
+ ('agg', 'agg', None),
+ ('qt', 'qtagg', 'qt'),
+ ('TkCairo', 'tkcairo', 'tk'),
+ ]
+)
+def test_resolve_gui_or_backend(gui_or_backend, expected_backend, expected_gui):
+ backend, gui = backend_registry.resolve_gui_or_backend(gui_or_backend)
+ assert backend == expected_backend
+ assert gui == expected_gui
+
+
+def test_resolve_gui_or_backend_invalid():
+ match = "is not a recognised GUI loop or backend name"
+ with pytest.raises(RuntimeError, match=match):
+ backend_registry.resolve_gui_or_backend('no-such-name')
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_backend_tk.py b/lib/python3.12/site-packages/matplotlib/tests/test_backend_tk.py
new file mode 100644
index 0000000000000000000000000000000000000000..1210c8c9993ed11f606fb9888520df2bab4d4da7
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_backend_tk.py
@@ -0,0 +1,280 @@
+import functools
+import importlib
+import os
+import platform
+import subprocess
+import sys
+
+import pytest
+
+from matplotlib import _c_internal_utils
+from matplotlib.testing import subprocess_run_helper
+
+
+_test_timeout = 60 # A reasonably safe value for slower architectures.
+
+
+def _isolated_tk_test(success_count, func=None):
+ """
+ A decorator to run *func* in a subprocess and assert that it prints
+ "success" *success_count* times and nothing on stderr.
+
+ TkAgg tests seem to have interactions between tests, so isolate each test
+ in a subprocess. See GH#18261
+ """
+
+ if func is None:
+ return functools.partial(_isolated_tk_test, success_count)
+
+ if "MPL_TEST_ESCAPE_HATCH" in os.environ:
+ # set in subprocess_run_helper() below
+ return func
+
+ @pytest.mark.skipif(
+ not importlib.util.find_spec('tkinter'),
+ reason="missing tkinter"
+ )
+ @pytest.mark.skipif(
+ sys.platform == "linux" and not _c_internal_utils.xdisplay_is_valid(),
+ reason="$DISPLAY is unset"
+ )
+ @pytest.mark.xfail( # https://github.com/actions/setup-python/issues/649
+ ('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
+ sys.platform == 'darwin' and sys.version_info[:2] < (3, 11),
+ reason='Tk version mismatch on Azure macOS CI'
+ )
+ @functools.wraps(func)
+ def test_func():
+ # even if the package exists, may not actually be importable this can
+ # be the case on some CI systems.
+ pytest.importorskip('tkinter')
+ try:
+ proc = subprocess_run_helper(
+ func, timeout=_test_timeout, extra_env=dict(
+ MPLBACKEND="TkAgg", MPL_TEST_ESCAPE_HATCH="1"))
+ except subprocess.TimeoutExpired:
+ pytest.fail("Subprocess timed out")
+ except subprocess.CalledProcessError as e:
+ pytest.fail("Subprocess failed to test intended behavior\n"
+ + str(e.stderr))
+ else:
+ # macOS may actually emit irrelevant errors about Accelerated
+ # OpenGL vs. software OpenGL, or some permission error on Azure, so
+ # suppress them.
+ # Asserting stderr first (and printing it on failure) should be
+ # more helpful for debugging that printing a failed success count.
+ ignored_lines = ["OpenGL", "CFMessagePort: bootstrap_register",
+ "/usr/include/servers/bootstrap_defs.h"]
+ assert not [line for line in proc.stderr.splitlines()
+ if all(msg not in line for msg in ignored_lines)]
+ assert proc.stdout.count("success") == success_count
+
+ return test_func
+
+
+@_isolated_tk_test(success_count=6) # len(bad_boxes)
+def test_blit():
+ import matplotlib.pyplot as plt
+ import numpy as np
+ import matplotlib.backends.backend_tkagg # noqa
+ from matplotlib.backends import _backend_tk, _tkagg
+
+ fig, ax = plt.subplots()
+ photoimage = fig.canvas._tkphoto
+ data = np.ones((4, 4, 4), dtype=np.uint8)
+ # Test out of bounds blitting.
+ bad_boxes = ((-1, 2, 0, 2),
+ (2, 0, 0, 2),
+ (1, 6, 0, 2),
+ (0, 2, -1, 2),
+ (0, 2, 2, 0),
+ (0, 2, 1, 6))
+ for bad_box in bad_boxes:
+ try:
+ _tkagg.blit(
+ photoimage.tk.interpaddr(), str(photoimage), data,
+ _tkagg.TK_PHOTO_COMPOSITE_OVERLAY, (0, 1, 2, 3), bad_box)
+ except ValueError:
+ print("success")
+
+ # Test blitting to a destroyed canvas.
+ plt.close(fig)
+ _backend_tk.blit(photoimage, data, (0, 1, 2, 3))
+
+
+@_isolated_tk_test(success_count=1)
+def test_figuremanager_preserves_host_mainloop():
+ import tkinter
+ import matplotlib.pyplot as plt
+ success = []
+
+ def do_plot():
+ plt.figure()
+ plt.plot([1, 2], [3, 5])
+ plt.close()
+ root.after(0, legitimate_quit)
+
+ def legitimate_quit():
+ root.quit()
+ success.append(True)
+
+ root = tkinter.Tk()
+ root.after(0, do_plot)
+ root.mainloop()
+
+ if success:
+ print("success")
+
+
+@pytest.mark.skipif(platform.python_implementation() != 'CPython',
+ reason='PyPy does not support Tkinter threading: '
+ 'https://foss.heptapod.net/pypy/pypy/-/issues/1929')
+@pytest.mark.flaky(reruns=3)
+@_isolated_tk_test(success_count=1)
+def test_figuremanager_cleans_own_mainloop():
+ import tkinter
+ import time
+ import matplotlib.pyplot as plt
+ import threading
+ from matplotlib.cbook import _get_running_interactive_framework
+
+ root = tkinter.Tk()
+ plt.plot([1, 2, 3], [1, 2, 5])
+
+ def target():
+ while not 'tk' == _get_running_interactive_framework():
+ time.sleep(.01)
+ plt.close()
+ if show_finished_event.wait():
+ print('success')
+
+ show_finished_event = threading.Event()
+ thread = threading.Thread(target=target, daemon=True)
+ thread.start()
+ plt.show(block=True) # Testing if this function hangs.
+ show_finished_event.set()
+ thread.join()
+
+
+@pytest.mark.flaky(reruns=3)
+@_isolated_tk_test(success_count=0)
+def test_never_update():
+ import tkinter
+ del tkinter.Misc.update
+ del tkinter.Misc.update_idletasks
+
+ import matplotlib.pyplot as plt
+ fig = plt.figure()
+ plt.show(block=False)
+
+ plt.draw() # Test FigureCanvasTkAgg.
+ fig.canvas.toolbar.configure_subplots() # Test NavigationToolbar2Tk.
+ # Test FigureCanvasTk filter_destroy callback
+ fig.canvas.get_tk_widget().after(100, plt.close, fig)
+
+ # Check for update() or update_idletasks() in the event queue, functionally
+ # equivalent to tkinter.Misc.update.
+ plt.show(block=True)
+
+ # Note that exceptions would be printed to stderr; _isolated_tk_test
+ # checks them.
+
+
+@_isolated_tk_test(success_count=2)
+def test_missing_back_button():
+ import matplotlib.pyplot as plt
+ from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk
+
+ class Toolbar(NavigationToolbar2Tk):
+ # Only display the buttons we need.
+ toolitems = [t for t in NavigationToolbar2Tk.toolitems if
+ t[0] in ('Home', 'Pan', 'Zoom')]
+
+ fig = plt.figure()
+ print("success")
+ Toolbar(fig.canvas, fig.canvas.manager.window) # This should not raise.
+ print("success")
+
+
+@_isolated_tk_test(success_count=2)
+def test_save_figure_return():
+ import matplotlib.pyplot as plt
+ from unittest import mock
+ fig = plt.figure()
+ prop = "tkinter.filedialog.asksaveasfilename"
+ with mock.patch(prop, return_value="foobar.png"):
+ fname = fig.canvas.manager.toolbar.save_figure()
+ os.remove("foobar.png")
+ assert fname == "foobar.png"
+ print("success")
+ with mock.patch(prop, return_value=""):
+ fname = fig.canvas.manager.toolbar.save_figure()
+ assert fname is None
+ print("success")
+
+
+@_isolated_tk_test(success_count=1)
+def test_canvas_focus():
+ import tkinter as tk
+ import matplotlib.pyplot as plt
+ success = []
+
+ def check_focus():
+ tkcanvas = fig.canvas.get_tk_widget()
+ # Give the plot window time to appear
+ if not tkcanvas.winfo_viewable():
+ tkcanvas.wait_visibility()
+ # Make sure the canvas has the focus, so that it's able to receive
+ # keyboard events.
+ if tkcanvas.focus_lastfor() == tkcanvas:
+ success.append(True)
+ plt.close()
+ root.destroy()
+
+ root = tk.Tk()
+ fig = plt.figure()
+ plt.plot([1, 2, 3])
+ root.after(0, plt.show)
+ root.after(100, check_focus)
+ root.mainloop()
+
+ if success:
+ print("success")
+
+
+@_isolated_tk_test(success_count=2)
+def test_embedding():
+ import tkinter as tk
+ from matplotlib.backends.backend_tkagg import (
+ FigureCanvasTkAgg, NavigationToolbar2Tk)
+ from matplotlib.backend_bases import key_press_handler
+ from matplotlib.figure import Figure
+
+ root = tk.Tk()
+
+ def test_figure(master):
+ fig = Figure()
+ ax = fig.add_subplot()
+ ax.plot([1, 2, 3])
+
+ canvas = FigureCanvasTkAgg(fig, master=master)
+ canvas.draw()
+ canvas.mpl_connect("key_press_event", key_press_handler)
+ canvas.get_tk_widget().pack(expand=True, fill="both")
+
+ toolbar = NavigationToolbar2Tk(canvas, master, pack_toolbar=False)
+ toolbar.pack(expand=True, fill="x")
+
+ canvas.get_tk_widget().forget()
+ toolbar.forget()
+
+ test_figure(root)
+ print("success")
+
+ # Test with a dark button color. Doesn't actually check whether the icon
+ # color becomes lighter, just that the code doesn't break.
+
+ root.tk_setPalette(background="sky blue", selectColor="midnight blue",
+ foreground="white")
+ test_figure(root)
+ print("success")
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_backend_tools.py b/lib/python3.12/site-packages/matplotlib/tests/test_backend_tools.py
new file mode 100644
index 0000000000000000000000000000000000000000..cc05a1a98f78f6b7267cf3d861013b3cf4416624
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_backend_tools.py
@@ -0,0 +1,20 @@
+import pytest
+
+from matplotlib.backend_tools import ToolHelpBase
+
+
+@pytest.mark.parametrize('rc_shortcut,expected', [
+ ('home', 'Home'),
+ ('backspace', 'Backspace'),
+ ('f1', 'F1'),
+ ('ctrl+a', 'Ctrl+A'),
+ ('ctrl+A', 'Ctrl+Shift+A'),
+ ('a', 'a'),
+ ('A', 'A'),
+ ('ctrl+shift+f1', 'Ctrl+Shift+F1'),
+ ('1', '1'),
+ ('cmd+p', 'Cmd+P'),
+ ('cmd+1', 'Cmd+1'),
+])
+def test_format_shortcut(rc_shortcut, expected):
+ assert ToolHelpBase.format_shortcut(rc_shortcut) == expected
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_backend_webagg.py b/lib/python3.12/site-packages/matplotlib/tests/test_backend_webagg.py
new file mode 100644
index 0000000000000000000000000000000000000000..1d6769494ef937edfd0116c612839a3fe08f3ec6
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_backend_webagg.py
@@ -0,0 +1,32 @@
+import os
+import sys
+import pytest
+
+import matplotlib.backends.backend_webagg_core
+from matplotlib.testing import subprocess_run_for_testing
+
+
+@pytest.mark.parametrize("backend", ["webagg", "nbagg"])
+def test_webagg_fallback(backend):
+ pytest.importorskip("tornado")
+ if backend == "nbagg":
+ pytest.importorskip("IPython")
+ env = dict(os.environ)
+ if sys.platform != "win32":
+ env["DISPLAY"] = ""
+
+ env["MPLBACKEND"] = backend
+
+ test_code = (
+ "import os;"
+ + f"assert os.environ['MPLBACKEND'] == '{backend}';"
+ + "import matplotlib.pyplot as plt; "
+ + "print(plt.get_backend());"
+ f"assert '{backend}' == plt.get_backend().lower();"
+ )
+ subprocess_run_for_testing([sys.executable, "-c", test_code], env=env, check=True)
+
+
+def test_webagg_core_no_toolbar():
+ fm = matplotlib.backends.backend_webagg_core.FigureManagerWebAgg
+ assert fm._toolbar2_class is None
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_backends_interactive.py b/lib/python3.12/site-packages/matplotlib/tests/test_backends_interactive.py
new file mode 100644
index 0000000000000000000000000000000000000000..063c72e2cde7a229900c235d20b9d438c7a3ab9a
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_backends_interactive.py
@@ -0,0 +1,794 @@
+import functools
+import importlib
+import importlib.util
+import inspect
+import json
+import os
+import platform
+import signal
+import subprocess
+import sys
+import tempfile
+import time
+import urllib.request
+
+from PIL import Image
+
+import pytest
+
+import matplotlib as mpl
+from matplotlib import _c_internal_utils
+from matplotlib.backend_tools import ToolToggleBase
+from matplotlib.testing import subprocess_run_helper as _run_helper, is_ci_environment
+
+
+class _WaitForStringPopen(subprocess.Popen):
+ """
+ A Popen that passes flags that allow triggering KeyboardInterrupt.
+ """
+
+ def __init__(self, *args, **kwargs):
+ if sys.platform == 'win32':
+ kwargs['creationflags'] = subprocess.CREATE_NEW_CONSOLE
+ super().__init__(
+ *args, **kwargs,
+ # Force Agg so that each test can switch to its desired backend.
+ env={**os.environ, "MPLBACKEND": "Agg", "SOURCE_DATE_EPOCH": "0"},
+ stdout=subprocess.PIPE, universal_newlines=True)
+
+ def wait_for(self, terminator):
+ """Read until the terminator is reached."""
+ buf = ''
+ while True:
+ c = self.stdout.read(1)
+ if not c:
+ raise RuntimeError(
+ f'Subprocess died before emitting expected {terminator!r}')
+ buf += c
+ if buf.endswith(terminator):
+ return
+
+
+# Minimal smoke-testing of the backends for which the dependencies are
+# PyPI-installable on CI. They are not available for all tested Python
+# versions so we don't fail on missing backends.
+
+@functools.lru_cache
+def _get_available_interactive_backends():
+ _is_linux_and_display_invalid = (sys.platform == "linux" and
+ not _c_internal_utils.display_is_valid())
+ _is_linux_and_xdisplay_invalid = (sys.platform == "linux" and
+ not _c_internal_utils.xdisplay_is_valid())
+ envs = []
+ for deps, env in [
+ *[([qt_api],
+ {"MPLBACKEND": "qtagg", "QT_API": qt_api})
+ for qt_api in ["PyQt6", "PySide6", "PyQt5", "PySide2"]],
+ *[([qt_api, "cairocffi"],
+ {"MPLBACKEND": "qtcairo", "QT_API": qt_api})
+ for qt_api in ["PyQt6", "PySide6", "PyQt5", "PySide2"]],
+ *[(["cairo", "gi"], {"MPLBACKEND": f"gtk{version}{renderer}"})
+ for version in [3, 4] for renderer in ["agg", "cairo"]],
+ (["tkinter"], {"MPLBACKEND": "tkagg"}),
+ (["wx"], {"MPLBACKEND": "wx"}),
+ (["wx"], {"MPLBACKEND": "wxagg"}),
+ (["matplotlib.backends._macosx"], {"MPLBACKEND": "macosx"}),
+ ]:
+ reason = None
+ missing = [dep for dep in deps if not importlib.util.find_spec(dep)]
+ if missing:
+ reason = "{} cannot be imported".format(", ".join(missing))
+ elif _is_linux_and_xdisplay_invalid and (
+ env["MPLBACKEND"] == "tkagg"
+ # Remove when https://github.com/wxWidgets/Phoenix/pull/2638 is out.
+ or env["MPLBACKEND"].startswith("wx")):
+ reason = "$DISPLAY is unset"
+ elif _is_linux_and_display_invalid:
+ reason = "$DISPLAY and $WAYLAND_DISPLAY are unset"
+ elif env["MPLBACKEND"] == 'macosx' and os.environ.get('TF_BUILD'):
+ reason = "macosx backend fails on Azure"
+ elif env["MPLBACKEND"].startswith('gtk'):
+ try:
+ import gi # type: ignore[import]
+ except ImportError:
+ # Though we check that `gi` exists above, it is possible that its
+ # C-level dependencies are not available, and then it still raises an
+ # `ImportError`, so guard against that.
+ available_gtk_versions = []
+ else:
+ gi_repo = gi.Repository.get_default()
+ available_gtk_versions = gi_repo.enumerate_versions('Gtk')
+ version = env["MPLBACKEND"][3]
+ if f'{version}.0' not in available_gtk_versions:
+ reason = "no usable GTK bindings"
+ marks = []
+ if reason:
+ marks.append(pytest.mark.skip(reason=f"Skipping {env} because {reason}"))
+ elif env["MPLBACKEND"].startswith('wx') and sys.platform == 'darwin':
+ # ignore on macosx because that's currently broken (github #16849)
+ marks.append(pytest.mark.xfail(reason='github #16849'))
+ elif (env['MPLBACKEND'] == 'tkagg' and
+ ('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
+ sys.platform == 'darwin' and
+ sys.version_info[:2] < (3, 11)
+ ):
+ marks.append( # https://github.com/actions/setup-python/issues/649
+ pytest.mark.xfail(reason='Tk version mismatch on Azure macOS CI'))
+ envs.append(({**env, 'BACKEND_DEPS': ','.join(deps)}, marks))
+ return envs
+
+
+def _get_testable_interactive_backends():
+ # We re-create this because some of the callers below might modify the markers.
+ return [pytest.param({**env}, marks=[*marks],
+ id='-'.join(f'{k}={v}' for k, v in env.items()))
+ for env, marks in _get_available_interactive_backends()]
+
+
+# Reasonable safe values for slower CI/Remote and local architectures.
+_test_timeout = 120 if is_ci_environment() else 20
+
+
+def _test_toolbar_button_la_mode_icon(fig):
+ # test a toolbar button icon using an image in LA mode (GH issue 25174)
+ # create an icon in LA mode
+ with tempfile.TemporaryDirectory() as tempdir:
+ img = Image.new("LA", (26, 26))
+ tmp_img_path = os.path.join(tempdir, "test_la_icon.png")
+ img.save(tmp_img_path)
+
+ class CustomTool(ToolToggleBase):
+ image = tmp_img_path
+ description = "" # gtk3 backend does not allow None
+
+ toolmanager = fig.canvas.manager.toolmanager
+ toolbar = fig.canvas.manager.toolbar
+ toolmanager.add_tool("test", CustomTool)
+ toolbar.add_tool("test", "group")
+
+
+# The source of this function gets extracted and run in another process, so it
+# must be fully self-contained.
+# Using a timer not only allows testing of timers (on other backends), but is
+# also necessary on gtk3 and wx, where directly processing a KeyEvent() for "q"
+# from draw_event causes breakage as the canvas widget gets deleted too early.
+def _test_interactive_impl():
+ import importlib.util
+ import io
+ import json
+ import sys
+
+ import pytest
+
+ import matplotlib as mpl
+ from matplotlib import pyplot as plt
+ from matplotlib.backend_bases import KeyEvent
+ mpl.rcParams.update({
+ "webagg.open_in_browser": False,
+ "webagg.port_retries": 1,
+ })
+
+ mpl.rcParams.update(json.loads(sys.argv[1]))
+ backend = plt.rcParams["backend"].lower()
+
+ if backend.endswith("agg") and not backend.startswith(("gtk", "web")):
+ # Force interactive framework setup.
+ fig = plt.figure()
+ plt.close(fig)
+
+ # Check that we cannot switch to a backend using another interactive
+ # framework, but can switch to a backend using cairo instead of agg,
+ # or a non-interactive backend. In the first case, we use tkagg as
+ # the "other" interactive backend as it is (essentially) guaranteed
+ # to be present. Moreover, don't test switching away from gtk3 (as
+ # Gtk.main_level() is not set up at this point yet) and webagg (which
+ # uses no interactive framework).
+
+ if backend != "tkagg":
+ with pytest.raises(ImportError):
+ mpl.use("tkagg", force=True)
+
+ def check_alt_backend(alt_backend):
+ mpl.use(alt_backend, force=True)
+ fig = plt.figure()
+ assert (type(fig.canvas).__module__ ==
+ f"matplotlib.backends.backend_{alt_backend}")
+ plt.close("all")
+
+ if importlib.util.find_spec("cairocffi"):
+ check_alt_backend(backend[:-3] + "cairo")
+ check_alt_backend("svg")
+ mpl.use(backend, force=True)
+
+ fig, ax = plt.subplots()
+ assert type(fig.canvas).__module__ == f"matplotlib.backends.backend_{backend}"
+
+ assert fig.canvas.manager.get_window_title() == "Figure 1"
+
+ if mpl.rcParams["toolbar"] == "toolmanager":
+ # test toolbar button icon LA mode see GH issue 25174
+ _test_toolbar_button_la_mode_icon(fig)
+
+ ax.plot([0, 1], [2, 3])
+ if fig.canvas.toolbar: # i.e toolbar2.
+ fig.canvas.toolbar.draw_rubberband(None, 1., 1, 2., 2)
+
+ timer = fig.canvas.new_timer(1.) # Test that floats are cast to int.
+ timer.add_callback(KeyEvent("key_press_event", fig.canvas, "q")._process)
+ # Trigger quitting upon draw.
+ fig.canvas.mpl_connect("draw_event", lambda event: timer.start())
+ fig.canvas.mpl_connect("close_event", print)
+
+ result = io.BytesIO()
+ fig.savefig(result, format='png')
+
+ plt.show()
+
+ # Ensure that the window is really closed.
+ plt.pause(0.5)
+
+ # Test that saving works after interactive window is closed, but the figure
+ # is not deleted.
+ result_after = io.BytesIO()
+ fig.savefig(result_after, format='png')
+
+ assert result.getvalue() == result_after.getvalue()
+
+
+@pytest.mark.parametrize("env", _get_testable_interactive_backends())
+@pytest.mark.parametrize("toolbar", ["toolbar2", "toolmanager"])
+@pytest.mark.flaky(reruns=3)
+def test_interactive_backend(env, toolbar):
+ if env["MPLBACKEND"] == "macosx":
+ if toolbar == "toolmanager":
+ pytest.skip("toolmanager is not implemented for macosx.")
+ if env["MPLBACKEND"] == "wx":
+ pytest.skip("wx backend is deprecated; tests failed on appveyor")
+ if env["MPLBACKEND"] == "wxagg" and toolbar == "toolmanager":
+ pytest.skip("Temporarily deactivated: show() changes figure height "
+ "and thus fails the test")
+ try:
+ proc = _run_helper(
+ _test_interactive_impl,
+ json.dumps({"toolbar": toolbar}),
+ timeout=_test_timeout,
+ extra_env=env,
+ )
+ except subprocess.CalledProcessError as err:
+ pytest.fail(
+ "Subprocess failed to test intended behavior\n"
+ + str(err.stderr))
+ assert proc.stdout.count("CloseEvent") == 1
+
+
+def _test_thread_impl():
+ from concurrent.futures import ThreadPoolExecutor
+
+ import matplotlib as mpl
+ from matplotlib import pyplot as plt
+
+ mpl.rcParams.update({
+ "webagg.open_in_browser": False,
+ "webagg.port_retries": 1,
+ })
+
+ # Test artist creation and drawing does not crash from thread
+ # No other guarantees!
+ fig, ax = plt.subplots()
+ # plt.pause needed vs plt.show(block=False) at least on toolbar2-tkagg
+ plt.pause(0.5)
+
+ future = ThreadPoolExecutor().submit(ax.plot, [1, 3, 6])
+ future.result() # Joins the thread; rethrows any exception.
+
+ fig.canvas.mpl_connect("close_event", print)
+ future = ThreadPoolExecutor().submit(fig.canvas.draw)
+ plt.pause(0.5) # flush_events fails here on at least Tkagg (bpo-41176)
+ future.result() # Joins the thread; rethrows any exception.
+ plt.close() # backend is responsible for flushing any events here
+ if plt.rcParams["backend"].lower().startswith("wx"):
+ # TODO: debug why WX needs this only on py >= 3.8
+ fig.canvas.flush_events()
+
+
+_thread_safe_backends = _get_testable_interactive_backends()
+# Known unsafe backends. Remove the xfails if they start to pass!
+for param in _thread_safe_backends:
+ backend = param.values[0]["MPLBACKEND"]
+ if "cairo" in backend:
+ # Cairo backends save a cairo_t on the graphics context, and sharing
+ # these is not threadsafe.
+ param.marks.append(
+ pytest.mark.xfail(raises=subprocess.CalledProcessError))
+ elif backend == "wx":
+ param.marks.append(
+ pytest.mark.xfail(raises=subprocess.CalledProcessError))
+ elif backend == "macosx":
+ from packaging.version import parse
+ mac_ver = platform.mac_ver()[0]
+ # Note, macOS Big Sur is both 11 and 10.16, depending on SDK that
+ # Python was compiled against.
+ if mac_ver and parse(mac_ver) < parse('10.16'):
+ param.marks.append(
+ pytest.mark.xfail(raises=subprocess.TimeoutExpired,
+ strict=True))
+ elif param.values[0].get("QT_API") == "PySide2":
+ param.marks.append(
+ pytest.mark.xfail(raises=subprocess.CalledProcessError))
+ elif backend == "tkagg" and platform.python_implementation() != 'CPython':
+ param.marks.append(
+ pytest.mark.xfail(
+ reason='PyPy does not support Tkinter threading: '
+ 'https://foss.heptapod.net/pypy/pypy/-/issues/1929',
+ strict=True))
+ elif (backend == 'tkagg' and
+ ('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
+ sys.platform == 'darwin' and sys.version_info[:2] < (3, 11)):
+ param.marks.append( # https://github.com/actions/setup-python/issues/649
+ pytest.mark.xfail('Tk version mismatch on Azure macOS CI'))
+
+
+@pytest.mark.parametrize("env", _thread_safe_backends)
+@pytest.mark.flaky(reruns=3)
+def test_interactive_thread_safety(env):
+ proc = _run_helper(_test_thread_impl, timeout=_test_timeout, extra_env=env)
+ assert proc.stdout.count("CloseEvent") == 1
+
+
+def _impl_test_lazy_auto_backend_selection():
+ import matplotlib
+ import matplotlib.pyplot as plt
+ # just importing pyplot should not be enough to trigger resolution
+ bk = matplotlib.rcParams._get('backend')
+ assert not isinstance(bk, str)
+ assert plt._backend_mod is None
+ # but actually plotting should
+ plt.plot(5)
+ assert plt._backend_mod is not None
+ bk = matplotlib.rcParams._get('backend')
+ assert isinstance(bk, str)
+
+
+def test_lazy_auto_backend_selection():
+ _run_helper(_impl_test_lazy_auto_backend_selection,
+ timeout=_test_timeout)
+
+
+def _implqt5agg():
+ import matplotlib.backends.backend_qt5agg # noqa
+ import sys
+
+ assert 'PyQt6' not in sys.modules
+ assert 'pyside6' not in sys.modules
+ assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
+
+
+def _implcairo():
+ import matplotlib.backends.backend_qt5cairo # noqa
+ import sys
+
+ assert 'PyQt6' not in sys.modules
+ assert 'pyside6' not in sys.modules
+ assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
+
+
+def _implcore():
+ import matplotlib.backends.backend_qt5 # noqa
+ import sys
+
+ assert 'PyQt6' not in sys.modules
+ assert 'pyside6' not in sys.modules
+ assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
+
+
+def test_qt5backends_uses_qt5():
+ qt5_bindings = [
+ dep for dep in ['PyQt5', 'pyside2']
+ if importlib.util.find_spec(dep) is not None
+ ]
+ qt6_bindings = [
+ dep for dep in ['PyQt6', 'pyside6']
+ if importlib.util.find_spec(dep) is not None
+ ]
+ if len(qt5_bindings) == 0 or len(qt6_bindings) == 0:
+ pytest.skip('need both QT6 and QT5 bindings')
+ _run_helper(_implqt5agg, timeout=_test_timeout)
+ if importlib.util.find_spec('pycairo') is not None:
+ _run_helper(_implcairo, timeout=_test_timeout)
+ _run_helper(_implcore, timeout=_test_timeout)
+
+
+def _impl_missing():
+ import sys
+ # Simulate uninstalled
+ sys.modules["PyQt6"] = None
+ sys.modules["PyQt5"] = None
+ sys.modules["PySide2"] = None
+ sys.modules["PySide6"] = None
+
+ import matplotlib.pyplot as plt
+ with pytest.raises(ImportError, match="Failed to import any of the following Qt"):
+ plt.switch_backend("qtagg")
+ # Specifically ensure that Pyside6/Pyqt6 are not in the error message for qt5agg
+ with pytest.raises(ImportError, match="^(?:(?!(PySide6|PyQt6)).)*$"):
+ plt.switch_backend("qt5agg")
+
+
+def test_qt_missing():
+ _run_helper(_impl_missing, timeout=_test_timeout)
+
+
+def _impl_test_cross_Qt_imports():
+ import importlib
+ import sys
+ import warnings
+
+ _, host_binding, mpl_binding = sys.argv
+ # import the mpl binding. This will force us to use that binding
+ importlib.import_module(f'{mpl_binding}.QtCore')
+ mpl_binding_qwidgets = importlib.import_module(f'{mpl_binding}.QtWidgets')
+ import matplotlib.backends.backend_qt
+ host_qwidgets = importlib.import_module(f'{host_binding}.QtWidgets')
+
+ host_app = host_qwidgets.QApplication(["mpl testing"])
+ warnings.filterwarnings("error", message=r".*Mixing Qt major.*",
+ category=UserWarning)
+ matplotlib.backends.backend_qt._create_qApp()
+
+
+def qt5_and_qt6_pairs():
+ qt5_bindings = [
+ dep for dep in ['PyQt5', 'PySide2']
+ if importlib.util.find_spec(dep) is not None
+ ]
+ qt6_bindings = [
+ dep for dep in ['PyQt6', 'PySide6']
+ if importlib.util.find_spec(dep) is not None
+ ]
+ if len(qt5_bindings) == 0 or len(qt6_bindings) == 0:
+ yield pytest.param(None, None,
+ marks=[pytest.mark.skip('need both QT6 and QT5 bindings')])
+ return
+
+ for qt5 in qt5_bindings:
+ for qt6 in qt6_bindings:
+ yield from ([qt5, qt6], [qt6, qt5])
+
+
+@pytest.mark.skipif(
+ sys.platform == "linux" and not _c_internal_utils.display_is_valid(),
+ reason="$DISPLAY and $WAYLAND_DISPLAY are unset")
+@pytest.mark.parametrize('host, mpl', [*qt5_and_qt6_pairs()])
+def test_cross_Qt_imports(host, mpl):
+ try:
+ proc = _run_helper(_impl_test_cross_Qt_imports, host, mpl,
+ timeout=_test_timeout)
+ except subprocess.CalledProcessError as ex:
+ # We do try to warn the user they are doing something that we do not
+ # expect to work, so we're going to ignore if the subprocess crashes or
+ # is killed, and just check that the warning is printed.
+ stderr = ex.stderr
+ else:
+ stderr = proc.stderr
+ assert "Mixing Qt major versions may not work as expected." in stderr
+
+
+@pytest.mark.skipif('TF_BUILD' in os.environ,
+ reason="this test fails an azure for unknown reasons")
+@pytest.mark.skipif(sys.platform == "win32", reason="Cannot send SIGINT on Windows.")
+def test_webagg():
+ pytest.importorskip("tornado")
+ proc = subprocess.Popen(
+ [sys.executable, "-c",
+ inspect.getsource(_test_interactive_impl)
+ + "\n_test_interactive_impl()", "{}"],
+ env={**os.environ, "MPLBACKEND": "webagg", "SOURCE_DATE_EPOCH": "0"})
+ url = f'http://{mpl.rcParams["webagg.address"]}:{mpl.rcParams["webagg.port"]}'
+ timeout = time.perf_counter() + _test_timeout
+ try:
+ while True:
+ try:
+ retcode = proc.poll()
+ # check that the subprocess for the server is not dead
+ assert retcode is None
+ conn = urllib.request.urlopen(url)
+ break
+ except urllib.error.URLError:
+ if time.perf_counter() > timeout:
+ pytest.fail("Failed to connect to the webagg server.")
+ else:
+ continue
+ conn.close()
+ proc.send_signal(signal.SIGINT)
+ assert proc.wait(timeout=_test_timeout) == 0
+ finally:
+ if proc.poll() is None:
+ proc.kill()
+
+
+def _lazy_headless():
+ import os
+ import sys
+
+ backend, deps = sys.argv[1:]
+ deps = deps.split(',')
+
+ # make it look headless
+ os.environ.pop('DISPLAY', None)
+ os.environ.pop('WAYLAND_DISPLAY', None)
+ for dep in deps:
+ assert dep not in sys.modules
+
+ # we should fast-track to Agg
+ import matplotlib.pyplot as plt
+ assert plt.get_backend() == 'agg'
+ for dep in deps:
+ assert dep not in sys.modules
+
+ # make sure we really have dependencies installed
+ for dep in deps:
+ importlib.import_module(dep)
+ assert dep in sys.modules
+
+ # try to switch and make sure we fail with ImportError
+ try:
+ plt.switch_backend(backend)
+ except ImportError:
+ pass
+ else:
+ sys.exit(1)
+
+
+@pytest.mark.skipif(sys.platform != "linux", reason="this a linux-only test")
+@pytest.mark.parametrize("env", _get_testable_interactive_backends())
+def test_lazy_linux_headless(env):
+ proc = _run_helper(
+ _lazy_headless,
+ env.pop('MPLBACKEND'), env.pop("BACKEND_DEPS"),
+ timeout=_test_timeout,
+ extra_env={**env, 'DISPLAY': '', 'WAYLAND_DISPLAY': ''}
+ )
+
+
+def _test_number_of_draws_script():
+ import matplotlib.pyplot as plt
+
+ fig, ax = plt.subplots()
+
+ # animated=True tells matplotlib to only draw the artist when we
+ # explicitly request it
+ ln, = ax.plot([0, 1], [1, 2], animated=True)
+
+ # make sure the window is raised, but the script keeps going
+ plt.show(block=False)
+ plt.pause(0.3)
+ # Connect to draw_event to count the occurrences
+ fig.canvas.mpl_connect('draw_event', print)
+
+ # get copy of entire figure (everything inside fig.bbox)
+ # sans animated artist
+ bg = fig.canvas.copy_from_bbox(fig.bbox)
+ # draw the animated artist, this uses a cached renderer
+ ax.draw_artist(ln)
+ # show the result to the screen
+ fig.canvas.blit(fig.bbox)
+
+ for j in range(10):
+ # reset the background back in the canvas state, screen unchanged
+ fig.canvas.restore_region(bg)
+ # Create a **new** artist here, this is poor usage of blitting
+ # but good for testing to make sure that this doesn't create
+ # excessive draws
+ ln, = ax.plot([0, 1], [1, 2])
+ # render the artist, updating the canvas state, but not the screen
+ ax.draw_artist(ln)
+ # copy the image to the GUI state, but screen might not changed yet
+ fig.canvas.blit(fig.bbox)
+ # flush any pending GUI events, re-painting the screen if needed
+ fig.canvas.flush_events()
+
+ # Let the event loop process everything before leaving
+ plt.pause(0.1)
+
+
+_blit_backends = _get_testable_interactive_backends()
+for param in _blit_backends:
+ backend = param.values[0]["MPLBACKEND"]
+ if backend == "gtk3cairo":
+ # copy_from_bbox only works when rendering to an ImageSurface
+ param.marks.append(
+ pytest.mark.skip("gtk3cairo does not support blitting"))
+ elif backend == "gtk4cairo":
+ # copy_from_bbox only works when rendering to an ImageSurface
+ param.marks.append(
+ pytest.mark.skip("gtk4cairo does not support blitting"))
+ elif backend == "wx":
+ param.marks.append(
+ pytest.mark.skip("wx does not support blitting"))
+ elif (backend == 'tkagg' and
+ ('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
+ sys.platform == 'darwin' and
+ sys.version_info[:2] < (3, 11)
+ ):
+ param.marks.append( # https://github.com/actions/setup-python/issues/649
+ pytest.mark.xfail('Tk version mismatch on Azure macOS CI')
+ )
+
+
+@pytest.mark.parametrize("env", _blit_backends)
+# subprocesses can struggle to get the display, so rerun a few times
+@pytest.mark.flaky(reruns=4)
+def test_blitting_events(env):
+ proc = _run_helper(
+ _test_number_of_draws_script, timeout=_test_timeout, extra_env=env)
+ # Count the number of draw_events we got. We could count some initial
+ # canvas draws (which vary in number by backend), but the critical
+ # check here is that it isn't 10 draws, which would be called if
+ # blitting is not properly implemented
+ ndraws = proc.stdout.count("DrawEvent")
+ assert 0 < ndraws < 5
+
+
+def _impl_test_interactive_timers():
+ # A timer with <1 millisecond gets converted to int and therefore 0
+ # milliseconds, which the mac framework interprets as singleshot.
+ # We only want singleshot if we specify that ourselves, otherwise we want
+ # a repeating timer
+ import os
+ from unittest.mock import Mock
+ import matplotlib.pyplot as plt
+ # increase pause duration on CI to let things spin up
+ # particularly relevant for gtk3cairo
+ pause_time = 2 if os.getenv("CI") else 0.5
+ fig = plt.figure()
+ plt.pause(pause_time)
+ timer = fig.canvas.new_timer(0.1)
+ mock = Mock()
+ timer.add_callback(mock)
+ timer.start()
+ plt.pause(pause_time)
+ timer.stop()
+ assert mock.call_count > 1
+
+ # Now turn it into a single shot timer and verify only one gets triggered
+ mock.call_count = 0
+ timer.single_shot = True
+ timer.start()
+ plt.pause(pause_time)
+ assert mock.call_count == 1
+
+ # Make sure we can start the timer a second time
+ timer.start()
+ plt.pause(pause_time)
+ assert mock.call_count == 2
+ plt.close("all")
+
+
+@pytest.mark.parametrize("env", _get_testable_interactive_backends())
+def test_interactive_timers(env):
+ if env["MPLBACKEND"] == "gtk3cairo" and os.getenv("CI"):
+ pytest.skip("gtk3cairo timers do not work in remote CI")
+ if env["MPLBACKEND"] == "wx":
+ pytest.skip("wx backend is deprecated; tests failed on appveyor")
+ _run_helper(_impl_test_interactive_timers,
+ timeout=_test_timeout, extra_env=env)
+
+
+def _test_sigint_impl(backend, target_name, kwargs):
+ import sys
+ import matplotlib.pyplot as plt
+ import os
+ import threading
+
+ plt.switch_backend(backend)
+
+ def interrupter():
+ if sys.platform == 'win32':
+ import win32api
+ win32api.GenerateConsoleCtrlEvent(0, 0)
+ else:
+ import signal
+ os.kill(os.getpid(), signal.SIGINT)
+
+ target = getattr(plt, target_name)
+ timer = threading.Timer(1, interrupter)
+ fig = plt.figure()
+ fig.canvas.mpl_connect(
+ 'draw_event',
+ lambda *args: print('DRAW', flush=True)
+ )
+ fig.canvas.mpl_connect(
+ 'draw_event',
+ lambda *args: timer.start()
+ )
+ try:
+ target(**kwargs)
+ except KeyboardInterrupt:
+ print('SUCCESS', flush=True)
+
+
+@pytest.mark.parametrize("env", _get_testable_interactive_backends())
+@pytest.mark.parametrize("target, kwargs", [
+ ('show', {'block': True}),
+ ('pause', {'interval': 10})
+])
+def test_sigint(env, target, kwargs):
+ backend = env.get("MPLBACKEND")
+ if not backend.startswith(("qt", "macosx")):
+ pytest.skip("SIGINT currently only tested on qt and macosx")
+ proc = _WaitForStringPopen(
+ [sys.executable, "-c",
+ inspect.getsource(_test_sigint_impl) +
+ f"\n_test_sigint_impl({backend!r}, {target!r}, {kwargs!r})"])
+ try:
+ proc.wait_for('DRAW')
+ stdout, _ = proc.communicate(timeout=_test_timeout)
+ except Exception:
+ proc.kill()
+ stdout, _ = proc.communicate()
+ raise
+ assert 'SUCCESS' in stdout
+
+
+def _test_other_signal_before_sigint_impl(backend, target_name, kwargs):
+ import signal
+ import matplotlib.pyplot as plt
+
+ plt.switch_backend(backend)
+
+ target = getattr(plt, target_name)
+
+ fig = plt.figure()
+ fig.canvas.mpl_connect('draw_event', lambda *args: print('DRAW', flush=True))
+
+ timer = fig.canvas.new_timer(interval=1)
+ timer.single_shot = True
+ timer.add_callback(print, 'SIGUSR1', flush=True)
+
+ def custom_signal_handler(signum, frame):
+ timer.start()
+ signal.signal(signal.SIGUSR1, custom_signal_handler)
+
+ try:
+ target(**kwargs)
+ except KeyboardInterrupt:
+ print('SUCCESS', flush=True)
+
+
+@pytest.mark.skipif(sys.platform == 'win32',
+ reason='No other signal available to send on Windows')
+@pytest.mark.parametrize("env", _get_testable_interactive_backends())
+@pytest.mark.parametrize("target, kwargs", [
+ ('show', {'block': True}),
+ ('pause', {'interval': 10})
+])
+def test_other_signal_before_sigint(env, target, kwargs, request):
+ backend = env.get("MPLBACKEND")
+ if not backend.startswith(("qt", "macosx")):
+ pytest.skip("SIGINT currently only tested on qt and macosx")
+ if backend == "macosx":
+ request.node.add_marker(pytest.mark.xfail(reason="macosx backend is buggy"))
+ if sys.platform == "darwin" and target == "show":
+ # We've not previously had these toolkits installed on CI, and so were never
+ # aware that this was crashing. However, we've had little luck reproducing it
+ # locally, so mark it xfail for now. For more information, see
+ # https://github.com/matplotlib/matplotlib/issues/27984
+ request.node.add_marker(
+ pytest.mark.xfail(reason="Qt backend is buggy on macOS"))
+ proc = _WaitForStringPopen(
+ [sys.executable, "-c",
+ inspect.getsource(_test_other_signal_before_sigint_impl) +
+ "\n_test_other_signal_before_sigint_impl("
+ f"{backend!r}, {target!r}, {kwargs!r})"])
+ try:
+ proc.wait_for('DRAW')
+ os.kill(proc.pid, signal.SIGUSR1)
+ proc.wait_for('SIGUSR1')
+ os.kill(proc.pid, signal.SIGINT)
+ stdout, _ = proc.communicate(timeout=_test_timeout)
+ except Exception:
+ proc.kill()
+ stdout, _ = proc.communicate()
+ raise
+ print(stdout)
+ assert 'SUCCESS' in stdout
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_basic.py b/lib/python3.12/site-packages/matplotlib/tests/test_basic.py
new file mode 100644
index 0000000000000000000000000000000000000000..f6aa1e458555dbd313f556e19094e82062ba8ade
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_basic.py
@@ -0,0 +1,48 @@
+import builtins
+import os
+import sys
+import textwrap
+
+from matplotlib.testing import subprocess_run_for_testing
+
+
+def test_simple():
+ assert 1 + 1 == 2
+
+
+def test_override_builtins():
+ import pylab # type: ignore[import]
+ ok_to_override = {
+ '__name__',
+ '__doc__',
+ '__package__',
+ '__loader__',
+ '__spec__',
+ 'any',
+ 'all',
+ 'sum',
+ 'divmod'
+ }
+ overridden = {key for key in {*dir(pylab)} & {*dir(builtins)}
+ if getattr(pylab, key) != getattr(builtins, key)}
+ assert overridden <= ok_to_override
+
+
+def test_lazy_imports():
+ source = textwrap.dedent("""
+ import sys
+
+ import matplotlib.figure
+ import matplotlib.backend_bases
+ import matplotlib.pyplot
+
+ assert 'matplotlib._tri' not in sys.modules
+ assert 'matplotlib._qhull' not in sys.modules
+ assert 'matplotlib._contour' not in sys.modules
+ assert 'urllib.request' not in sys.modules
+ """)
+
+ subprocess_run_for_testing(
+ [sys.executable, '-c', source],
+ env={**os.environ, "MPLBACKEND": "", "MATPLOTLIBRC": os.devnull},
+ check=True)
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_bezier.py b/lib/python3.12/site-packages/matplotlib/tests/test_bezier.py
new file mode 100644
index 0000000000000000000000000000000000000000..65e2c616e7381ee7e0b611ac8feaac8ab411dac1
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_bezier.py
@@ -0,0 +1,17 @@
+"""
+Tests specific to the bezier module.
+"""
+
+from matplotlib.bezier import inside_circle, split_bezier_intersecting_with_closedpath
+
+
+def test_split_bezier_with_large_values():
+ # These numbers come from gh-27753
+ arrow_path = [(96950809781500.0, 804.7503795623779),
+ (96950809781500.0, 859.6242585800646),
+ (96950809781500.0, 914.4981375977513)]
+ in_f = inside_circle(96950809781500.0, 804.7503795623779, 0.06)
+ split_bezier_intersecting_with_closedpath(arrow_path, in_f)
+ # All we are testing is that this completes
+ # The failure case is an infinite loop resulting from floating point precision
+ # pytest will timeout if that occurs
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_colorbar.py b/lib/python3.12/site-packages/matplotlib/tests/test_colorbar.py
new file mode 100644
index 0000000000000000000000000000000000000000..24eeab689424983c0047e95ae465a36b7a999edd
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_colorbar.py
@@ -0,0 +1,1240 @@
+import platform
+
+import numpy as np
+import pytest
+
+from matplotlib import cm
+import matplotlib.colors as mcolors
+import matplotlib as mpl
+
+
+from matplotlib import rc_context
+from matplotlib.testing.decorators import image_comparison
+import matplotlib.pyplot as plt
+from matplotlib.colors import (
+ BoundaryNorm, LogNorm, PowerNorm, Normalize, NoNorm
+)
+from matplotlib.colorbar import Colorbar
+from matplotlib.ticker import FixedLocator, LogFormatter, StrMethodFormatter
+from matplotlib.testing.decorators import check_figures_equal
+
+
+def _get_cmap_norms():
+ """
+ Define a colormap and appropriate norms for each of the four
+ possible settings of the extend keyword.
+
+ Helper function for _colorbar_extension_shape and
+ colorbar_extension_length.
+ """
+ # Create a colormap and specify the levels it represents.
+ cmap = mpl.colormaps["RdBu"].resampled(5)
+ clevs = [-5., -2.5, -.5, .5, 1.5, 3.5]
+ # Define norms for the colormaps.
+ norms = dict()
+ norms['neither'] = BoundaryNorm(clevs, len(clevs) - 1)
+ norms['min'] = BoundaryNorm([-10] + clevs[1:], len(clevs) - 1)
+ norms['max'] = BoundaryNorm(clevs[:-1] + [10], len(clevs) - 1)
+ norms['both'] = BoundaryNorm([-10] + clevs[1:-1] + [10], len(clevs) - 1)
+ return cmap, norms
+
+
+def _colorbar_extension_shape(spacing):
+ """
+ Produce 4 colorbars with rectangular extensions for either uniform
+ or proportional spacing.
+
+ Helper function for test_colorbar_extension_shape.
+ """
+ # Get a colormap and appropriate norms for each extension type.
+ cmap, norms = _get_cmap_norms()
+ # Create a figure and adjust whitespace for subplots.
+ fig = plt.figure()
+ fig.subplots_adjust(hspace=4)
+ for i, extension_type in enumerate(('neither', 'min', 'max', 'both')):
+ # Get the appropriate norm and use it to get colorbar boundaries.
+ norm = norms[extension_type]
+ boundaries = values = norm.boundaries
+ # note that the last value was silently dropped pre 3.3:
+ values = values[:-1]
+ # Create a subplot.
+ cax = fig.add_subplot(4, 1, i + 1)
+ # Generate the colorbar.
+ Colorbar(cax, cmap=cmap, norm=norm,
+ boundaries=boundaries, values=values,
+ extend=extension_type, extendrect=True,
+ orientation='horizontal', spacing=spacing)
+ # Turn off text and ticks.
+ cax.tick_params(left=False, labelleft=False,
+ bottom=False, labelbottom=False)
+ # Return the figure to the caller.
+ return fig
+
+
+def _colorbar_extension_length(spacing):
+ """
+ Produce 12 colorbars with variable length extensions for either
+ uniform or proportional spacing.
+
+ Helper function for test_colorbar_extension_length.
+ """
+ # Get a colormap and appropriate norms for each extension type.
+ cmap, norms = _get_cmap_norms()
+ # Create a figure and adjust whitespace for subplots.
+ fig = plt.figure()
+ fig.subplots_adjust(hspace=.6)
+ for i, extension_type in enumerate(('neither', 'min', 'max', 'both')):
+ # Get the appropriate norm and use it to get colorbar boundaries.
+ norm = norms[extension_type]
+ boundaries = values = norm.boundaries
+ values = values[:-1]
+ for j, extendfrac in enumerate((None, 'auto', 0.1)):
+ # Create a subplot.
+ cax = fig.add_subplot(12, 1, i*3 + j + 1)
+ # Generate the colorbar.
+ Colorbar(cax, cmap=cmap, norm=norm,
+ boundaries=boundaries, values=values,
+ extend=extension_type, extendfrac=extendfrac,
+ orientation='horizontal', spacing=spacing)
+ # Turn off text and ticks.
+ cax.tick_params(left=False, labelleft=False,
+ bottom=False, labelbottom=False)
+ # Return the figure to the caller.
+ return fig
+
+
+@image_comparison(['colorbar_extensions_shape_uniform.png',
+ 'colorbar_extensions_shape_proportional.png'])
+def test_colorbar_extension_shape():
+ """Test rectangular colorbar extensions."""
+ # Remove this line when this test image is regenerated.
+ plt.rcParams['pcolormesh.snap'] = False
+
+ # Create figures for uniform and proportionally spaced colorbars.
+ _colorbar_extension_shape('uniform')
+ _colorbar_extension_shape('proportional')
+
+
+@image_comparison(['colorbar_extensions_uniform.png',
+ 'colorbar_extensions_proportional.png'],
+ tol=1.0)
+def test_colorbar_extension_length():
+ """Test variable length colorbar extensions."""
+ # Remove this line when this test image is regenerated.
+ plt.rcParams['pcolormesh.snap'] = False
+
+ # Create figures for uniform and proportionally spaced colorbars.
+ _colorbar_extension_length('uniform')
+ _colorbar_extension_length('proportional')
+
+
+@pytest.mark.parametrize("orientation", ["horizontal", "vertical"])
+@pytest.mark.parametrize("extend,expected", [("min", (0, 0, 0, 1)),
+ ("max", (1, 1, 1, 1)),
+ ("both", (1, 1, 1, 1))])
+def test_colorbar_extension_inverted_axis(orientation, extend, expected):
+ """Test extension color with an inverted axis"""
+ data = np.arange(12).reshape(3, 4)
+ fig, ax = plt.subplots()
+ cmap = mpl.colormaps["viridis"].with_extremes(under=(0, 0, 0, 1),
+ over=(1, 1, 1, 1))
+ im = ax.imshow(data, cmap=cmap)
+ cbar = fig.colorbar(im, orientation=orientation, extend=extend)
+ if orientation == "horizontal":
+ cbar.ax.invert_xaxis()
+ else:
+ cbar.ax.invert_yaxis()
+ assert cbar._extend_patches[0].get_facecolor() == expected
+ if extend == "both":
+ assert len(cbar._extend_patches) == 2
+ assert cbar._extend_patches[1].get_facecolor() == (0, 0, 0, 1)
+ else:
+ assert len(cbar._extend_patches) == 1
+
+
+@pytest.mark.parametrize('use_gridspec', [True, False])
+@image_comparison(['cbar_with_orientation',
+ 'cbar_locationing',
+ 'double_cbar',
+ 'cbar_sharing',
+ ],
+ extensions=['png'], remove_text=True,
+ savefig_kwarg={'dpi': 40})
+def test_colorbar_positioning(use_gridspec):
+ # Remove this line when this test image is regenerated.
+ plt.rcParams['pcolormesh.snap'] = False
+
+ data = np.arange(1200).reshape(30, 40)
+ levels = [0, 200, 400, 600, 800, 1000, 1200]
+
+ # -------------------
+ plt.figure()
+ plt.contourf(data, levels=levels)
+ plt.colorbar(orientation='horizontal', use_gridspec=use_gridspec)
+
+ locations = ['left', 'right', 'top', 'bottom']
+ plt.figure()
+ for i, location in enumerate(locations):
+ plt.subplot(2, 2, i + 1)
+ plt.contourf(data, levels=levels)
+ plt.colorbar(location=location, use_gridspec=use_gridspec)
+
+ # -------------------
+ plt.figure()
+ # make some other data (random integers)
+ data_2nd = np.array([[2, 3, 2, 3], [1.5, 2, 2, 3], [2, 3, 3, 4]])
+ # make the random data expand to the shape of the main data
+ data_2nd = np.repeat(np.repeat(data_2nd, 10, axis=1), 10, axis=0)
+
+ color_mappable = plt.contourf(data, levels=levels, extend='both')
+ # test extend frac here
+ hatch_mappable = plt.contourf(data_2nd, levels=[1, 2, 3], colors='none',
+ hatches=['/', 'o', '+'], extend='max')
+ plt.contour(hatch_mappable, colors='black')
+
+ plt.colorbar(color_mappable, location='left', label='variable 1',
+ use_gridspec=use_gridspec)
+ plt.colorbar(hatch_mappable, location='right', label='variable 2',
+ use_gridspec=use_gridspec)
+
+ # -------------------
+ plt.figure()
+ ax1 = plt.subplot(211, anchor='NE', aspect='equal')
+ plt.contourf(data, levels=levels)
+ ax2 = plt.subplot(223)
+ plt.contourf(data, levels=levels)
+ ax3 = plt.subplot(224)
+ plt.contourf(data, levels=levels)
+
+ plt.colorbar(ax=[ax2, ax3, ax1], location='right', pad=0.0, shrink=0.5,
+ panchor=False, use_gridspec=use_gridspec)
+ plt.colorbar(ax=[ax2, ax3, ax1], location='left', shrink=0.5,
+ panchor=False, use_gridspec=use_gridspec)
+ plt.colorbar(ax=[ax1], location='bottom', panchor=False,
+ anchor=(0.8, 0.5), shrink=0.6, use_gridspec=use_gridspec)
+
+
+def test_colorbar_single_ax_panchor_false():
+ # Note that this differs from the tests above with panchor=False because
+ # there use_gridspec is actually ineffective: passing *ax* as lists always
+ # disables use_gridspec.
+ ax = plt.subplot(111, anchor='N')
+ plt.imshow([[0, 1]])
+ plt.colorbar(panchor=False)
+ assert ax.get_anchor() == 'N'
+
+
+@pytest.mark.parametrize('constrained', [False, True],
+ ids=['standard', 'constrained'])
+def test_colorbar_single_ax_panchor_east(constrained):
+ fig = plt.figure(constrained_layout=constrained)
+ ax = fig.add_subplot(111, anchor='N')
+ plt.imshow([[0, 1]])
+ plt.colorbar(panchor='E')
+ assert ax.get_anchor() == 'E'
+
+
+@image_comparison(['contour_colorbar.png'], remove_text=True,
+ tol=0 if platform.machine() == 'x86_64' else 0.054)
+def test_contour_colorbar():
+ fig, ax = plt.subplots(figsize=(4, 2))
+ data = np.arange(1200).reshape(30, 40) - 500
+ levels = np.array([0, 200, 400, 600, 800, 1000, 1200]) - 500
+
+ CS = ax.contour(data, levels=levels, extend='both')
+ fig.colorbar(CS, orientation='horizontal', extend='both')
+ fig.colorbar(CS, orientation='vertical')
+
+
+@image_comparison(['cbar_with_subplots_adjust.png'], remove_text=True,
+ savefig_kwarg={'dpi': 40})
+def test_gridspec_make_colorbar():
+ plt.figure()
+ data = np.arange(1200).reshape(30, 40)
+ levels = [0, 200, 400, 600, 800, 1000, 1200]
+
+ plt.subplot(121)
+ plt.contourf(data, levels=levels)
+ plt.colorbar(use_gridspec=True, orientation='vertical')
+
+ plt.subplot(122)
+ plt.contourf(data, levels=levels)
+ plt.colorbar(use_gridspec=True, orientation='horizontal')
+
+ plt.subplots_adjust(top=0.95, right=0.95, bottom=0.2, hspace=0.25)
+
+
+@image_comparison(['colorbar_single_scatter.png'], remove_text=True,
+ savefig_kwarg={'dpi': 40})
+def test_colorbar_single_scatter():
+ # Issue #2642: if a path collection has only one entry,
+ # the norm scaling within the colorbar must ensure a
+ # finite range, otherwise a zero denominator will occur in _locate.
+ plt.figure()
+ x = y = [0]
+ z = [50]
+ cmap = mpl.colormaps['jet'].resampled(16)
+ cs = plt.scatter(x, y, z, c=z, cmap=cmap)
+ plt.colorbar(cs)
+
+
+@pytest.mark.parametrize('use_gridspec', [True, False])
+@pytest.mark.parametrize('nested_gridspecs', [True, False])
+def test_remove_from_figure(nested_gridspecs, use_gridspec):
+ """Test `remove` with the specified ``use_gridspec`` setting."""
+ fig = plt.figure()
+ if nested_gridspecs:
+ gs = fig.add_gridspec(2, 2)[1, 1].subgridspec(2, 2)
+ ax = fig.add_subplot(gs[1, 1])
+ else:
+ ax = fig.add_subplot()
+ sc = ax.scatter([1, 2], [3, 4])
+ sc.set_array(np.array([5, 6]))
+ pre_position = ax.get_position()
+ cb = fig.colorbar(sc, use_gridspec=use_gridspec)
+ fig.subplots_adjust()
+ cb.remove()
+ fig.subplots_adjust()
+ post_position = ax.get_position()
+ assert (pre_position.get_points() == post_position.get_points()).all()
+
+
+def test_remove_from_figure_cl():
+ """Test `remove` with constrained_layout."""
+ fig, ax = plt.subplots(constrained_layout=True)
+ sc = ax.scatter([1, 2], [3, 4])
+ sc.set_array(np.array([5, 6]))
+ fig.draw_without_rendering()
+ pre_position = ax.get_position()
+ cb = fig.colorbar(sc)
+ cb.remove()
+ fig.draw_without_rendering()
+ post_position = ax.get_position()
+ np.testing.assert_allclose(pre_position.get_points(),
+ post_position.get_points())
+
+
+def test_colorbarbase():
+ # smoke test from #3805
+ ax = plt.gca()
+ Colorbar(ax, cmap=plt.cm.bone)
+
+
+def test_parentless_mappable():
+ pc = mpl.collections.PatchCollection([], cmap=plt.get_cmap('viridis'), array=[])
+ with pytest.raises(ValueError, match='Unable to determine Axes to steal'):
+ plt.colorbar(pc)
+
+
+@image_comparison(['colorbar_closed_patch.png'], remove_text=True)
+def test_colorbar_closed_patch():
+ # Remove this line when this test image is regenerated.
+ plt.rcParams['pcolormesh.snap'] = False
+
+ fig = plt.figure(figsize=(8, 6))
+ ax1 = fig.add_axes([0.05, 0.85, 0.9, 0.1])
+ ax2 = fig.add_axes([0.1, 0.65, 0.75, 0.1])
+ ax3 = fig.add_axes([0.05, 0.45, 0.9, 0.1])
+ ax4 = fig.add_axes([0.05, 0.25, 0.9, 0.1])
+ ax5 = fig.add_axes([0.05, 0.05, 0.9, 0.1])
+
+ cmap = mpl.colormaps["RdBu"].resampled(5)
+
+ im = ax1.pcolormesh(np.linspace(0, 10, 16).reshape((4, 4)), cmap=cmap)
+
+ # The use of a "values" kwarg here is unusual. It works only
+ # because it is matched to the data range in the image and to
+ # the number of colors in the LUT.
+ values = np.linspace(0, 10, 5)
+ cbar_kw = dict(orientation='horizontal', values=values, ticks=[])
+
+ # The wide line is to show that the closed path is being handled
+ # correctly. See PR #4186.
+ with rc_context({'axes.linewidth': 16}):
+ plt.colorbar(im, cax=ax2, extend='both', extendfrac=0.5, **cbar_kw)
+ plt.colorbar(im, cax=ax3, extend='both', **cbar_kw)
+ plt.colorbar(im, cax=ax4, extend='both', extendrect=True, **cbar_kw)
+ plt.colorbar(im, cax=ax5, extend='neither', **cbar_kw)
+
+
+def test_colorbar_ticks():
+ # test fix for #5673
+ fig, ax = plt.subplots()
+ x = np.arange(-3.0, 4.001)
+ y = np.arange(-4.0, 3.001)
+ X, Y = np.meshgrid(x, y)
+ Z = X * Y
+ clevs = np.array([-12, -5, 0, 5, 12], dtype=float)
+ colors = ['r', 'g', 'b', 'c']
+ cs = ax.contourf(X, Y, Z, clevs, colors=colors, extend='neither')
+ cbar = fig.colorbar(cs, ax=ax, orientation='horizontal', ticks=clevs)
+ assert len(cbar.ax.xaxis.get_ticklocs()) == len(clevs)
+
+
+def test_colorbar_minorticks_on_off():
+ # test for github issue #11510 and PR #11584
+ np.random.seed(seed=12345)
+ data = np.random.randn(20, 20)
+ with rc_context({'_internal.classic_mode': False}):
+ fig, ax = plt.subplots()
+ # purposefully setting vmin and vmax to odd fractions
+ # so as to check for the correct locations of the minor ticks
+ im = ax.pcolormesh(data, vmin=-2.3, vmax=3.3)
+
+ cbar = fig.colorbar(im, extend='both')
+ # testing after minorticks_on()
+ cbar.minorticks_on()
+ np.testing.assert_almost_equal(
+ cbar.ax.yaxis.get_minorticklocs(),
+ [-2.2, -1.8, -1.6, -1.4, -1.2, -0.8, -0.6, -0.4, -0.2,
+ 0.2, 0.4, 0.6, 0.8, 1.2, 1.4, 1.6, 1.8, 2.2, 2.4, 2.6, 2.8, 3.2])
+ # testing after minorticks_off()
+ cbar.minorticks_off()
+ np.testing.assert_almost_equal(cbar.ax.yaxis.get_minorticklocs(), [])
+
+ im.set_clim(vmin=-1.2, vmax=1.2)
+ cbar.minorticks_on()
+ np.testing.assert_almost_equal(
+ cbar.ax.yaxis.get_minorticklocs(),
+ [-1.2, -1.1, -0.9, -0.8, -0.7, -0.6, -0.4, -0.3, -0.2, -0.1,
+ 0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9, 1.1, 1.2])
+
+ # tests for github issue #13257 and PR #13265
+ data = np.random.uniform(low=1, high=10, size=(20, 20))
+
+ fig, ax = plt.subplots()
+ im = ax.pcolormesh(data, norm=LogNorm())
+ cbar = fig.colorbar(im)
+ fig.canvas.draw()
+ default_minorticklocks = cbar.ax.yaxis.get_minorticklocs()
+ # test that minorticks turn off for LogNorm
+ cbar.minorticks_off()
+ np.testing.assert_equal(cbar.ax.yaxis.get_minorticklocs(), [])
+
+ # test that minorticks turn back on for LogNorm
+ cbar.minorticks_on()
+ np.testing.assert_equal(cbar.ax.yaxis.get_minorticklocs(),
+ default_minorticklocks)
+
+ # test issue #13339: minorticks for LogNorm should stay off
+ cbar.minorticks_off()
+ cbar.set_ticks([3, 5, 7, 9])
+ np.testing.assert_equal(cbar.ax.yaxis.get_minorticklocs(), [])
+
+
+def test_cbar_minorticks_for_rc_xyminortickvisible():
+ """
+ issue gh-16468.
+
+ Making sure that minor ticks on the colorbar are turned on
+ (internally) using the cbar.minorticks_on() method when
+ rcParams['xtick.minor.visible'] = True (for horizontal cbar)
+ rcParams['ytick.minor.visible'] = True (for vertical cbar).
+ Using cbar.minorticks_on() ensures that the minor ticks
+ don't overflow into the extend regions of the colorbar.
+ """
+
+ plt.rcParams['ytick.minor.visible'] = True
+ plt.rcParams['xtick.minor.visible'] = True
+
+ vmin, vmax = 0.4, 2.6
+ fig, ax = plt.subplots()
+ im = ax.pcolormesh([[1, 2]], vmin=vmin, vmax=vmax)
+
+ cbar = fig.colorbar(im, extend='both', orientation='vertical')
+ assert cbar.ax.yaxis.get_minorticklocs()[0] >= vmin
+ assert cbar.ax.yaxis.get_minorticklocs()[-1] <= vmax
+
+ cbar = fig.colorbar(im, extend='both', orientation='horizontal')
+ assert cbar.ax.xaxis.get_minorticklocs()[0] >= vmin
+ assert cbar.ax.xaxis.get_minorticklocs()[-1] <= vmax
+
+
+def test_colorbar_autoticks():
+ # Test new autotick modes. Needs to be classic because
+ # non-classic doesn't go this route.
+ with rc_context({'_internal.classic_mode': False}):
+ fig, ax = plt.subplots(2, 1)
+ x = np.arange(-3.0, 4.001)
+ y = np.arange(-4.0, 3.001)
+ X, Y = np.meshgrid(x, y)
+ Z = X * Y
+ Z = Z[:-1, :-1]
+ pcm = ax[0].pcolormesh(X, Y, Z)
+ cbar = fig.colorbar(pcm, ax=ax[0], extend='both',
+ orientation='vertical')
+
+ pcm = ax[1].pcolormesh(X, Y, Z)
+ cbar2 = fig.colorbar(pcm, ax=ax[1], extend='both',
+ orientation='vertical', shrink=0.4)
+ # note only -10 to 10 are visible,
+ np.testing.assert_almost_equal(cbar.ax.yaxis.get_ticklocs(),
+ np.arange(-15, 16, 5))
+ # note only -10 to 10 are visible
+ np.testing.assert_almost_equal(cbar2.ax.yaxis.get_ticklocs(),
+ np.arange(-20, 21, 10))
+
+
+def test_colorbar_autotickslog():
+ # Test new autotick modes...
+ with rc_context({'_internal.classic_mode': False}):
+ fig, ax = plt.subplots(2, 1)
+ x = np.arange(-3.0, 4.001)
+ y = np.arange(-4.0, 3.001)
+ X, Y = np.meshgrid(x, y)
+ Z = X * Y
+ Z = Z[:-1, :-1]
+ pcm = ax[0].pcolormesh(X, Y, 10**Z, norm=LogNorm())
+ cbar = fig.colorbar(pcm, ax=ax[0], extend='both',
+ orientation='vertical')
+
+ pcm = ax[1].pcolormesh(X, Y, 10**Z, norm=LogNorm())
+ cbar2 = fig.colorbar(pcm, ax=ax[1], extend='both',
+ orientation='vertical', shrink=0.4)
+ # note only -12 to +12 are visible
+ np.testing.assert_almost_equal(cbar.ax.yaxis.get_ticklocs(),
+ 10**np.arange(-16., 16.2, 4.))
+ # note only -24 to +24 are visible
+ np.testing.assert_almost_equal(cbar2.ax.yaxis.get_ticklocs(),
+ 10**np.arange(-24., 25., 12.))
+
+
+def test_colorbar_get_ticks():
+ # test feature for #5792
+ plt.figure()
+ data = np.arange(1200).reshape(30, 40)
+ levels = [0, 200, 400, 600, 800, 1000, 1200]
+
+ plt.contourf(data, levels=levels)
+
+ # testing getter for user set ticks
+ userTicks = plt.colorbar(ticks=[0, 600, 1200])
+ assert userTicks.get_ticks().tolist() == [0, 600, 1200]
+
+ # testing for getter after calling set_ticks
+ userTicks.set_ticks([600, 700, 800])
+ assert userTicks.get_ticks().tolist() == [600, 700, 800]
+
+ # testing for getter after calling set_ticks with some ticks out of bounds
+ # removed #20054: other axes don't trim fixed lists, so colorbars
+ # should not either:
+ # userTicks.set_ticks([600, 1300, 1400, 1500])
+ # assert userTicks.get_ticks().tolist() == [600]
+
+ # testing getter when no ticks are assigned
+ defTicks = plt.colorbar(orientation='horizontal')
+ np.testing.assert_allclose(defTicks.get_ticks().tolist(), levels)
+
+ # test normal ticks and minor ticks
+ fig, ax = plt.subplots()
+ x = np.arange(-3.0, 4.001)
+ y = np.arange(-4.0, 3.001)
+ X, Y = np.meshgrid(x, y)
+ Z = X * Y
+ Z = Z[:-1, :-1]
+ pcm = ax.pcolormesh(X, Y, Z)
+ cbar = fig.colorbar(pcm, ax=ax, extend='both',
+ orientation='vertical')
+ ticks = cbar.get_ticks()
+ np.testing.assert_allclose(ticks, np.arange(-15, 16, 5))
+ assert len(cbar.get_ticks(minor=True)) == 0
+
+
+@pytest.mark.parametrize("extend", ['both', 'min', 'max'])
+def test_colorbar_lognorm_extension(extend):
+ # Test that colorbar with lognorm is extended correctly
+ f, ax = plt.subplots()
+ cb = Colorbar(ax, norm=LogNorm(vmin=0.1, vmax=1000.0),
+ orientation='vertical', extend=extend)
+ assert cb._values[0] >= 0.0
+
+
+def test_colorbar_powernorm_extension():
+ # Test that colorbar with powernorm is extended correctly
+ # Just a smoke test that adding the colorbar doesn't raise an error or warning
+ fig, ax = plt.subplots()
+ Colorbar(ax, norm=PowerNorm(gamma=0.5, vmin=0.0, vmax=1.0),
+ orientation='vertical', extend='both')
+
+
+def test_colorbar_axes_kw():
+ # test fix for #8493: This does only test, that axes-related keywords pass
+ # and do not raise an exception.
+ plt.figure()
+ plt.imshow([[1, 2], [3, 4]])
+ plt.colorbar(orientation='horizontal', fraction=0.2, pad=0.2, shrink=0.5,
+ aspect=10, anchor=(0., 0.), panchor=(0., 1.))
+
+
+def test_colorbar_log_minortick_labels():
+ with rc_context({'_internal.classic_mode': False}):
+ fig, ax = plt.subplots()
+ pcm = ax.imshow([[10000, 50000]], norm=LogNorm())
+ cb = fig.colorbar(pcm)
+ fig.canvas.draw()
+ lb = [l.get_text() for l in cb.ax.yaxis.get_ticklabels(which='both')]
+ expected = [r'$\mathdefault{10^{4}}$',
+ r'$\mathdefault{2\times10^{4}}$',
+ r'$\mathdefault{3\times10^{4}}$',
+ r'$\mathdefault{4\times10^{4}}$']
+ for exp in expected:
+ assert exp in lb
+
+
+def test_colorbar_renorm():
+ x, y = np.ogrid[-4:4:31j, -4:4:31j]
+ z = 120000*np.exp(-x**2 - y**2)
+
+ fig, ax = plt.subplots()
+ im = ax.imshow(z)
+ cbar = fig.colorbar(im)
+ np.testing.assert_allclose(cbar.ax.yaxis.get_majorticklocs(),
+ np.arange(0, 120000.1, 20000))
+
+ cbar.set_ticks([1, 2, 3])
+ assert isinstance(cbar.locator, FixedLocator)
+
+ norm = LogNorm(z.min(), z.max())
+ im.set_norm(norm)
+ np.testing.assert_allclose(cbar.ax.yaxis.get_majorticklocs(),
+ np.logspace(-10, 7, 18))
+ # note that set_norm removes the FixedLocator...
+ assert np.isclose(cbar.vmin, z.min())
+ cbar.set_ticks([1, 2, 3])
+ assert isinstance(cbar.locator, FixedLocator)
+ np.testing.assert_allclose(cbar.ax.yaxis.get_majorticklocs(),
+ [1.0, 2.0, 3.0])
+
+ norm = LogNorm(z.min() * 1000, z.max() * 1000)
+ im.set_norm(norm)
+ assert np.isclose(cbar.vmin, z.min() * 1000)
+ assert np.isclose(cbar.vmax, z.max() * 1000)
+
+
+@pytest.mark.parametrize('fmt', ['%4.2e', '{x:.2e}'])
+def test_colorbar_format(fmt):
+ # make sure that format is passed properly
+ x, y = np.ogrid[-4:4:31j, -4:4:31j]
+ z = 120000*np.exp(-x**2 - y**2)
+
+ fig, ax = plt.subplots()
+ im = ax.imshow(z)
+ cbar = fig.colorbar(im, format=fmt)
+ fig.canvas.draw()
+ assert cbar.ax.yaxis.get_ticklabels()[4].get_text() == '8.00e+04'
+
+ # make sure that if we change the clim of the mappable that the
+ # formatting is *not* lost:
+ im.set_clim([4, 200])
+ fig.canvas.draw()
+ assert cbar.ax.yaxis.get_ticklabels()[4].get_text() == '2.00e+02'
+
+ # but if we change the norm:
+ im.set_norm(LogNorm(vmin=0.1, vmax=10))
+ fig.canvas.draw()
+ assert (cbar.ax.yaxis.get_ticklabels()[0].get_text() ==
+ '$\\mathdefault{10^{-2}}$')
+
+
+def test_colorbar_scale_reset():
+ x, y = np.ogrid[-4:4:31j, -4:4:31j]
+ z = 120000*np.exp(-x**2 - y**2)
+
+ fig, ax = plt.subplots()
+ pcm = ax.pcolormesh(z, cmap='RdBu_r', rasterized=True)
+ cbar = fig.colorbar(pcm, ax=ax)
+ cbar.outline.set_edgecolor('red')
+ assert cbar.ax.yaxis.get_scale() == 'linear'
+
+ pcm.set_norm(LogNorm(vmin=1, vmax=100))
+ assert cbar.ax.yaxis.get_scale() == 'log'
+ pcm.set_norm(Normalize(vmin=-20, vmax=20))
+ assert cbar.ax.yaxis.get_scale() == 'linear'
+
+ assert cbar.outline.get_edgecolor() == mcolors.to_rgba('red')
+
+ # log scale with no vmin/vmax set should scale to the data if there
+ # is a mappable already associated with the colorbar, not (0, 1)
+ pcm.norm = LogNorm()
+ assert pcm.norm.vmin == z.min()
+ assert pcm.norm.vmax == z.max()
+
+
+def test_colorbar_get_ticks_2():
+ plt.rcParams['_internal.classic_mode'] = False
+ fig, ax = plt.subplots()
+ pc = ax.pcolormesh([[.05, .95]])
+ cb = fig.colorbar(pc)
+ np.testing.assert_allclose(cb.get_ticks(), [0., 0.2, 0.4, 0.6, 0.8, 1.0])
+
+
+def test_colorbar_inverted_ticks():
+ fig, axs = plt.subplots(2)
+ ax = axs[0]
+ pc = ax.pcolormesh(10**np.arange(1, 5).reshape(2, 2), norm=LogNorm())
+ cbar = fig.colorbar(pc, ax=ax, extend='both')
+ ticks = cbar.get_ticks()
+ cbar.ax.invert_yaxis()
+ np.testing.assert_allclose(ticks, cbar.get_ticks())
+
+ ax = axs[1]
+ pc = ax.pcolormesh(np.arange(1, 5).reshape(2, 2))
+ cbar = fig.colorbar(pc, ax=ax, extend='both')
+ cbar.minorticks_on()
+ ticks = cbar.get_ticks()
+ minorticks = cbar.get_ticks(minor=True)
+ assert isinstance(minorticks, np.ndarray)
+ cbar.ax.invert_yaxis()
+ np.testing.assert_allclose(ticks, cbar.get_ticks())
+ np.testing.assert_allclose(minorticks, cbar.get_ticks(minor=True))
+
+
+def test_mappable_no_alpha():
+ fig, ax = plt.subplots()
+ sm = cm.ScalarMappable(norm=mcolors.Normalize(), cmap='viridis')
+ fig.colorbar(sm, ax=ax)
+ sm.set_cmap('plasma')
+ plt.draw()
+
+
+def test_mappable_2d_alpha():
+ fig, ax = plt.subplots()
+ x = np.arange(1, 5).reshape(2, 2)/4
+ pc = ax.pcolormesh(x, alpha=x)
+ cb = fig.colorbar(pc, ax=ax)
+ # The colorbar's alpha should be None and the mappable should still have
+ # the original alpha array
+ assert cb.alpha is None
+ assert pc.get_alpha() is x
+ fig.draw_without_rendering()
+
+
+def test_colorbar_label():
+ """
+ Test the label parameter. It should just be mapped to the xlabel/ylabel of
+ the axes, depending on the orientation.
+ """
+ fig, ax = plt.subplots()
+ im = ax.imshow([[1, 2], [3, 4]])
+ cbar = fig.colorbar(im, label='cbar')
+ assert cbar.ax.get_ylabel() == 'cbar'
+ cbar.set_label(None)
+ assert cbar.ax.get_ylabel() == ''
+ cbar.set_label('cbar 2')
+ assert cbar.ax.get_ylabel() == 'cbar 2'
+
+ cbar2 = fig.colorbar(im, label=None)
+ assert cbar2.ax.get_ylabel() == ''
+
+ cbar3 = fig.colorbar(im, orientation='horizontal', label='horizontal cbar')
+ assert cbar3.ax.get_xlabel() == 'horizontal cbar'
+
+
+@image_comparison(['colorbar_keeping_xlabel.png'], style='mpl20')
+def test_keeping_xlabel():
+ # github issue #23398 - xlabels being ignored in colorbar axis
+ arr = np.arange(25).reshape((5, 5))
+ fig, ax = plt.subplots()
+ im = ax.imshow(arr)
+ cbar = plt.colorbar(im)
+ cbar.ax.set_xlabel('Visible Xlabel')
+ cbar.set_label('YLabel')
+
+
+@pytest.mark.parametrize("clim", [(-20000, 20000), (-32768, 0)])
+def test_colorbar_int(clim):
+ # Check that we cast to float early enough to not
+ # overflow ``int16(20000) - int16(-20000)`` or
+ # run into ``abs(int16(-32768)) == -32768``.
+ fig, ax = plt.subplots()
+ im = ax.imshow([[*map(np.int16, clim)]])
+ fig.colorbar(im)
+ assert (im.norm.vmin, im.norm.vmax) == clim
+
+
+def test_anchored_cbar_position_using_specgrid():
+ data = np.arange(1200).reshape(30, 40)
+ levels = [0, 200, 400, 600, 800, 1000, 1200]
+ shrink = 0.5
+ anchor_y = 0.3
+ # right
+ fig, ax = plt.subplots()
+ cs = ax.contourf(data, levels=levels)
+ cbar = plt.colorbar(
+ cs, ax=ax, use_gridspec=True,
+ location='right', anchor=(1, anchor_y), shrink=shrink)
+
+ # the bottom left corner of one ax is (x0, y0)
+ # the top right corner of one ax is (x1, y1)
+ # p0: the vertical / horizontal position of anchor
+ x0, y0, x1, y1 = ax.get_position().extents
+ cx0, cy0, cx1, cy1 = cbar.ax.get_position().extents
+ p0 = (y1 - y0) * anchor_y + y0
+
+ np.testing.assert_allclose(
+ [cy1, cy0],
+ [y1 * shrink + (1 - shrink) * p0, p0 * (1 - shrink) + y0 * shrink])
+
+ # left
+ fig, ax = plt.subplots()
+ cs = ax.contourf(data, levels=levels)
+ cbar = plt.colorbar(
+ cs, ax=ax, use_gridspec=True,
+ location='left', anchor=(1, anchor_y), shrink=shrink)
+
+ # the bottom left corner of one ax is (x0, y0)
+ # the top right corner of one ax is (x1, y1)
+ # p0: the vertical / horizontal position of anchor
+ x0, y0, x1, y1 = ax.get_position().extents
+ cx0, cy0, cx1, cy1 = cbar.ax.get_position().extents
+ p0 = (y1 - y0) * anchor_y + y0
+
+ np.testing.assert_allclose(
+ [cy1, cy0],
+ [y1 * shrink + (1 - shrink) * p0, p0 * (1 - shrink) + y0 * shrink])
+
+ # top
+ shrink = 0.5
+ anchor_x = 0.3
+ fig, ax = plt.subplots()
+ cs = ax.contourf(data, levels=levels)
+ cbar = plt.colorbar(
+ cs, ax=ax, use_gridspec=True,
+ location='top', anchor=(anchor_x, 1), shrink=shrink)
+
+ # the bottom left corner of one ax is (x0, y0)
+ # the top right corner of one ax is (x1, y1)
+ # p0: the vertical / horizontal position of anchor
+ x0, y0, x1, y1 = ax.get_position().extents
+ cx0, cy0, cx1, cy1 = cbar.ax.get_position().extents
+ p0 = (x1 - x0) * anchor_x + x0
+
+ np.testing.assert_allclose(
+ [cx1, cx0],
+ [x1 * shrink + (1 - shrink) * p0, p0 * (1 - shrink) + x0 * shrink])
+
+ # bottom
+ shrink = 0.5
+ anchor_x = 0.3
+ fig, ax = plt.subplots()
+ cs = ax.contourf(data, levels=levels)
+ cbar = plt.colorbar(
+ cs, ax=ax, use_gridspec=True,
+ location='bottom', anchor=(anchor_x, 1), shrink=shrink)
+
+ # the bottom left corner of one ax is (x0, y0)
+ # the top right corner of one ax is (x1, y1)
+ # p0: the vertical / horizontal position of anchor
+ x0, y0, x1, y1 = ax.get_position().extents
+ cx0, cy0, cx1, cy1 = cbar.ax.get_position().extents
+ p0 = (x1 - x0) * anchor_x + x0
+
+ np.testing.assert_allclose(
+ [cx1, cx0],
+ [x1 * shrink + (1 - shrink) * p0, p0 * (1 - shrink) + x0 * shrink])
+
+
+@image_comparison(['colorbar_change_lim_scale.png'], remove_text=True,
+ style='mpl20')
+def test_colorbar_change_lim_scale():
+ fig, ax = plt.subplots(1, 2, constrained_layout=True)
+ pc = ax[0].pcolormesh(np.arange(100).reshape(10, 10)+1)
+ cb = fig.colorbar(pc, ax=ax[0], extend='both')
+ cb.ax.set_yscale('log')
+
+ pc = ax[1].pcolormesh(np.arange(100).reshape(10, 10)+1)
+ cb = fig.colorbar(pc, ax=ax[1], extend='both')
+ cb.ax.set_ylim([20, 90])
+
+
+@check_figures_equal(extensions=["png"])
+def test_axes_handles_same_functions(fig_ref, fig_test):
+ # prove that cax and cb.ax are functionally the same
+ for nn, fig in enumerate([fig_ref, fig_test]):
+ ax = fig.add_subplot()
+ pc = ax.pcolormesh(np.ones(300).reshape(10, 30))
+ cax = fig.add_axes([0.9, 0.1, 0.03, 0.8])
+ cb = fig.colorbar(pc, cax=cax)
+ if nn == 0:
+ caxx = cax
+ else:
+ caxx = cb.ax
+ caxx.set_yticks(np.arange(0, 20))
+ caxx.set_yscale('log')
+ caxx.set_position([0.92, 0.1, 0.02, 0.7])
+
+
+def test_inset_colorbar_layout():
+ fig, ax = plt.subplots(constrained_layout=True, figsize=(3, 6))
+ pc = ax.imshow(np.arange(100).reshape(10, 10))
+ cax = ax.inset_axes([1.02, 0.1, 0.03, 0.8])
+ cb = fig.colorbar(pc, cax=cax)
+
+ fig.draw_without_rendering()
+ # make sure this is in the figure. In the colorbar swapping
+ # it was being dropped from the list of children...
+ np.testing.assert_allclose(cb.ax.get_position().bounds,
+ [0.87, 0.342, 0.0237, 0.315], atol=0.01)
+ assert cb.ax in ax.child_axes
+
+
+@image_comparison(['colorbar_twoslope.png'], remove_text=True,
+ style='mpl20')
+def test_twoslope_colorbar():
+ # Note that the second tick = 20, and should be in the middle
+ # of the colorbar (white)
+ # There should be no tick right at the bottom, nor at the top.
+ fig, ax = plt.subplots()
+
+ norm = mcolors.TwoSlopeNorm(20, 5, 95)
+ pc = ax.pcolormesh(np.arange(1, 11), np.arange(1, 11),
+ np.arange(100).reshape(10, 10),
+ norm=norm, cmap='RdBu_r')
+ fig.colorbar(pc)
+
+
+@check_figures_equal(extensions=["png"])
+def test_remove_cb_whose_mappable_has_no_figure(fig_ref, fig_test):
+ ax = fig_test.add_subplot()
+ cb = fig_test.colorbar(cm.ScalarMappable(), cax=ax)
+ cb.remove()
+
+
+def test_aspects():
+ fig, ax = plt.subplots(3, 2, figsize=(8, 8))
+ aspects = [20, 20, 10]
+ extends = ['neither', 'both', 'both']
+ cb = [[None, None, None], [None, None, None]]
+ for nn, orient in enumerate(['vertical', 'horizontal']):
+ for mm, (aspect, extend) in enumerate(zip(aspects, extends)):
+ pc = ax[mm, nn].pcolormesh(np.arange(100).reshape(10, 10))
+ cb[nn][mm] = fig.colorbar(pc, ax=ax[mm, nn], orientation=orient,
+ aspect=aspect, extend=extend)
+ fig.draw_without_rendering()
+ # check the extends are right ratio:
+ np.testing.assert_almost_equal(cb[0][1].ax.get_position().height,
+ cb[0][0].ax.get_position().height * 0.9,
+ decimal=2)
+ # horizontal
+ np.testing.assert_almost_equal(cb[1][1].ax.get_position().width,
+ cb[1][0].ax.get_position().width * 0.9,
+ decimal=2)
+ # check correct aspect:
+ pos = cb[0][0].ax.get_position(original=False)
+ np.testing.assert_almost_equal(pos.height, pos.width * 20, decimal=2)
+ pos = cb[1][0].ax.get_position(original=False)
+ np.testing.assert_almost_equal(pos.height * 20, pos.width, decimal=2)
+ # check twice as wide if aspect is 10 instead of 20
+ np.testing.assert_almost_equal(
+ cb[0][0].ax.get_position(original=False).width * 2,
+ cb[0][2].ax.get_position(original=False).width, decimal=2)
+ np.testing.assert_almost_equal(
+ cb[1][0].ax.get_position(original=False).height * 2,
+ cb[1][2].ax.get_position(original=False).height, decimal=2)
+
+
+@image_comparison(['proportional_colorbars.png'], remove_text=True,
+ style='mpl20')
+def test_proportional_colorbars():
+
+ x = y = np.arange(-3.0, 3.01, 0.025)
+ X, Y = np.meshgrid(x, y)
+ Z1 = np.exp(-X**2 - Y**2)
+ Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
+ Z = (Z1 - Z2) * 2
+
+ levels = [-1.25, -0.5, -0.125, 0.125, 0.5, 1.25]
+ cmap = mcolors.ListedColormap(
+ ['0.3', '0.5', 'white', 'lightblue', 'steelblue'])
+ cmap.set_under('darkred')
+ cmap.set_over('crimson')
+ norm = mcolors.BoundaryNorm(levels, cmap.N)
+
+ extends = ['neither', 'both']
+ spacings = ['uniform', 'proportional']
+ fig, axs = plt.subplots(2, 2)
+ for i in range(2):
+ for j in range(2):
+ CS3 = axs[i, j].contourf(X, Y, Z, levels, cmap=cmap, norm=norm,
+ extend=extends[i])
+ fig.colorbar(CS3, spacing=spacings[j], ax=axs[i, j])
+
+
+@image_comparison(['extend_drawedges.png'], remove_text=True, style='mpl20')
+def test_colorbar_extend_drawedges():
+ params = [
+ ('both', 1, [[[1.1, 0], [1.1, 1]],
+ [[2, 0], [2, 1]],
+ [[2.9, 0], [2.9, 1]]]),
+ ('min', 0, [[[1.1, 0], [1.1, 1]],
+ [[2, 0], [2, 1]]]),
+ ('max', 0, [[[2, 0], [2, 1]],
+ [[2.9, 0], [2.9, 1]]]),
+ ('neither', -1, [[[2, 0], [2, 1]]]),
+ ]
+
+ plt.rcParams['axes.linewidth'] = 2
+
+ fig = plt.figure(figsize=(10, 4))
+ subfigs = fig.subfigures(1, 2)
+
+ for orientation, subfig in zip(['horizontal', 'vertical'], subfigs):
+ if orientation == 'horizontal':
+ axs = subfig.subplots(4, 1)
+ else:
+ axs = subfig.subplots(1, 4)
+ fig.subplots_adjust(left=0.05, bottom=0.05, right=0.95, top=0.95)
+
+ for ax, (extend, coloroffset, res) in zip(axs, params):
+ cmap = mpl.colormaps["viridis"]
+ bounds = np.arange(5)
+ nb_colors = len(bounds) + coloroffset
+ colors = cmap(np.linspace(100, 255, nb_colors).astype(int))
+ cmap, norm = mcolors.from_levels_and_colors(bounds, colors,
+ extend=extend)
+
+ cbar = Colorbar(ax, cmap=cmap, norm=norm, orientation=orientation,
+ drawedges=True)
+ # Set limits such that only two colours are visible, and the
+ # dividers would be outside the Axes, to ensure that a) they are
+ # not drawn outside, and b) a divider still appears between the
+ # main colour and the extension.
+ if orientation == 'horizontal':
+ ax.set_xlim(1.1, 2.9)
+ else:
+ ax.set_ylim(1.1, 2.9)
+ res = np.array(res)[:, :, [1, 0]]
+ np.testing.assert_array_equal(cbar.dividers.get_segments(), res)
+
+
+@image_comparison(['contourf_extend_patches.png'], remove_text=True,
+ style='mpl20')
+def test_colorbar_contourf_extend_patches():
+ params = [
+ ('both', 5, ['\\', '//']),
+ ('min', 7, ['+']),
+ ('max', 2, ['|', '-', '/', '\\', '//']),
+ ('neither', 10, ['//', '\\', '||']),
+ ]
+
+ plt.rcParams['axes.linewidth'] = 2
+
+ fig = plt.figure(figsize=(10, 4))
+ subfigs = fig.subfigures(1, 2)
+ fig.subplots_adjust(left=0.05, bottom=0.05, right=0.95, top=0.95)
+
+ x = np.linspace(-2, 3, 50)
+ y = np.linspace(-2, 3, 30)
+ z = np.cos(x[np.newaxis, :]) + np.sin(y[:, np.newaxis])
+
+ cmap = mpl.colormaps["viridis"]
+ for orientation, subfig in zip(['horizontal', 'vertical'], subfigs):
+ axs = subfig.subplots(2, 2).ravel()
+ for ax, (extend, levels, hatches) in zip(axs, params):
+ cs = ax.contourf(x, y, z, levels, hatches=hatches,
+ cmap=cmap, extend=extend)
+ subfig.colorbar(cs, ax=ax, orientation=orientation, fraction=0.4,
+ extendfrac=0.2, aspect=5)
+
+
+def test_negative_boundarynorm():
+ fig, ax = plt.subplots(figsize=(1, 3))
+ cmap = mpl.colormaps["viridis"]
+
+ clevs = np.arange(-94, -85)
+ norm = BoundaryNorm(clevs, cmap.N)
+ cb = fig.colorbar(cm.ScalarMappable(cmap=cmap, norm=norm), cax=ax)
+ np.testing.assert_allclose(cb.ax.get_ylim(), [clevs[0], clevs[-1]])
+ np.testing.assert_allclose(cb.ax.get_yticks(), clevs)
+
+ clevs = np.arange(85, 94)
+ norm = BoundaryNorm(clevs, cmap.N)
+ cb = fig.colorbar(cm.ScalarMappable(cmap=cmap, norm=norm), cax=ax)
+ np.testing.assert_allclose(cb.ax.get_ylim(), [clevs[0], clevs[-1]])
+ np.testing.assert_allclose(cb.ax.get_yticks(), clevs)
+
+ clevs = np.arange(-3, 3)
+ norm = BoundaryNorm(clevs, cmap.N)
+ cb = fig.colorbar(cm.ScalarMappable(cmap=cmap, norm=norm), cax=ax)
+ np.testing.assert_allclose(cb.ax.get_ylim(), [clevs[0], clevs[-1]])
+ np.testing.assert_allclose(cb.ax.get_yticks(), clevs)
+
+ clevs = np.arange(-8, 1)
+ norm = BoundaryNorm(clevs, cmap.N)
+ cb = fig.colorbar(cm.ScalarMappable(cmap=cmap, norm=norm), cax=ax)
+ np.testing.assert_allclose(cb.ax.get_ylim(), [clevs[0], clevs[-1]])
+ np.testing.assert_allclose(cb.ax.get_yticks(), clevs)
+
+
+def test_centerednorm():
+ # Test default centered norm gets expanded with non-singular limits
+ # when plot data is all equal (autoscale halfrange == 0)
+ fig, ax = plt.subplots(figsize=(1, 3))
+
+ norm = mcolors.CenteredNorm()
+ mappable = ax.pcolormesh(np.zeros((3, 3)), norm=norm)
+ fig.colorbar(mappable)
+ assert (norm.vmin, norm.vmax) == (-0.1, 0.1)
+
+
+@image_comparison(['nonorm_colorbars.svg'], style='mpl20')
+def test_nonorm():
+ plt.rcParams['svg.fonttype'] = 'none'
+ data = [1, 2, 3, 4, 5]
+
+ fig, ax = plt.subplots(figsize=(6, 1))
+ fig.subplots_adjust(bottom=0.5)
+
+ norm = NoNorm(vmin=min(data), vmax=max(data))
+ cmap = mpl.colormaps["viridis"].resampled(len(data))
+ mappable = cm.ScalarMappable(norm=norm, cmap=cmap)
+ cbar = fig.colorbar(mappable, cax=ax, orientation="horizontal")
+
+
+@image_comparison(['test_boundaries.png'], remove_text=True,
+ style='mpl20')
+def test_boundaries():
+ np.random.seed(seed=19680808)
+ fig, ax = plt.subplots(figsize=(2, 2))
+ pc = ax.pcolormesh(np.random.randn(10, 10), cmap='RdBu_r')
+ cb = fig.colorbar(pc, ax=ax, boundaries=np.linspace(-3, 3, 7))
+
+
+def test_colorbar_no_warning_rcparams_grid_true():
+ # github issue #21723 - If mpl style has 'axes.grid' = True,
+ # fig.colorbar raises a warning about Auto-removal of grids
+ # by pcolor() and pcolormesh(). This is fixed by PR #22216.
+ plt.rcParams['axes.grid'] = True
+ fig, ax = plt.subplots()
+ ax.grid(False)
+ im = ax.pcolormesh([0, 1], [0, 1], [[1]])
+ # make sure that no warning is raised by fig.colorbar
+ fig.colorbar(im)
+
+
+def test_colorbar_set_formatter_locator():
+ # check that the locator properties echo what is on the axis:
+ fig, ax = plt.subplots()
+ pc = ax.pcolormesh(np.random.randn(10, 10))
+ cb = fig.colorbar(pc)
+ cb.ax.yaxis.set_major_locator(FixedLocator(np.arange(10)))
+ cb.ax.yaxis.set_minor_locator(FixedLocator(np.arange(0, 10, 0.2)))
+ assert cb.locator is cb.ax.yaxis.get_major_locator()
+ assert cb.minorlocator is cb.ax.yaxis.get_minor_locator()
+ cb.ax.yaxis.set_major_formatter(LogFormatter())
+ cb.ax.yaxis.set_minor_formatter(LogFormatter())
+ assert cb.formatter is cb.ax.yaxis.get_major_formatter()
+ assert cb.minorformatter is cb.ax.yaxis.get_minor_formatter()
+
+ # check that the setter works as expected:
+ loc = FixedLocator(np.arange(7))
+ cb.locator = loc
+ assert cb.ax.yaxis.get_major_locator() is loc
+ loc = FixedLocator(np.arange(0, 7, 0.1))
+ cb.minorlocator = loc
+ assert cb.ax.yaxis.get_minor_locator() is loc
+ fmt = LogFormatter()
+ cb.formatter = fmt
+ assert cb.ax.yaxis.get_major_formatter() is fmt
+ fmt = LogFormatter()
+ cb.minorformatter = fmt
+ assert cb.ax.yaxis.get_minor_formatter() is fmt
+ assert cb.long_axis is cb.ax.yaxis
+
+
+@image_comparison(['colorbar_extend_alpha.png'], remove_text=True,
+ savefig_kwarg={'dpi': 40})
+def test_colorbar_extend_alpha():
+ fig, ax = plt.subplots()
+ im = ax.imshow([[0, 1], [2, 3]], alpha=0.3, interpolation="none")
+ fig.colorbar(im, extend='both', boundaries=[0.5, 1.5, 2.5])
+
+
+def test_offset_text_loc():
+ plt.style.use('mpl20')
+ fig, ax = plt.subplots()
+ np.random.seed(seed=19680808)
+ pc = ax.pcolormesh(np.random.randn(10, 10)*1e6)
+ cb = fig.colorbar(pc, location='right', extend='max')
+ fig.draw_without_rendering()
+ # check that the offsetText is in the proper place above the
+ # colorbar axes. In this case the colorbar axes is the same
+ # height as the parent, so use the parents bbox.
+ assert cb.ax.yaxis.offsetText.get_position()[1] > ax.bbox.y1
+
+
+def test_title_text_loc():
+ plt.style.use('mpl20')
+ fig, ax = plt.subplots()
+ np.random.seed(seed=19680808)
+ pc = ax.pcolormesh(np.random.randn(10, 10))
+ cb = fig.colorbar(pc, location='right', extend='max')
+ cb.ax.set_title('Aardvark')
+ fig.draw_without_rendering()
+ # check that the title is in the proper place above the
+ # colorbar axes, including its extend triangles....
+ assert (cb.ax.title.get_window_extent(fig.canvas.get_renderer()).ymax >
+ cb.ax.spines['outline'].get_window_extent().ymax)
+
+
+@check_figures_equal(extensions=["png"])
+def test_passing_location(fig_ref, fig_test):
+ ax_ref = fig_ref.add_subplot()
+ im = ax_ref.imshow([[0, 1], [2, 3]])
+ ax_ref.get_figure().colorbar(im, cax=ax_ref.inset_axes([0, 1.05, 1, 0.05]),
+ orientation="horizontal", ticklocation="top")
+ ax_test = fig_test.add_subplot()
+ im = ax_test.imshow([[0, 1], [2, 3]])
+ ax_test.get_figure().colorbar(im, cax=ax_test.inset_axes([0, 1.05, 1, 0.05]),
+ location="top")
+
+
+@pytest.mark.parametrize("kwargs,error,message", [
+ ({'location': 'top', 'orientation': 'vertical'}, TypeError,
+ "location and orientation are mutually exclusive"),
+ ({'location': 'top', 'orientation': 'vertical', 'cax': True}, TypeError,
+ "location and orientation are mutually exclusive"), # Different to above
+ ({'ticklocation': 'top', 'orientation': 'vertical', 'cax': True},
+ ValueError, "'top' is not a valid value for position"),
+ ({'location': 'top', 'extendfrac': (0, None)}, ValueError,
+ "invalid value for extendfrac"),
+ ])
+def test_colorbar_errors(kwargs, error, message):
+ fig, ax = plt.subplots()
+ im = ax.imshow([[0, 1], [2, 3]])
+ if kwargs.get('cax', None) is True:
+ kwargs['cax'] = ax.inset_axes([0, 1.05, 1, 0.05])
+ with pytest.raises(error, match=message):
+ fig.colorbar(im, **kwargs)
+
+
+def test_colorbar_axes_parmeters():
+ fig, ax = plt.subplots(2)
+ im = ax[0].imshow([[0, 1], [2, 3]])
+ # colorbar should accept any form of axes sequence:
+ fig.colorbar(im, ax=ax)
+ fig.colorbar(im, ax=ax[0])
+ fig.colorbar(im, ax=[_ax for _ax in ax])
+ fig.colorbar(im, ax=(ax[0], ax[1]))
+ fig.colorbar(im, ax={i: _ax for i, _ax in enumerate(ax)}.values())
+ fig.draw_without_rendering()
+
+
+def test_colorbar_wrong_figure():
+ # If we decide in the future to disallow calling colorbar() on the "wrong" figure,
+ # just delete this test.
+ fig_tl = plt.figure(layout="tight")
+ fig_cl = plt.figure(layout="constrained")
+ im = fig_cl.add_subplot().imshow([[0, 1]])
+ # Make sure this doesn't try to setup a gridspec-controlled colorbar on fig_cl,
+ # which would crash CL.
+ with pytest.warns(UserWarning, match="different Figure"):
+ fig_tl.colorbar(im)
+ fig_tl.draw_without_rendering()
+ fig_cl.draw_without_rendering()
+
+
+def test_colorbar_format_string_and_old():
+ plt.imshow([[0, 1]])
+ cb = plt.colorbar(format="{x}%")
+ assert isinstance(cb._formatter, StrMethodFormatter)
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_colors.py b/lib/python3.12/site-packages/matplotlib/tests/test_colors.py
new file mode 100644
index 0000000000000000000000000000000000000000..7da4bab69c159a95eb36c784374b2951e7d1ab09
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_colors.py
@@ -0,0 +1,1747 @@
+import copy
+import itertools
+import unittest.mock
+
+from io import BytesIO
+import numpy as np
+from PIL import Image
+import pytest
+import base64
+
+from numpy.testing import assert_array_equal, assert_array_almost_equal
+
+from matplotlib import cbook, cm
+import matplotlib
+import matplotlib as mpl
+import matplotlib.colors as mcolors
+import matplotlib.colorbar as mcolorbar
+import matplotlib.colorizer as mcolorizer
+import matplotlib.pyplot as plt
+import matplotlib.scale as mscale
+from matplotlib.rcsetup import cycler
+from matplotlib.testing.decorators import image_comparison, check_figures_equal
+from matplotlib.colors import is_color_like, to_rgba_array
+
+
+@pytest.mark.parametrize('N, result', [
+ (5, [1, .6, .2, .1, 0]),
+ (2, [1, 0]),
+ (1, [0]),
+])
+def test_create_lookup_table(N, result):
+ data = [(0.0, 1.0, 1.0), (0.5, 0.2, 0.2), (1.0, 0.0, 0.0)]
+ assert_array_almost_equal(mcolors._create_lookup_table(N, data), result)
+
+
+@pytest.mark.parametrize("dtype", [np.uint8, int, np.float16, float])
+def test_index_dtype(dtype):
+ # We use subtraction in the indexing, so need to verify that uint8 works
+ cm = mpl.colormaps["viridis"]
+ assert_array_equal(cm(dtype(0)), cm(0))
+
+
+def test_resampled():
+ """
+ GitHub issue #6025 pointed to incorrect ListedColormap.resampled;
+ here we test the method for LinearSegmentedColormap as well.
+ """
+ n = 101
+ colorlist = np.empty((n, 4), float)
+ colorlist[:, 0] = np.linspace(0, 1, n)
+ colorlist[:, 1] = 0.2
+ colorlist[:, 2] = np.linspace(1, 0, n)
+ colorlist[:, 3] = 0.7
+ lsc = mcolors.LinearSegmentedColormap.from_list('lsc', colorlist)
+ lc = mcolors.ListedColormap(colorlist)
+ # Set some bad values for testing too
+ for cmap in [lsc, lc]:
+ cmap.set_under('r')
+ cmap.set_over('g')
+ cmap.set_bad('b')
+ lsc3 = lsc.resampled(3)
+ lc3 = lc.resampled(3)
+ expected = np.array([[0.0, 0.2, 1.0, 0.7],
+ [0.5, 0.2, 0.5, 0.7],
+ [1.0, 0.2, 0.0, 0.7]], float)
+ assert_array_almost_equal(lsc3([0, 0.5, 1]), expected)
+ assert_array_almost_equal(lc3([0, 0.5, 1]), expected)
+ # Test over/under was copied properly
+ assert_array_almost_equal(lsc(np.inf), lsc3(np.inf))
+ assert_array_almost_equal(lsc(-np.inf), lsc3(-np.inf))
+ assert_array_almost_equal(lsc(np.nan), lsc3(np.nan))
+ assert_array_almost_equal(lc(np.inf), lc3(np.inf))
+ assert_array_almost_equal(lc(-np.inf), lc3(-np.inf))
+ assert_array_almost_equal(lc(np.nan), lc3(np.nan))
+
+
+def test_colormaps_get_cmap():
+ cr = mpl.colormaps
+
+ # check str, and Colormap pass
+ assert cr.get_cmap('plasma') == cr["plasma"]
+ assert cr.get_cmap(cr["magma"]) == cr["magma"]
+
+ # check default
+ assert cr.get_cmap(None) == cr[mpl.rcParams['image.cmap']]
+
+ # check ValueError on bad name
+ bad_cmap = 'AardvarksAreAwkward'
+ with pytest.raises(ValueError, match=bad_cmap):
+ cr.get_cmap(bad_cmap)
+
+ # check TypeError on bad type
+ with pytest.raises(TypeError, match='object'):
+ cr.get_cmap(object())
+
+
+def test_double_register_builtin_cmap():
+ name = "viridis"
+ match = f"Re-registering the builtin cmap {name!r}."
+ with pytest.raises(ValueError, match=match):
+ matplotlib.colormaps.register(mpl.colormaps[name], name=name, force=True)
+
+
+def test_colormap_copy():
+ cmap = plt.cm.Reds
+ copied_cmap = copy.copy(cmap)
+ with np.errstate(invalid='ignore'):
+ ret1 = copied_cmap([-1, 0, .5, 1, np.nan, np.inf])
+ cmap2 = copy.copy(copied_cmap)
+ cmap2.set_bad('g')
+ with np.errstate(invalid='ignore'):
+ ret2 = copied_cmap([-1, 0, .5, 1, np.nan, np.inf])
+ assert_array_equal(ret1, ret2)
+ # again with the .copy method:
+ cmap = plt.cm.Reds
+ copied_cmap = cmap.copy()
+ with np.errstate(invalid='ignore'):
+ ret1 = copied_cmap([-1, 0, .5, 1, np.nan, np.inf])
+ cmap2 = copy.copy(copied_cmap)
+ cmap2.set_bad('g')
+ with np.errstate(invalid='ignore'):
+ ret2 = copied_cmap([-1, 0, .5, 1, np.nan, np.inf])
+ assert_array_equal(ret1, ret2)
+
+
+def test_colormap_equals():
+ cmap = mpl.colormaps["plasma"]
+ cm_copy = cmap.copy()
+ # different object id's
+ assert cm_copy is not cmap
+ # But the same data should be equal
+ assert cm_copy == cmap
+ # Change the copy
+ cm_copy.set_bad('y')
+ assert cm_copy != cmap
+ # Make sure we can compare different sizes without failure
+ cm_copy._lut = cm_copy._lut[:10, :]
+ assert cm_copy != cmap
+ # Test different names are equal if the lookup table is the same
+ cm_copy = cmap.copy()
+ cm_copy.name = "Test"
+ assert cm_copy == cmap
+ # Test colorbar extends
+ cm_copy = cmap.copy()
+ cm_copy.colorbar_extend = not cmap.colorbar_extend
+ assert cm_copy != cmap
+
+
+def test_colormap_endian():
+ """
+ GitHub issue #1005: a bug in putmask caused erroneous
+ mapping of 1.0 when input from a non-native-byteorder
+ array.
+ """
+ cmap = mpl.colormaps["jet"]
+ # Test under, over, and invalid along with values 0 and 1.
+ a = [-0.5, 0, 0.5, 1, 1.5, np.nan]
+ for dt in ["f2", "f4", "f8"]:
+ anative = np.ma.masked_invalid(np.array(a, dtype=dt))
+ aforeign = anative.byteswap().view(anative.dtype.newbyteorder())
+ assert_array_equal(cmap(anative), cmap(aforeign))
+
+
+def test_colormap_invalid():
+ """
+ GitHub issue #9892: Handling of nan's were getting mapped to under
+ rather than bad. This tests to make sure all invalid values
+ (-inf, nan, inf) are mapped respectively to (under, bad, over).
+ """
+ cmap = mpl.colormaps["plasma"]
+ x = np.array([-np.inf, -1, 0, np.nan, .7, 2, np.inf])
+
+ expected = np.array([[0.050383, 0.029803, 0.527975, 1.],
+ [0.050383, 0.029803, 0.527975, 1.],
+ [0.050383, 0.029803, 0.527975, 1.],
+ [0., 0., 0., 0.],
+ [0.949217, 0.517763, 0.295662, 1.],
+ [0.940015, 0.975158, 0.131326, 1.],
+ [0.940015, 0.975158, 0.131326, 1.]])
+ assert_array_equal(cmap(x), expected)
+
+ # Test masked representation (-inf, inf) are now masked
+ expected = np.array([[0., 0., 0., 0.],
+ [0.050383, 0.029803, 0.527975, 1.],
+ [0.050383, 0.029803, 0.527975, 1.],
+ [0., 0., 0., 0.],
+ [0.949217, 0.517763, 0.295662, 1.],
+ [0.940015, 0.975158, 0.131326, 1.],
+ [0., 0., 0., 0.]])
+ assert_array_equal(cmap(np.ma.masked_invalid(x)), expected)
+
+ # Test scalar representations
+ assert_array_equal(cmap(-np.inf), cmap(0))
+ assert_array_equal(cmap(np.inf), cmap(1.0))
+ assert_array_equal(cmap(np.nan), [0., 0., 0., 0.])
+
+
+def test_colormap_return_types():
+ """
+ Make sure that tuples are returned for scalar input and
+ that the proper shapes are returned for ndarrays.
+ """
+ cmap = mpl.colormaps["plasma"]
+ # Test return types and shapes
+ # scalar input needs to return a tuple of length 4
+ assert isinstance(cmap(0.5), tuple)
+ assert len(cmap(0.5)) == 4
+
+ # input array returns an ndarray of shape x.shape + (4,)
+ x = np.ones(4)
+ assert cmap(x).shape == x.shape + (4,)
+
+ # multi-dimensional array input
+ x2d = np.zeros((2, 2))
+ assert cmap(x2d).shape == x2d.shape + (4,)
+
+
+def test_BoundaryNorm():
+ """
+ GitHub issue #1258: interpolation was failing with numpy
+ 1.7 pre-release.
+ """
+
+ boundaries = [0, 1.1, 2.2]
+ vals = [-1, 0, 1, 2, 2.2, 4]
+
+ # Without interpolation
+ expected = [-1, 0, 0, 1, 2, 2]
+ ncolors = len(boundaries) - 1
+ bn = mcolors.BoundaryNorm(boundaries, ncolors)
+ assert_array_equal(bn(vals), expected)
+
+ # ncolors != len(boundaries) - 1 triggers interpolation
+ expected = [-1, 0, 0, 2, 3, 3]
+ ncolors = len(boundaries)
+ bn = mcolors.BoundaryNorm(boundaries, ncolors)
+ assert_array_equal(bn(vals), expected)
+
+ # with a single region and interpolation
+ expected = [-1, 1, 1, 1, 3, 3]
+ bn = mcolors.BoundaryNorm([0, 2.2], ncolors)
+ assert_array_equal(bn(vals), expected)
+
+ # more boundaries for a third color
+ boundaries = [0, 1, 2, 3]
+ vals = [-1, 0.1, 1.1, 2.2, 4]
+ ncolors = 5
+ expected = [-1, 0, 2, 4, 5]
+ bn = mcolors.BoundaryNorm(boundaries, ncolors)
+ assert_array_equal(bn(vals), expected)
+
+ # a scalar as input should not trigger an error and should return a scalar
+ boundaries = [0, 1, 2]
+ vals = [-1, 0.1, 1.1, 2.2]
+ bn = mcolors.BoundaryNorm(boundaries, 2)
+ expected = [-1, 0, 1, 2]
+ for v, ex in zip(vals, expected):
+ ret = bn(v)
+ assert isinstance(ret, int)
+ assert_array_equal(ret, ex)
+ assert_array_equal(bn([v]), ex)
+
+ # same with interp
+ bn = mcolors.BoundaryNorm(boundaries, 3)
+ expected = [-1, 0, 2, 3]
+ for v, ex in zip(vals, expected):
+ ret = bn(v)
+ assert isinstance(ret, int)
+ assert_array_equal(ret, ex)
+ assert_array_equal(bn([v]), ex)
+
+ # Clipping
+ bn = mcolors.BoundaryNorm(boundaries, 3, clip=True)
+ expected = [0, 0, 2, 2]
+ for v, ex in zip(vals, expected):
+ ret = bn(v)
+ assert isinstance(ret, int)
+ assert_array_equal(ret, ex)
+ assert_array_equal(bn([v]), ex)
+
+ # Masked arrays
+ boundaries = [0, 1.1, 2.2]
+ vals = np.ma.masked_invalid([-1., np.nan, 0, 1.4, 9])
+
+ # Without interpolation
+ ncolors = len(boundaries) - 1
+ bn = mcolors.BoundaryNorm(boundaries, ncolors)
+ expected = np.ma.masked_array([-1, -99, 0, 1, 2], mask=[0, 1, 0, 0, 0])
+ assert_array_equal(bn(vals), expected)
+
+ # With interpolation
+ bn = mcolors.BoundaryNorm(boundaries, len(boundaries))
+ expected = np.ma.masked_array([-1, -99, 0, 2, 3], mask=[0, 1, 0, 0, 0])
+ assert_array_equal(bn(vals), expected)
+
+ # Non-trivial masked arrays
+ vals = np.ma.masked_invalid([np.inf, np.nan])
+ assert np.all(bn(vals).mask)
+ vals = np.ma.masked_invalid([np.inf])
+ assert np.all(bn(vals).mask)
+
+ # Incompatible extend and clip
+ with pytest.raises(ValueError, match="not compatible"):
+ mcolors.BoundaryNorm(np.arange(4), 5, extend='both', clip=True)
+
+ # Too small ncolors argument
+ with pytest.raises(ValueError, match="ncolors must equal or exceed"):
+ mcolors.BoundaryNorm(np.arange(4), 2)
+
+ with pytest.raises(ValueError, match="ncolors must equal or exceed"):
+ mcolors.BoundaryNorm(np.arange(4), 3, extend='min')
+
+ with pytest.raises(ValueError, match="ncolors must equal or exceed"):
+ mcolors.BoundaryNorm(np.arange(4), 4, extend='both')
+
+ # Testing extend keyword, with interpolation (large cmap)
+ bounds = [1, 2, 3]
+ cmap = mpl.colormaps['viridis']
+ mynorm = mcolors.BoundaryNorm(bounds, cmap.N, extend='both')
+ refnorm = mcolors.BoundaryNorm([0] + bounds + [4], cmap.N)
+ x = np.random.randn(100) * 10 + 2
+ ref = refnorm(x)
+ ref[ref == 0] = -1
+ ref[ref == cmap.N - 1] = cmap.N
+ assert_array_equal(mynorm(x), ref)
+
+ # Without interpolation
+ cmref = mcolors.ListedColormap(['blue', 'red'])
+ cmref.set_over('black')
+ cmref.set_under('white')
+ cmshould = mcolors.ListedColormap(['white', 'blue', 'red', 'black'])
+
+ assert mcolors.same_color(cmref.get_over(), 'black')
+ assert mcolors.same_color(cmref.get_under(), 'white')
+
+ refnorm = mcolors.BoundaryNorm(bounds, cmref.N)
+ mynorm = mcolors.BoundaryNorm(bounds, cmshould.N, extend='both')
+ assert mynorm.vmin == refnorm.vmin
+ assert mynorm.vmax == refnorm.vmax
+
+ assert mynorm(bounds[0] - 0.1) == -1 # under
+ assert mynorm(bounds[0] + 0.1) == 1 # first bin -> second color
+ assert mynorm(bounds[-1] - 0.1) == cmshould.N - 2 # next-to-last color
+ assert mynorm(bounds[-1] + 0.1) == cmshould.N # over
+
+ x = [-1, 1.2, 2.3, 9.6]
+ assert_array_equal(cmshould(mynorm(x)), cmshould([0, 1, 2, 3]))
+ x = np.random.randn(100) * 10 + 2
+ assert_array_equal(cmshould(mynorm(x)), cmref(refnorm(x)))
+
+ # Just min
+ cmref = mcolors.ListedColormap(['blue', 'red'])
+ cmref.set_under('white')
+ cmshould = mcolors.ListedColormap(['white', 'blue', 'red'])
+
+ assert mcolors.same_color(cmref.get_under(), 'white')
+
+ assert cmref.N == 2
+ assert cmshould.N == 3
+ refnorm = mcolors.BoundaryNorm(bounds, cmref.N)
+ mynorm = mcolors.BoundaryNorm(bounds, cmshould.N, extend='min')
+ assert mynorm.vmin == refnorm.vmin
+ assert mynorm.vmax == refnorm.vmax
+ x = [-1, 1.2, 2.3]
+ assert_array_equal(cmshould(mynorm(x)), cmshould([0, 1, 2]))
+ x = np.random.randn(100) * 10 + 2
+ assert_array_equal(cmshould(mynorm(x)), cmref(refnorm(x)))
+
+ # Just max
+ cmref = mcolors.ListedColormap(['blue', 'red'])
+ cmref.set_over('black')
+ cmshould = mcolors.ListedColormap(['blue', 'red', 'black'])
+
+ assert mcolors.same_color(cmref.get_over(), 'black')
+
+ assert cmref.N == 2
+ assert cmshould.N == 3
+ refnorm = mcolors.BoundaryNorm(bounds, cmref.N)
+ mynorm = mcolors.BoundaryNorm(bounds, cmshould.N, extend='max')
+ assert mynorm.vmin == refnorm.vmin
+ assert mynorm.vmax == refnorm.vmax
+ x = [1.2, 2.3, 4]
+ assert_array_equal(cmshould(mynorm(x)), cmshould([0, 1, 2]))
+ x = np.random.randn(100) * 10 + 2
+ assert_array_equal(cmshould(mynorm(x)), cmref(refnorm(x)))
+
+
+def test_CenteredNorm():
+ np.random.seed(0)
+
+ # Assert equivalence to symmetrical Normalize.
+ x = np.random.normal(size=100)
+ x_maxabs = np.max(np.abs(x))
+ norm_ref = mcolors.Normalize(vmin=-x_maxabs, vmax=x_maxabs)
+ norm = mcolors.CenteredNorm()
+ assert_array_almost_equal(norm_ref(x), norm(x))
+
+ # Check that vcenter is in the center of vmin and vmax
+ # when vcenter is set.
+ vcenter = int(np.random.normal(scale=50))
+ norm = mcolors.CenteredNorm(vcenter=vcenter)
+ norm.autoscale_None([1, 2])
+ assert norm.vmax + norm.vmin == 2 * vcenter
+
+ # Check that halfrange can be set without setting vcenter and that it is
+ # not reset through autoscale_None.
+ norm = mcolors.CenteredNorm(halfrange=1.0)
+ norm.autoscale_None([1, 3000])
+ assert norm.halfrange == 1.0
+
+ # Check that halfrange input works correctly.
+ x = np.random.normal(size=10)
+ norm = mcolors.CenteredNorm(vcenter=0.5, halfrange=0.5)
+ assert_array_almost_equal(x, norm(x))
+ norm = mcolors.CenteredNorm(vcenter=1, halfrange=1)
+ assert_array_almost_equal(x, 2 * norm(x))
+
+ # Check that halfrange input works correctly and use setters.
+ norm = mcolors.CenteredNorm()
+ norm.vcenter = 2
+ norm.halfrange = 2
+ assert_array_almost_equal(x, 4 * norm(x))
+
+ # Check that prior to adding data, setting halfrange first has same effect.
+ norm = mcolors.CenteredNorm()
+ norm.halfrange = 2
+ norm.vcenter = 2
+ assert_array_almost_equal(x, 4 * norm(x))
+
+ # Check that manual change of vcenter adjusts halfrange accordingly.
+ norm = mcolors.CenteredNorm()
+ assert norm.vcenter == 0
+ # add data
+ norm(np.linspace(-1.0, 0.0, 10))
+ assert norm.vmax == 1.0
+ assert norm.halfrange == 1.0
+ # set vcenter to 1, which should move the center but leave the
+ # halfrange unchanged
+ norm.vcenter = 1
+ assert norm.vmin == 0
+ assert norm.vmax == 2
+ assert norm.halfrange == 1
+
+ # Check setting vmin directly updates the halfrange and vmax, but
+ # leaves vcenter alone
+ norm.vmin = -1
+ assert norm.halfrange == 2
+ assert norm.vmax == 3
+ assert norm.vcenter == 1
+
+ # also check vmax updates
+ norm.vmax = 2
+ assert norm.halfrange == 1
+ assert norm.vmin == 0
+ assert norm.vcenter == 1
+
+
+@pytest.mark.parametrize("vmin,vmax", [[-1, 2], [3, 1]])
+def test_lognorm_invalid(vmin, vmax):
+ # Check that invalid limits in LogNorm error
+ norm = mcolors.LogNorm(vmin=vmin, vmax=vmax)
+ with pytest.raises(ValueError):
+ norm(1)
+ with pytest.raises(ValueError):
+ norm.inverse(1)
+
+
+def test_LogNorm():
+ """
+ LogNorm ignored clip, now it has the same
+ behavior as Normalize, e.g., values > vmax are bigger than 1
+ without clip, with clip they are 1.
+ """
+ ln = mcolors.LogNorm(clip=True, vmax=5)
+ assert_array_equal(ln([1, 6]), [0, 1.0])
+
+
+def test_LogNorm_inverse():
+ """
+ Test that lists work, and that the inverse works
+ """
+ norm = mcolors.LogNorm(vmin=0.1, vmax=10)
+ assert_array_almost_equal(norm([0.5, 0.4]), [0.349485, 0.30103])
+ assert_array_almost_equal([0.5, 0.4], norm.inverse([0.349485, 0.30103]))
+ assert_array_almost_equal(norm(0.4), [0.30103])
+ assert_array_almost_equal([0.4], norm.inverse([0.30103]))
+
+
+def test_PowerNorm():
+ # Check an exponent of 1 gives same results as a normal linear
+ # normalization. Also implicitly checks that vmin/vmax are
+ # automatically initialized from first array input.
+ a = np.array([0, 0.5, 1, 1.5], dtype=float)
+ pnorm = mcolors.PowerNorm(1)
+ norm = mcolors.Normalize()
+ assert_array_almost_equal(norm(a), pnorm(a))
+
+ a = np.array([-0.5, 0, 2, 4, 8], dtype=float)
+ expected = [-1/16, 0, 1/16, 1/4, 1]
+ pnorm = mcolors.PowerNorm(2, vmin=0, vmax=8)
+ assert_array_almost_equal(pnorm(a), expected)
+ assert pnorm(a[0]) == expected[0]
+ assert pnorm(a[2]) == expected[2]
+ # Check inverse
+ a_roundtrip = pnorm.inverse(pnorm(a))
+ assert_array_almost_equal(a, a_roundtrip)
+ # PowerNorm inverse adds a mask, so check that is correct too
+ assert_array_equal(a_roundtrip.mask, np.zeros(a.shape, dtype=bool))
+
+ # Clip = True
+ a = np.array([-0.5, 0, 1, 8, 16], dtype=float)
+ expected = [0, 0, 0, 1, 1]
+ # Clip = True when creating the norm
+ pnorm = mcolors.PowerNorm(2, vmin=2, vmax=8, clip=True)
+ assert_array_almost_equal(pnorm(a), expected)
+ assert pnorm(a[0]) == expected[0]
+ assert pnorm(a[-1]) == expected[-1]
+ # Clip = True at call time
+ pnorm = mcolors.PowerNorm(2, vmin=2, vmax=8, clip=False)
+ assert_array_almost_equal(pnorm(a, clip=True), expected)
+ assert pnorm(a[0], clip=True) == expected[0]
+ assert pnorm(a[-1], clip=True) == expected[-1]
+
+ # Check clip=True preserves masked values
+ a = np.ma.array([5, 2], mask=[True, False])
+ out = pnorm(a, clip=True)
+ assert_array_equal(out.mask, [True, False])
+
+
+def test_PowerNorm_translation_invariance():
+ a = np.array([0, 1/2, 1], dtype=float)
+ expected = [0, 1/8, 1]
+ pnorm = mcolors.PowerNorm(vmin=0, vmax=1, gamma=3)
+ assert_array_almost_equal(pnorm(a), expected)
+ pnorm = mcolors.PowerNorm(vmin=-2, vmax=-1, gamma=3)
+ assert_array_almost_equal(pnorm(a - 2), expected)
+
+
+def test_powernorm_cbar_limits():
+ fig, ax = plt.subplots()
+ vmin, vmax = 300, 1000
+ data = np.arange(10*10).reshape(10, 10) + vmin
+ im = ax.imshow(data, norm=mcolors.PowerNorm(gamma=0.2, vmin=vmin, vmax=vmax))
+ cbar = fig.colorbar(im)
+ assert cbar.ax.get_ylim() == (vmin, vmax)
+
+
+def test_Normalize():
+ norm = mcolors.Normalize()
+ vals = np.arange(-10, 10, 1, dtype=float)
+ _inverse_tester(norm, vals)
+ _scalar_tester(norm, vals)
+ _mask_tester(norm, vals)
+
+ # Handle integer input correctly (don't overflow when computing max-min,
+ # i.e. 127-(-128) here).
+ vals = np.array([-128, 127], dtype=np.int8)
+ norm = mcolors.Normalize(vals.min(), vals.max())
+ assert_array_equal(norm(vals), [0, 1])
+
+ # Don't lose precision on longdoubles (float128 on Linux):
+ # for array inputs...
+ vals = np.array([1.2345678901, 9.8765432109], dtype=np.longdouble)
+ norm = mcolors.Normalize(vals[0], vals[1])
+ assert norm(vals).dtype == np.longdouble
+ assert_array_equal(norm(vals), [0, 1])
+ # and for scalar ones.
+ eps = np.finfo(np.longdouble).resolution
+ norm = plt.Normalize(1, 1 + 100 * eps)
+ # This returns exactly 0.5 when longdouble is extended precision (80-bit),
+ # but only a value close to it when it is quadruple precision (128-bit).
+ assert_array_almost_equal(norm(1 + 50 * eps), 0.5, decimal=3)
+
+
+def test_FuncNorm():
+ def forward(x):
+ return (x**2)
+ def inverse(x):
+ return np.sqrt(x)
+
+ norm = mcolors.FuncNorm((forward, inverse), vmin=0, vmax=10)
+ expected = np.array([0, 0.25, 1])
+ input = np.array([0, 5, 10])
+ assert_array_almost_equal(norm(input), expected)
+ assert_array_almost_equal(norm.inverse(expected), input)
+
+ def forward(x):
+ return np.log10(x)
+ def inverse(x):
+ return 10**x
+ norm = mcolors.FuncNorm((forward, inverse), vmin=0.1, vmax=10)
+ lognorm = mcolors.LogNorm(vmin=0.1, vmax=10)
+ assert_array_almost_equal(norm([0.2, 5, 10]), lognorm([0.2, 5, 10]))
+ assert_array_almost_equal(norm.inverse([0.2, 5, 10]),
+ lognorm.inverse([0.2, 5, 10]))
+
+
+def test_TwoSlopeNorm_autoscale():
+ norm = mcolors.TwoSlopeNorm(vcenter=20)
+ norm.autoscale([10, 20, 30, 40])
+ assert norm.vmin == 10.
+ assert norm.vmax == 40.
+
+
+def test_TwoSlopeNorm_autoscale_None_vmin():
+ norm = mcolors.TwoSlopeNorm(2, vmin=0, vmax=None)
+ norm.autoscale_None([1, 2, 3, 4, 5])
+ assert norm(5) == 1
+ assert norm.vmax == 5
+
+
+def test_TwoSlopeNorm_autoscale_None_vmax():
+ norm = mcolors.TwoSlopeNorm(2, vmin=None, vmax=10)
+ norm.autoscale_None([1, 2, 3, 4, 5])
+ assert norm(1) == 0
+ assert norm.vmin == 1
+
+
+def test_TwoSlopeNorm_scale():
+ norm = mcolors.TwoSlopeNorm(2)
+ assert norm.scaled() is False
+ norm([1, 2, 3, 4])
+ assert norm.scaled() is True
+
+
+def test_TwoSlopeNorm_scaleout_center():
+ # test the vmin never goes above vcenter
+ norm = mcolors.TwoSlopeNorm(vcenter=0)
+ norm([0, 1, 2, 3, 5])
+ assert norm.vmin == -5
+ assert norm.vmax == 5
+
+
+def test_TwoSlopeNorm_scaleout_center_max():
+ # test the vmax never goes below vcenter
+ norm = mcolors.TwoSlopeNorm(vcenter=0)
+ norm([0, -1, -2, -3, -5])
+ assert norm.vmax == 5
+ assert norm.vmin == -5
+
+
+def test_TwoSlopeNorm_Even():
+ norm = mcolors.TwoSlopeNorm(vmin=-1, vcenter=0, vmax=4)
+ vals = np.array([-1.0, -0.5, 0.0, 1.0, 2.0, 3.0, 4.0])
+ expected = np.array([0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
+ assert_array_equal(norm(vals), expected)
+
+
+def test_TwoSlopeNorm_Odd():
+ norm = mcolors.TwoSlopeNorm(vmin=-2, vcenter=0, vmax=5)
+ vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
+ expected = np.array([0.0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
+ assert_array_equal(norm(vals), expected)
+
+
+def test_TwoSlopeNorm_VminEqualsVcenter():
+ with pytest.raises(ValueError):
+ mcolors.TwoSlopeNorm(vmin=-2, vcenter=-2, vmax=2)
+
+
+def test_TwoSlopeNorm_VmaxEqualsVcenter():
+ with pytest.raises(ValueError):
+ mcolors.TwoSlopeNorm(vmin=-2, vcenter=2, vmax=2)
+
+
+def test_TwoSlopeNorm_VminGTVcenter():
+ with pytest.raises(ValueError):
+ mcolors.TwoSlopeNorm(vmin=10, vcenter=0, vmax=20)
+
+
+def test_TwoSlopeNorm_TwoSlopeNorm_VminGTVmax():
+ with pytest.raises(ValueError):
+ mcolors.TwoSlopeNorm(vmin=10, vcenter=0, vmax=5)
+
+
+def test_TwoSlopeNorm_VcenterGTVmax():
+ with pytest.raises(ValueError):
+ mcolors.TwoSlopeNorm(vmin=10, vcenter=25, vmax=20)
+
+
+def test_TwoSlopeNorm_premature_scaling():
+ norm = mcolors.TwoSlopeNorm(vcenter=2)
+ with pytest.raises(ValueError):
+ norm.inverse(np.array([0.1, 0.5, 0.9]))
+
+
+def test_SymLogNorm():
+ """
+ Test SymLogNorm behavior
+ """
+ norm = mcolors.SymLogNorm(3, vmax=5, linscale=1.2, base=np.e)
+ vals = np.array([-30, -1, 2, 6], dtype=float)
+ normed_vals = norm(vals)
+ expected = [0., 0.53980074, 0.826991, 1.02758204]
+ assert_array_almost_equal(normed_vals, expected)
+ _inverse_tester(norm, vals)
+ _scalar_tester(norm, vals)
+ _mask_tester(norm, vals)
+
+ # Ensure that specifying vmin returns the same result as above
+ norm = mcolors.SymLogNorm(3, vmin=-30, vmax=5, linscale=1.2, base=np.e)
+ normed_vals = norm(vals)
+ assert_array_almost_equal(normed_vals, expected)
+
+ # test something more easily checked.
+ norm = mcolors.SymLogNorm(1, vmin=-np.e**3, vmax=np.e**3, base=np.e)
+ nn = norm([-np.e**3, -np.e**2, -np.e**1, -1,
+ 0, 1, np.e**1, np.e**2, np.e**3])
+ xx = np.array([0., 0.109123, 0.218246, 0.32737, 0.5, 0.67263,
+ 0.781754, 0.890877, 1.])
+ assert_array_almost_equal(nn, xx)
+ norm = mcolors.SymLogNorm(1, vmin=-10**3, vmax=10**3, base=10)
+ nn = norm([-10**3, -10**2, -10**1, -1,
+ 0, 1, 10**1, 10**2, 10**3])
+ xx = np.array([0., 0.121622, 0.243243, 0.364865, 0.5, 0.635135,
+ 0.756757, 0.878378, 1.])
+ assert_array_almost_equal(nn, xx)
+
+
+def test_SymLogNorm_colorbar():
+ """
+ Test un-called SymLogNorm in a colorbar.
+ """
+ norm = mcolors.SymLogNorm(0.1, vmin=-1, vmax=1, linscale=1, base=np.e)
+ fig = plt.figure()
+ mcolorbar.ColorbarBase(fig.add_subplot(), norm=norm)
+ plt.close(fig)
+
+
+def test_SymLogNorm_single_zero():
+ """
+ Test SymLogNorm to ensure it is not adding sub-ticks to zero label
+ """
+ fig = plt.figure()
+ norm = mcolors.SymLogNorm(1e-5, vmin=-1, vmax=1, base=np.e)
+ cbar = mcolorbar.ColorbarBase(fig.add_subplot(), norm=norm)
+ ticks = cbar.get_ticks()
+ assert np.count_nonzero(ticks == 0) <= 1
+ plt.close(fig)
+
+
+class TestAsinhNorm:
+ """
+ Tests for `~.colors.AsinhNorm`
+ """
+
+ def test_init(self):
+ norm0 = mcolors.AsinhNorm()
+ assert norm0.linear_width == 1
+
+ norm5 = mcolors.AsinhNorm(linear_width=5)
+ assert norm5.linear_width == 5
+
+ def test_norm(self):
+ norm = mcolors.AsinhNorm(2, vmin=-4, vmax=4)
+ vals = np.arange(-3.5, 3.5, 10)
+ normed_vals = norm(vals)
+ asinh2 = np.arcsinh(2)
+
+ expected = (2 * np.arcsinh(vals / 2) + 2 * asinh2) / (4 * asinh2)
+ assert_array_almost_equal(normed_vals, expected)
+
+
+def _inverse_tester(norm_instance, vals):
+ """
+ Checks if the inverse of the given normalization is working.
+ """
+ assert_array_almost_equal(norm_instance.inverse(norm_instance(vals)), vals)
+
+
+def _scalar_tester(norm_instance, vals):
+ """
+ Checks if scalars and arrays are handled the same way.
+ Tests only for float.
+ """
+ scalar_result = [norm_instance(float(v)) for v in vals]
+ assert_array_almost_equal(scalar_result, norm_instance(vals))
+
+
+def _mask_tester(norm_instance, vals):
+ """
+ Checks mask handling
+ """
+ masked_array = np.ma.array(vals)
+ masked_array[0] = np.ma.masked
+ assert_array_equal(masked_array.mask, norm_instance(masked_array).mask)
+
+
+@image_comparison(['levels_and_colors.png'])
+def test_cmap_and_norm_from_levels_and_colors():
+ # Remove this line when this test image is regenerated.
+ plt.rcParams['pcolormesh.snap'] = False
+
+ data = np.linspace(-2, 4, 49).reshape(7, 7)
+ levels = [-1, 2, 2.5, 3]
+ colors = ['red', 'green', 'blue', 'yellow', 'black']
+ extend = 'both'
+ cmap, norm = mcolors.from_levels_and_colors(levels, colors, extend=extend)
+
+ ax = plt.axes()
+ m = plt.pcolormesh(data, cmap=cmap, norm=norm)
+ plt.colorbar(m)
+
+ # Hide the axes labels (but not the colorbar ones, as they are useful)
+ ax.tick_params(labelleft=False, labelbottom=False)
+
+
+@image_comparison(baseline_images=['boundarynorm_and_colorbar'],
+ extensions=['png'], tol=1.0)
+def test_boundarynorm_and_colorbarbase():
+ # Remove this line when this test image is regenerated.
+ plt.rcParams['pcolormesh.snap'] = False
+
+ # Make a figure and axes with dimensions as desired.
+ fig = plt.figure()
+ ax1 = fig.add_axes([0.05, 0.80, 0.9, 0.15])
+ ax2 = fig.add_axes([0.05, 0.475, 0.9, 0.15])
+ ax3 = fig.add_axes([0.05, 0.15, 0.9, 0.15])
+
+ # Set the colormap and bounds
+ bounds = [-1, 2, 5, 7, 12, 15]
+ cmap = mpl.colormaps['viridis']
+
+ # Default behavior
+ norm = mcolors.BoundaryNorm(bounds, cmap.N)
+ cb1 = mcolorbar.ColorbarBase(ax1, cmap=cmap, norm=norm, extend='both',
+ orientation='horizontal', spacing='uniform')
+ # New behavior
+ norm = mcolors.BoundaryNorm(bounds, cmap.N, extend='both')
+ cb2 = mcolorbar.ColorbarBase(ax2, cmap=cmap, norm=norm,
+ orientation='horizontal')
+
+ # User can still force to any extend='' if really needed
+ norm = mcolors.BoundaryNorm(bounds, cmap.N, extend='both')
+ cb3 = mcolorbar.ColorbarBase(ax3, cmap=cmap, norm=norm,
+ extend='neither', orientation='horizontal')
+
+
+def test_cmap_and_norm_from_levels_and_colors2():
+ levels = [-1, 2, 2.5, 3]
+ colors = ['red', (0, 1, 0), 'blue', (0.5, 0.5, 0.5), (0.0, 0.0, 0.0, 1.0)]
+ clr = mcolors.to_rgba_array(colors)
+ bad = (0.1, 0.1, 0.1, 0.1)
+ no_color = (0.0, 0.0, 0.0, 0.0)
+ masked_value = 'masked_value'
+
+ # Define the test values which are of interest.
+ # Note: levels are lev[i] <= v < lev[i+1]
+ tests = [('both', None, {-2: clr[0],
+ -1: clr[1],
+ 2: clr[2],
+ 2.25: clr[2],
+ 3: clr[4],
+ 3.5: clr[4],
+ masked_value: bad}),
+
+ ('min', -1, {-2: clr[0],
+ -1: clr[1],
+ 2: clr[2],
+ 2.25: clr[2],
+ 3: no_color,
+ 3.5: no_color,
+ masked_value: bad}),
+
+ ('max', -1, {-2: no_color,
+ -1: clr[0],
+ 2: clr[1],
+ 2.25: clr[1],
+ 3: clr[3],
+ 3.5: clr[3],
+ masked_value: bad}),
+
+ ('neither', -2, {-2: no_color,
+ -1: clr[0],
+ 2: clr[1],
+ 2.25: clr[1],
+ 3: no_color,
+ 3.5: no_color,
+ masked_value: bad}),
+ ]
+
+ for extend, i1, cases in tests:
+ cmap, norm = mcolors.from_levels_and_colors(levels, colors[0:i1],
+ extend=extend)
+ cmap.set_bad(bad)
+ for d_val, expected_color in cases.items():
+ if d_val == masked_value:
+ d_val = np.ma.array([1], mask=True)
+ else:
+ d_val = [d_val]
+ assert_array_equal(expected_color, cmap(norm(d_val))[0],
+ f'With extend={extend!r} and data '
+ f'value={d_val!r}')
+
+ with pytest.raises(ValueError):
+ mcolors.from_levels_and_colors(levels, colors)
+
+
+def test_rgb_hsv_round_trip():
+ for a_shape in [(500, 500, 3), (500, 3), (1, 3), (3,)]:
+ np.random.seed(0)
+ tt = np.random.random(a_shape)
+ assert_array_almost_equal(
+ tt, mcolors.hsv_to_rgb(mcolors.rgb_to_hsv(tt)))
+ assert_array_almost_equal(
+ tt, mcolors.rgb_to_hsv(mcolors.hsv_to_rgb(tt)))
+
+
+def test_autoscale_masked():
+ # Test for #2336. Previously fully masked data would trigger a ValueError.
+ data = np.ma.masked_all((12, 20))
+ plt.pcolor(data)
+ plt.draw()
+
+
+@image_comparison(['light_source_shading_topo.png'])
+def test_light_source_topo_surface():
+ """Shades a DEM using different v.e.'s and blend modes."""
+ dem = cbook.get_sample_data('jacksboro_fault_dem.npz')
+ elev = dem['elevation']
+ dx, dy = dem['dx'], dem['dy']
+ # Get the true cellsize in meters for accurate vertical exaggeration
+ # Convert from decimal degrees to meters
+ dx = 111320.0 * dx * np.cos(dem['ymin'])
+ dy = 111320.0 * dy
+
+ ls = mcolors.LightSource(315, 45)
+ cmap = cm.gist_earth
+
+ fig, axs = plt.subplots(nrows=3, ncols=3)
+ for row, mode in zip(axs, ['hsv', 'overlay', 'soft']):
+ for ax, ve in zip(row, [0.1, 1, 10]):
+ rgb = ls.shade(elev, cmap, vert_exag=ve, dx=dx, dy=dy,
+ blend_mode=mode)
+ ax.imshow(rgb)
+ ax.set(xticks=[], yticks=[])
+
+
+def test_light_source_shading_default():
+ """
+ Array comparison test for the default "hsv" blend mode. Ensure the
+ default result doesn't change without warning.
+ """
+ y, x = np.mgrid[-1.2:1.2:8j, -1.2:1.2:8j]
+ z = 10 * np.cos(x**2 + y**2)
+
+ cmap = plt.cm.copper
+ ls = mcolors.LightSource(315, 45)
+ rgb = ls.shade(z, cmap)
+
+ # Result stored transposed and rounded for more compact display...
+ expect = np.array(
+ [[[0.00, 0.45, 0.90, 0.90, 0.82, 0.62, 0.28, 0.00],
+ [0.45, 0.94, 0.99, 1.00, 1.00, 0.96, 0.65, 0.17],
+ [0.90, 0.99, 1.00, 1.00, 1.00, 1.00, 0.94, 0.35],
+ [0.90, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 0.49],
+ [0.82, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 0.41],
+ [0.62, 0.96, 1.00, 1.00, 1.00, 1.00, 0.90, 0.07],
+ [0.28, 0.65, 0.94, 1.00, 1.00, 0.90, 0.35, 0.01],
+ [0.00, 0.17, 0.35, 0.49, 0.41, 0.07, 0.01, 0.00]],
+
+ [[0.00, 0.28, 0.59, 0.72, 0.62, 0.40, 0.18, 0.00],
+ [0.28, 0.78, 0.93, 0.92, 0.83, 0.66, 0.39, 0.11],
+ [0.59, 0.93, 0.99, 1.00, 0.92, 0.75, 0.50, 0.21],
+ [0.72, 0.92, 1.00, 0.99, 0.93, 0.76, 0.51, 0.18],
+ [0.62, 0.83, 0.92, 0.93, 0.87, 0.68, 0.42, 0.08],
+ [0.40, 0.66, 0.75, 0.76, 0.68, 0.52, 0.23, 0.02],
+ [0.18, 0.39, 0.50, 0.51, 0.42, 0.23, 0.00, 0.00],
+ [0.00, 0.11, 0.21, 0.18, 0.08, 0.02, 0.00, 0.00]],
+
+ [[0.00, 0.18, 0.38, 0.46, 0.39, 0.26, 0.11, 0.00],
+ [0.18, 0.50, 0.70, 0.75, 0.64, 0.44, 0.25, 0.07],
+ [0.38, 0.70, 0.91, 0.98, 0.81, 0.51, 0.29, 0.13],
+ [0.46, 0.75, 0.98, 0.96, 0.84, 0.48, 0.22, 0.12],
+ [0.39, 0.64, 0.81, 0.84, 0.71, 0.31, 0.11, 0.05],
+ [0.26, 0.44, 0.51, 0.48, 0.31, 0.10, 0.03, 0.01],
+ [0.11, 0.25, 0.29, 0.22, 0.11, 0.03, 0.00, 0.00],
+ [0.00, 0.07, 0.13, 0.12, 0.05, 0.01, 0.00, 0.00]],
+
+ [[1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00],
+ [1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00],
+ [1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00],
+ [1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00],
+ [1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00],
+ [1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00],
+ [1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00],
+ [1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00]]
+ ]).T
+
+ assert_array_almost_equal(rgb, expect, decimal=2)
+
+
+def test_light_source_shading_empty_mask():
+ y, x = np.mgrid[-1.2:1.2:8j, -1.2:1.2:8j]
+ z0 = 10 * np.cos(x**2 + y**2)
+ z1 = np.ma.array(z0)
+
+ cmap = plt.cm.copper
+ ls = mcolors.LightSource(315, 45)
+ rgb0 = ls.shade(z0, cmap)
+ rgb1 = ls.shade(z1, cmap)
+
+ assert_array_almost_equal(rgb0, rgb1)
+
+
+# Numpy 1.9.1 fixed a bug in masked arrays which resulted in
+# additional elements being masked when calculating the gradient thus
+# the output is different with earlier numpy versions.
+def test_light_source_masked_shading():
+ """
+ Array comparison test for a surface with a masked portion. Ensures that
+ we don't wind up with "fringes" of odd colors around masked regions.
+ """
+ y, x = np.mgrid[-1.2:1.2:8j, -1.2:1.2:8j]
+ z = 10 * np.cos(x**2 + y**2)
+
+ z = np.ma.masked_greater(z, 9.9)
+
+ cmap = plt.cm.copper
+ ls = mcolors.LightSource(315, 45)
+ rgb = ls.shade(z, cmap)
+
+ # Result stored transposed and rounded for more compact display...
+ expect = np.array(
+ [[[0.00, 0.46, 0.91, 0.91, 0.84, 0.64, 0.29, 0.00],
+ [0.46, 0.96, 1.00, 1.00, 1.00, 0.97, 0.67, 0.18],
+ [0.91, 1.00, 1.00, 1.00, 1.00, 1.00, 0.96, 0.36],
+ [0.91, 1.00, 1.00, 0.00, 0.00, 1.00, 1.00, 0.51],
+ [0.84, 1.00, 1.00, 0.00, 0.00, 1.00, 1.00, 0.44],
+ [0.64, 0.97, 1.00, 1.00, 1.00, 1.00, 0.94, 0.09],
+ [0.29, 0.67, 0.96, 1.00, 1.00, 0.94, 0.38, 0.01],
+ [0.00, 0.18, 0.36, 0.51, 0.44, 0.09, 0.01, 0.00]],
+
+ [[0.00, 0.29, 0.61, 0.75, 0.64, 0.41, 0.18, 0.00],
+ [0.29, 0.81, 0.95, 0.93, 0.85, 0.68, 0.40, 0.11],
+ [0.61, 0.95, 1.00, 0.78, 0.78, 0.77, 0.52, 0.22],
+ [0.75, 0.93, 0.78, 0.00, 0.00, 0.78, 0.54, 0.19],
+ [0.64, 0.85, 0.78, 0.00, 0.00, 0.78, 0.45, 0.08],
+ [0.41, 0.68, 0.77, 0.78, 0.78, 0.55, 0.25, 0.02],
+ [0.18, 0.40, 0.52, 0.54, 0.45, 0.25, 0.00, 0.00],
+ [0.00, 0.11, 0.22, 0.19, 0.08, 0.02, 0.00, 0.00]],
+
+ [[0.00, 0.19, 0.39, 0.48, 0.41, 0.26, 0.12, 0.00],
+ [0.19, 0.52, 0.73, 0.78, 0.66, 0.46, 0.26, 0.07],
+ [0.39, 0.73, 0.95, 0.50, 0.50, 0.53, 0.30, 0.14],
+ [0.48, 0.78, 0.50, 0.00, 0.00, 0.50, 0.23, 0.12],
+ [0.41, 0.66, 0.50, 0.00, 0.00, 0.50, 0.11, 0.05],
+ [0.26, 0.46, 0.53, 0.50, 0.50, 0.11, 0.03, 0.01],
+ [0.12, 0.26, 0.30, 0.23, 0.11, 0.03, 0.00, 0.00],
+ [0.00, 0.07, 0.14, 0.12, 0.05, 0.01, 0.00, 0.00]],
+
+ [[1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00],
+ [1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00],
+ [1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00],
+ [1.00, 1.00, 1.00, 0.00, 0.00, 1.00, 1.00, 1.00],
+ [1.00, 1.00, 1.00, 0.00, 0.00, 1.00, 1.00, 1.00],
+ [1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00],
+ [1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00],
+ [1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00]],
+ ]).T
+
+ assert_array_almost_equal(rgb, expect, decimal=2)
+
+
+def test_light_source_hillshading():
+ """
+ Compare the current hillshading method against one that should be
+ mathematically equivalent. Illuminates a cone from a range of angles.
+ """
+
+ def alternative_hillshade(azimuth, elev, z):
+ illum = _sph2cart(*_azimuth2math(azimuth, elev))
+ illum = np.array(illum)
+
+ dy, dx = np.gradient(-z)
+ dy = -dy
+ dz = np.ones_like(dy)
+ normals = np.dstack([dx, dy, dz])
+ normals /= np.linalg.norm(normals, axis=2)[..., None]
+
+ intensity = np.tensordot(normals, illum, axes=(2, 0))
+ intensity -= intensity.min()
+ intensity /= np.ptp(intensity)
+ return intensity
+
+ y, x = np.mgrid[5:0:-1, :5]
+ z = -np.hypot(x - x.mean(), y - y.mean())
+
+ for az, elev in itertools.product(range(0, 390, 30), range(0, 105, 15)):
+ ls = mcolors.LightSource(az, elev)
+ h1 = ls.hillshade(z)
+ h2 = alternative_hillshade(az, elev, z)
+ assert_array_almost_equal(h1, h2)
+
+
+def test_light_source_planar_hillshading():
+ """
+ Ensure that the illumination intensity is correct for planar surfaces.
+ """
+
+ def plane(azimuth, elevation, x, y):
+ """
+ Create a plane whose normal vector is at the given azimuth and
+ elevation.
+ """
+ theta, phi = _azimuth2math(azimuth, elevation)
+ a, b, c = _sph2cart(theta, phi)
+ z = -(a*x + b*y) / c
+ return z
+
+ def angled_plane(azimuth, elevation, angle, x, y):
+ """
+ Create a plane whose normal vector is at an angle from the given
+ azimuth and elevation.
+ """
+ elevation = elevation + angle
+ if elevation > 90:
+ azimuth = (azimuth + 180) % 360
+ elevation = (90 - elevation) % 90
+ return plane(azimuth, elevation, x, y)
+
+ y, x = np.mgrid[5:0:-1, :5]
+ for az, elev in itertools.product(range(0, 390, 30), range(0, 105, 15)):
+ ls = mcolors.LightSource(az, elev)
+
+ # Make a plane at a range of angles to the illumination
+ for angle in range(0, 105, 15):
+ z = angled_plane(az, elev, angle, x, y)
+ h = ls.hillshade(z)
+ assert_array_almost_equal(h, np.cos(np.radians(angle)))
+
+
+def test_color_names():
+ assert mcolors.to_hex("blue") == "#0000ff"
+ assert mcolors.to_hex("xkcd:blue") == "#0343df"
+ assert mcolors.to_hex("tab:blue") == "#1f77b4"
+
+
+def _sph2cart(theta, phi):
+ x = np.cos(theta) * np.sin(phi)
+ y = np.sin(theta) * np.sin(phi)
+ z = np.cos(phi)
+ return x, y, z
+
+
+def _azimuth2math(azimuth, elevation):
+ """
+ Convert from clockwise-from-north and up-from-horizontal to mathematical
+ conventions.
+ """
+ theta = np.radians((90 - azimuth) % 360)
+ phi = np.radians(90 - elevation)
+ return theta, phi
+
+
+def test_pandas_iterable(pd):
+ # Using a list or series yields equivalent
+ # colormaps, i.e the series isn't seen as
+ # a single color
+ lst = ['red', 'blue', 'green']
+ s = pd.Series(lst)
+ cm1 = mcolors.ListedColormap(lst, N=5)
+ cm2 = mcolors.ListedColormap(s, N=5)
+ assert_array_equal(cm1.colors, cm2.colors)
+
+
+@pytest.mark.parametrize('name', sorted(mpl.colormaps()))
+def test_colormap_reversing(name):
+ """
+ Check the generated _lut data of a colormap and corresponding reversed
+ colormap if they are almost the same.
+ """
+ cmap = mpl.colormaps[name]
+ cmap_r = cmap.reversed()
+ if not cmap_r._isinit:
+ cmap._init()
+ cmap_r._init()
+ assert_array_almost_equal(cmap._lut[:-3], cmap_r._lut[-4::-1])
+ # Test the bad, over, under values too
+ assert_array_almost_equal(cmap(-np.inf), cmap_r(np.inf))
+ assert_array_almost_equal(cmap(np.inf), cmap_r(-np.inf))
+ assert_array_almost_equal(cmap(np.nan), cmap_r(np.nan))
+
+
+def test_has_alpha_channel():
+ assert mcolors._has_alpha_channel((0, 0, 0, 0))
+ assert mcolors._has_alpha_channel([1, 1, 1, 1])
+ assert not mcolors._has_alpha_channel('blue') # 4-char string!
+ assert not mcolors._has_alpha_channel('0.25')
+ assert not mcolors._has_alpha_channel('r')
+ assert not mcolors._has_alpha_channel((1, 0, 0))
+
+
+def test_cn():
+ matplotlib.rcParams['axes.prop_cycle'] = cycler('color',
+ ['blue', 'r'])
+ assert mcolors.to_hex("C0") == '#0000ff'
+ assert mcolors.to_hex("C1") == '#ff0000'
+
+ matplotlib.rcParams['axes.prop_cycle'] = cycler('color',
+ ['xkcd:blue', 'r'])
+ assert mcolors.to_hex("C0") == '#0343df'
+ assert mcolors.to_hex("C1") == '#ff0000'
+ assert mcolors.to_hex("C10") == '#0343df'
+ assert mcolors.to_hex("C11") == '#ff0000'
+
+ matplotlib.rcParams['axes.prop_cycle'] = cycler('color', ['8e4585', 'r'])
+
+ assert mcolors.to_hex("C0") == '#8e4585'
+ # if '8e4585' gets parsed as a float before it gets detected as a hex
+ # colour it will be interpreted as a very large number.
+ # this mustn't happen.
+ assert mcolors.to_rgb("C0")[0] != np.inf
+
+
+def test_conversions():
+ # to_rgba_array("none") returns a (0, 4) array.
+ assert_array_equal(mcolors.to_rgba_array("none"), np.zeros((0, 4)))
+ assert_array_equal(mcolors.to_rgba_array([]), np.zeros((0, 4)))
+ # a list of grayscale levels, not a single color.
+ assert_array_equal(
+ mcolors.to_rgba_array([".2", ".5", ".8"]),
+ np.vstack([mcolors.to_rgba(c) for c in [".2", ".5", ".8"]]))
+ # alpha is properly set.
+ assert mcolors.to_rgba((1, 1, 1), .5) == (1, 1, 1, .5)
+ assert mcolors.to_rgba(".1", .5) == (.1, .1, .1, .5)
+ # builtin round differs between py2 and py3.
+ assert mcolors.to_hex((.7, .7, .7)) == "#b2b2b2"
+ # hex roundtrip.
+ hex_color = "#1234abcd"
+ assert mcolors.to_hex(mcolors.to_rgba(hex_color), keep_alpha=True) == \
+ hex_color
+
+
+def test_conversions_masked():
+ x1 = np.ma.array(['k', 'b'], mask=[True, False])
+ x2 = np.ma.array([[0, 0, 0, 1], [0, 0, 1, 1]])
+ x2[0] = np.ma.masked
+ assert mcolors.to_rgba(x1[0]) == (0, 0, 0, 0)
+ assert_array_equal(mcolors.to_rgba_array(x1),
+ [[0, 0, 0, 0], [0, 0, 1, 1]])
+ assert_array_equal(mcolors.to_rgba_array(x2), mcolors.to_rgba_array(x1))
+
+
+def test_to_rgba_array_single_str():
+ # single color name is valid
+ assert_array_equal(mcolors.to_rgba_array("red"), [(1, 0, 0, 1)])
+
+ # single char color sequence is invalid
+ with pytest.raises(ValueError,
+ match="'rgb' is not a valid color value."):
+ array = mcolors.to_rgba_array("rgb")
+
+
+def test_to_rgba_array_2tuple_str():
+ expected = np.array([[0, 0, 0, 1], [1, 1, 1, 1]])
+ assert_array_equal(mcolors.to_rgba_array(("k", "w")), expected)
+
+
+def test_to_rgba_array_alpha_array():
+ with pytest.raises(ValueError, match="The number of colors must match"):
+ mcolors.to_rgba_array(np.ones((5, 3), float), alpha=np.ones((2,)))
+ alpha = [0.5, 0.6]
+ c = mcolors.to_rgba_array(np.ones((2, 3), float), alpha=alpha)
+ assert_array_equal(c[:, 3], alpha)
+ c = mcolors.to_rgba_array(['r', 'g'], alpha=alpha)
+ assert_array_equal(c[:, 3], alpha)
+
+
+def test_to_rgba_array_accepts_color_alpha_tuple():
+ assert_array_equal(
+ mcolors.to_rgba_array(('black', 0.9)),
+ [[0, 0, 0, 0.9]])
+
+
+def test_to_rgba_array_explicit_alpha_overrides_tuple_alpha():
+ assert_array_equal(
+ mcolors.to_rgba_array(('black', 0.9), alpha=0.5),
+ [[0, 0, 0, 0.5]])
+
+
+def test_to_rgba_array_accepts_color_alpha_tuple_with_multiple_colors():
+ color_array = np.array([[1., 1., 1., 1.], [0., 0., 1., 0.]])
+ assert_array_equal(
+ mcolors.to_rgba_array((color_array, 0.2)),
+ [[1., 1., 1., 0.2], [0., 0., 1., 0.2]])
+
+ color_sequence = [[1., 1., 1., 1.], [0., 0., 1., 0.]]
+ assert_array_equal(
+ mcolors.to_rgba_array((color_sequence, 0.4)),
+ [[1., 1., 1., 0.4], [0., 0., 1., 0.4]])
+
+
+def test_to_rgba_array_error_with_color_invalid_alpha_tuple():
+ with pytest.raises(ValueError, match="'alpha' must be between 0 and 1,"):
+ mcolors.to_rgba_array(('black', 2.0))
+
+
+@pytest.mark.parametrize('rgba_alpha',
+ [('white', 0.5), ('#ffffff', 0.5), ('#ffffff00', 0.5),
+ ((1.0, 1.0, 1.0, 1.0), 0.5)])
+def test_to_rgba_accepts_color_alpha_tuple(rgba_alpha):
+ assert mcolors.to_rgba(rgba_alpha) == (1, 1, 1, 0.5)
+
+
+def test_to_rgba_explicit_alpha_overrides_tuple_alpha():
+ assert mcolors.to_rgba(('red', 0.1), alpha=0.9) == (1, 0, 0, 0.9)
+
+
+def test_to_rgba_error_with_color_invalid_alpha_tuple():
+ with pytest.raises(ValueError, match="'alpha' must be between 0 and 1"):
+ mcolors.to_rgba(('blue', 2.0))
+
+
+@pytest.mark.parametrize("bytes", (True, False))
+def test_scalarmappable_to_rgba(bytes):
+ sm = cm.ScalarMappable()
+ alpha_1 = 255 if bytes else 1
+
+ # uint8 RGBA
+ x = np.ones((2, 3, 4), dtype=np.uint8)
+ expected = x.copy() if bytes else x.astype(np.float32)/255
+ np.testing.assert_almost_equal(sm.to_rgba(x, bytes=bytes), expected)
+ # uint8 RGB
+ expected[..., 3] = alpha_1
+ np.testing.assert_almost_equal(sm.to_rgba(x[..., :3], bytes=bytes), expected)
+ # uint8 masked RGBA
+ xm = np.ma.masked_array(x, mask=np.zeros_like(x))
+ xm.mask[0, 0, 0] = True
+ expected = x.copy() if bytes else x.astype(np.float32)/255
+ expected[0, 0, 3] = 0
+ np.testing.assert_almost_equal(sm.to_rgba(xm, bytes=bytes), expected)
+ # uint8 masked RGB
+ expected[..., 3] = alpha_1
+ expected[0, 0, 3] = 0
+ np.testing.assert_almost_equal(sm.to_rgba(xm[..., :3], bytes=bytes), expected)
+
+ # float RGBA
+ x = np.ones((2, 3, 4), dtype=float) * 0.5
+ expected = (x * 255).astype(np.uint8) if bytes else x.copy()
+ np.testing.assert_almost_equal(sm.to_rgba(x, bytes=bytes), expected)
+ # float RGB
+ expected[..., 3] = alpha_1
+ np.testing.assert_almost_equal(sm.to_rgba(x[..., :3], bytes=bytes), expected)
+ # float masked RGBA
+ xm = np.ma.masked_array(x, mask=np.zeros_like(x))
+ xm.mask[0, 0, 0] = True
+ expected = (x * 255).astype(np.uint8) if bytes else x.copy()
+ expected[0, 0, 3] = 0
+ np.testing.assert_almost_equal(sm.to_rgba(xm, bytes=bytes), expected)
+ # float masked RGB
+ expected[..., 3] = alpha_1
+ expected[0, 0, 3] = 0
+ np.testing.assert_almost_equal(sm.to_rgba(xm[..., :3], bytes=bytes), expected)
+
+
+@pytest.mark.parametrize("bytes", (True, False))
+def test_scalarmappable_nan_to_rgba(bytes):
+ sm = cm.ScalarMappable()
+
+ # RGBA
+ x = np.ones((2, 3, 4), dtype=float) * 0.5
+ x[0, 0, 0] = np.nan
+ expected = x.copy()
+ expected[0, 0, :] = 0
+ if bytes:
+ expected = (expected * 255).astype(np.uint8)
+ np.testing.assert_almost_equal(sm.to_rgba(x, bytes=bytes), expected)
+ assert np.any(np.isnan(x)) # Input array should not be changed
+
+ # RGB
+ expected[..., 3] = 255 if bytes else 1
+ expected[0, 0, 3] = 0
+ np.testing.assert_almost_equal(sm.to_rgba(x[..., :3], bytes=bytes), expected)
+ assert np.any(np.isnan(x)) # Input array should not be changed
+
+ # Out-of-range fail
+ x[1, 0, 0] = 42
+ with pytest.raises(ValueError, match='0..1 range'):
+ sm.to_rgba(x[..., :3], bytes=bytes)
+
+
+def test_failed_conversions():
+ with pytest.raises(ValueError):
+ mcolors.to_rgba('5')
+ with pytest.raises(ValueError):
+ mcolors.to_rgba('-1')
+ with pytest.raises(ValueError):
+ mcolors.to_rgba('nan')
+ with pytest.raises(ValueError):
+ mcolors.to_rgba('unknown_color')
+ with pytest.raises(ValueError):
+ # Gray must be a string to distinguish 3-4 grays from RGB or RGBA.
+ mcolors.to_rgba(0.4)
+
+
+def test_grey_gray():
+ color_mapping = mcolors._colors_full_map
+ for k in color_mapping.keys():
+ if 'grey' in k:
+ assert color_mapping[k] == color_mapping[k.replace('grey', 'gray')]
+ if 'gray' in k:
+ assert color_mapping[k] == color_mapping[k.replace('gray', 'grey')]
+
+
+def test_tableau_order():
+ dflt_cycle = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728',
+ '#9467bd', '#8c564b', '#e377c2', '#7f7f7f',
+ '#bcbd22', '#17becf']
+
+ assert list(mcolors.TABLEAU_COLORS.values()) == dflt_cycle
+
+
+def test_ndarray_subclass_norm():
+ # Emulate an ndarray subclass that handles units
+ # which objects when adding or subtracting with other
+ # arrays. See #6622 and #8696
+ class MyArray(np.ndarray):
+ def __isub__(self, other): # type: ignore[misc]
+ raise RuntimeError
+
+ def __add__(self, other):
+ raise RuntimeError
+
+ data = np.arange(-10, 10, 1, dtype=float).reshape((10, 2))
+ mydata = data.view(MyArray)
+
+ for norm in [mcolors.Normalize(), mcolors.LogNorm(),
+ mcolors.SymLogNorm(3, vmax=5, linscale=1, base=np.e),
+ mcolors.Normalize(vmin=mydata.min(), vmax=mydata.max()),
+ mcolors.SymLogNorm(3, vmin=mydata.min(), vmax=mydata.max(),
+ base=np.e),
+ mcolors.PowerNorm(1)]:
+ assert_array_equal(norm(mydata), norm(data))
+ fig, ax = plt.subplots()
+ ax.imshow(mydata, norm=norm)
+ fig.canvas.draw() # Check that no warning is emitted.
+
+
+def test_same_color():
+ assert mcolors.same_color('k', (0, 0, 0))
+ assert not mcolors.same_color('w', (1, 1, 0))
+ assert mcolors.same_color(['red', 'blue'], ['r', 'b'])
+ assert mcolors.same_color('none', 'none')
+ assert not mcolors.same_color('none', 'red')
+ with pytest.raises(ValueError):
+ mcolors.same_color(['r', 'g', 'b'], ['r'])
+ with pytest.raises(ValueError):
+ mcolors.same_color(['red', 'green'], 'none')
+
+
+def test_hex_shorthand_notation():
+ assert mcolors.same_color("#123", "#112233")
+ assert mcolors.same_color("#123a", "#112233aa")
+
+
+def test_repr_png():
+ cmap = mpl.colormaps['viridis']
+ png = cmap._repr_png_()
+ assert len(png) > 0
+ img = Image.open(BytesIO(png))
+ assert img.width > 0
+ assert img.height > 0
+ assert 'Title' in img.text
+ assert 'Description' in img.text
+ assert 'Author' in img.text
+ assert 'Software' in img.text
+
+
+def test_repr_html():
+ cmap = mpl.colormaps['viridis']
+ html = cmap._repr_html_()
+ assert len(html) > 0
+ png = cmap._repr_png_()
+ assert base64.b64encode(png).decode('ascii') in html
+ assert cmap.name in html
+ assert html.startswith('')
+
+
+def test_get_under_over_bad():
+ cmap = mpl.colormaps['viridis']
+ assert_array_equal(cmap.get_under(), cmap(-np.inf))
+ assert_array_equal(cmap.get_over(), cmap(np.inf))
+ assert_array_equal(cmap.get_bad(), cmap(np.nan))
+
+
+@pytest.mark.parametrize('kind', ('over', 'under', 'bad'))
+def test_non_mutable_get_values(kind):
+ cmap = copy.copy(mpl.colormaps['viridis'])
+ init_value = getattr(cmap, f'get_{kind}')()
+ getattr(cmap, f'set_{kind}')('k')
+ black_value = getattr(cmap, f'get_{kind}')()
+ assert np.all(black_value == [0, 0, 0, 1])
+ assert not np.all(init_value == black_value)
+
+
+def test_colormap_alpha_array():
+ cmap = mpl.colormaps['viridis']
+ vals = [-1, 0.5, 2] # under, valid, over
+ with pytest.raises(ValueError, match="alpha is array-like but"):
+ cmap(vals, alpha=[1, 1, 1, 1])
+ alpha = np.array([0.1, 0.2, 0.3])
+ c = cmap(vals, alpha=alpha)
+ assert_array_equal(c[:, -1], alpha)
+ c = cmap(vals, alpha=alpha, bytes=True)
+ assert_array_equal(c[:, -1], (alpha * 255).astype(np.uint8))
+
+
+def test_colormap_bad_data_with_alpha():
+ cmap = mpl.colormaps['viridis']
+ c = cmap(np.nan, alpha=0.5)
+ assert c == (0, 0, 0, 0)
+ c = cmap([0.5, np.nan], alpha=0.5)
+ assert_array_equal(c[1], (0, 0, 0, 0))
+ c = cmap([0.5, np.nan], alpha=[0.1, 0.2])
+ assert_array_equal(c[1], (0, 0, 0, 0))
+ c = cmap([[np.nan, 0.5], [0, 0]], alpha=0.5)
+ assert_array_equal(c[0, 0], (0, 0, 0, 0))
+ c = cmap([[np.nan, 0.5], [0, 0]], alpha=np.full((2, 2), 0.5))
+ assert_array_equal(c[0, 0], (0, 0, 0, 0))
+
+
+def test_2d_to_rgba():
+ color = np.array([0.1, 0.2, 0.3])
+ rgba_1d = mcolors.to_rgba(color.reshape(-1))
+ rgba_2d = mcolors.to_rgba(color.reshape((1, -1)))
+ assert rgba_1d == rgba_2d
+
+
+def test_set_dict_to_rgba():
+ # downstream libraries do this...
+ # note we can't test this because it is not well-ordered
+ # so just smoketest:
+ colors = {(0, .5, 1), (1, .2, .5), (.4, 1, .2)}
+ res = mcolors.to_rgba_array(colors)
+ palette = {"red": (1, 0, 0), "green": (0, 1, 0), "blue": (0, 0, 1)}
+ res = mcolors.to_rgba_array(palette.values())
+ exp = np.eye(3)
+ np.testing.assert_array_almost_equal(res[:, :-1], exp)
+
+
+def test_norm_deepcopy():
+ norm = mcolors.LogNorm()
+ norm.vmin = 0.0002
+ norm2 = copy.deepcopy(norm)
+ assert norm2.vmin == norm.vmin
+ assert isinstance(norm2._scale, mscale.LogScale)
+ norm = mcolors.Normalize()
+ norm.vmin = 0.0002
+ norm2 = copy.deepcopy(norm)
+ assert norm2._scale is None
+ assert norm2.vmin == norm.vmin
+
+
+def test_set_clim_emits_single_callback():
+ data = np.array([[1, 2], [3, 4]])
+ fig, ax = plt.subplots()
+ image = ax.imshow(data, cmap='viridis')
+
+ callback = unittest.mock.Mock()
+ image.norm.callbacks.connect('changed', callback)
+
+ callback.assert_not_called()
+
+ # Call set_clim() to update the limits
+ image.set_clim(1, 5)
+
+ # Assert that only one "changed" callback is sent after calling set_clim()
+ callback.assert_called_once()
+
+
+def test_norm_callback():
+ increment = unittest.mock.Mock(return_value=None)
+
+ norm = mcolors.Normalize()
+ norm.callbacks.connect('changed', increment)
+ # Haven't updated anything, so call count should be 0
+ assert increment.call_count == 0
+
+ # Now change vmin and vmax to test callbacks
+ norm.vmin = 1
+ assert increment.call_count == 1
+ norm.vmax = 5
+ assert increment.call_count == 2
+ # callback shouldn't be called if setting to the same value
+ norm.vmin = 1
+ assert increment.call_count == 2
+ norm.vmax = 5
+ assert increment.call_count == 2
+
+ # We only want autoscale() calls to send out one update signal
+ increment.call_count = 0
+ norm.autoscale([0, 1, 2])
+ assert increment.call_count == 1
+
+
+def test_scalarmappable_norm_update():
+ norm = mcolors.Normalize()
+ sm = matplotlib.cm.ScalarMappable(norm=norm, cmap='plasma')
+ # sm doesn't have a stale attribute at first, set it to False
+ sm.stale = False
+ # The mappable should be stale after updating vmin/vmax
+ norm.vmin = 5
+ assert sm.stale
+ sm.stale = False
+ norm.vmax = 5
+ assert sm.stale
+ sm.stale = False
+ norm.clip = True
+ assert sm.stale
+ # change to the CenteredNorm and TwoSlopeNorm to test those
+ # Also make sure that updating the norm directly and with
+ # set_norm both update the Norm callback
+ norm = mcolors.CenteredNorm()
+ sm.norm = norm
+ sm.stale = False
+ norm.vcenter = 1
+ assert sm.stale
+ norm = mcolors.TwoSlopeNorm(vcenter=0, vmin=-1, vmax=1)
+ sm.set_norm(norm)
+ sm.stale = False
+ norm.vcenter = 1
+ assert sm.stale
+
+
+@check_figures_equal(extensions=['png'])
+def test_norm_update_figs(fig_test, fig_ref):
+ ax_ref = fig_ref.add_subplot()
+ ax_test = fig_test.add_subplot()
+
+ z = np.arange(100).reshape((10, 10))
+ ax_ref.imshow(z, norm=mcolors.Normalize(10, 90))
+
+ # Create the norm beforehand with different limits and then update
+ # after adding to the plot
+ norm = mcolors.Normalize(0, 1)
+ ax_test.imshow(z, norm=norm)
+ # Force initial draw to make sure it isn't already stale
+ fig_test.canvas.draw()
+ norm.vmin, norm.vmax = 10, 90
+
+
+def test_make_norm_from_scale_name():
+ logitnorm = mcolors.make_norm_from_scale(
+ mscale.LogitScale, mcolors.Normalize)
+ assert logitnorm.__name__ == logitnorm.__qualname__ == "LogitScaleNorm"
+
+
+def test_color_sequences():
+ # basic access
+ assert plt.color_sequences is matplotlib.color_sequences # same registry
+ assert list(plt.color_sequences) == [
+ 'tab10', 'tab20', 'tab20b', 'tab20c', 'Pastel1', 'Pastel2', 'Paired',
+ 'Accent', 'Dark2', 'Set1', 'Set2', 'Set3', 'petroff10']
+ assert len(plt.color_sequences['tab10']) == 10
+ assert len(plt.color_sequences['tab20']) == 20
+
+ tab_colors = [
+ 'tab:blue', 'tab:orange', 'tab:green', 'tab:red', 'tab:purple',
+ 'tab:brown', 'tab:pink', 'tab:gray', 'tab:olive', 'tab:cyan']
+ for seq_color, tab_color in zip(plt.color_sequences['tab10'], tab_colors):
+ assert mcolors.same_color(seq_color, tab_color)
+
+ # registering
+ with pytest.raises(ValueError, match="reserved name"):
+ plt.color_sequences.register('tab10', ['r', 'g', 'b'])
+ with pytest.raises(ValueError, match="not a valid color specification"):
+ plt.color_sequences.register('invalid', ['not a color'])
+
+ rgb_colors = ['r', 'g', 'b']
+ plt.color_sequences.register('rgb', rgb_colors)
+ assert plt.color_sequences['rgb'] == ['r', 'g', 'b']
+ # should not affect the registered sequence because input is copied
+ rgb_colors.append('c')
+ assert plt.color_sequences['rgb'] == ['r', 'g', 'b']
+ # should not affect the registered sequence because returned list is a copy
+ plt.color_sequences['rgb'].append('c')
+ assert plt.color_sequences['rgb'] == ['r', 'g', 'b']
+
+ # unregister
+ plt.color_sequences.unregister('rgb')
+ with pytest.raises(KeyError):
+ plt.color_sequences['rgb'] # rgb is gone
+ plt.color_sequences.unregister('rgb') # multiple unregisters are ok
+ with pytest.raises(ValueError, match="Cannot unregister builtin"):
+ plt.color_sequences.unregister('tab10')
+
+
+def test_cm_set_cmap_error():
+ sm = cm.ScalarMappable()
+ # Pick a name we are pretty sure will never be a colormap name
+ bad_cmap = 'AardvarksAreAwkward'
+ with pytest.raises(ValueError, match=bad_cmap):
+ sm.set_cmap(bad_cmap)
+
+
+def test_set_cmap_mismatched_name():
+ cmap = matplotlib.colormaps["viridis"].with_extremes(over='r')
+ # register it with different names
+ cmap.name = "test-cmap"
+ matplotlib.colormaps.register(name='wrong-cmap', cmap=cmap)
+
+ plt.set_cmap("wrong-cmap")
+ cmap_returned = plt.get_cmap("wrong-cmap")
+ assert cmap_returned == cmap
+ assert cmap_returned.name == "wrong-cmap"
+
+
+def test_cmap_alias_names():
+ assert matplotlib.colormaps["gray"].name == "gray" # original
+ assert matplotlib.colormaps["grey"].name == "grey" # alias
+
+
+def test_to_rgba_array_none_color_with_alpha_param():
+ # effective alpha for color "none" must always be 0 to achieve a vanishing color
+ # even explicit alpha must be ignored
+ c = ["blue", "none"]
+ alpha = [1, 1]
+ assert_array_equal(
+ to_rgba_array(c, alpha), [[0., 0., 1., 1.], [0., 0., 0., 0.]]
+ )
+
+
+@pytest.mark.parametrize('input, expected',
+ [('red', True),
+ (('red', 0.5), True),
+ (('red', 2), False),
+ (['red', 0.5], False),
+ (('red', 'blue'), False),
+ (['red', 'blue'], False),
+ ('C3', True),
+ (('C3', 0.5), True)])
+def test_is_color_like(input, expected):
+ assert is_color_like(input) is expected
+
+
+def test_colorizer_vmin_vmax():
+ ca = mcolorizer.Colorizer()
+ assert ca.vmin is None
+ assert ca.vmax is None
+ ca.vmin = 1
+ ca.vmax = 3
+ assert ca.vmin == 1.0
+ assert ca.vmax == 3.0
+ assert ca.norm.vmin == 1.0
+ assert ca.norm.vmax == 3.0
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_compare_images.py b/lib/python3.12/site-packages/matplotlib/tests/test_compare_images.py
new file mode 100644
index 0000000000000000000000000000000000000000..6023f3d054682568c52256464c7ff7f4e525d7ba
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_compare_images.py
@@ -0,0 +1,73 @@
+from pathlib import Path
+import shutil
+
+import pytest
+from pytest import approx
+
+from matplotlib.testing.compare import compare_images
+from matplotlib.testing.decorators import _image_directories
+
+
+# Tests of the image comparison algorithm.
+@pytest.mark.parametrize(
+ 'im1, im2, tol, expect_rms',
+ [
+ # Comparison of an image and the same image with minor differences.
+ # This expects the images to compare equal under normal tolerance, and
+ # have a small RMS.
+ ('basn3p02.png', 'basn3p02-minorchange.png', 10, None),
+ # Now test with no tolerance.
+ ('basn3p02.png', 'basn3p02-minorchange.png', 0, 6.50646),
+ # Comparison with an image that is shifted by 1px in the X axis.
+ ('basn3p02.png', 'basn3p02-1px-offset.png', 0, 90.15611),
+ # Comparison with an image with half the pixels shifted by 1px in the X
+ # axis.
+ ('basn3p02.png', 'basn3p02-half-1px-offset.png', 0, 63.75),
+ # Comparison of an image and the same image scrambled.
+ # This expects the images to compare completely different, with a very
+ # large RMS.
+ # Note: The image has been scrambled in a specific way, by having
+ # each color component of each pixel randomly placed somewhere in the
+ # image. It contains exactly the same number of pixels of each color
+ # value of R, G and B, but in a totally different position.
+ # Test with no tolerance to make sure that we pick up even a very small
+ # RMS error.
+ ('basn3p02.png', 'basn3p02-scrambled.png', 0, 172.63582),
+ # Comparison of an image and a slightly brighter image.
+ # The two images are solid color, with the second image being exactly 1
+ # color value brighter.
+ # This expects the images to compare equal under normal tolerance, and
+ # have an RMS of exactly 1.
+ ('all127.png', 'all128.png', 0, 1),
+ # Now test the reverse comparison.
+ ('all128.png', 'all127.png', 0, 1),
+ ])
+def test_image_comparison_expect_rms(im1, im2, tol, expect_rms, tmp_path,
+ monkeypatch):
+ """
+ Compare two images, expecting a particular RMS error.
+
+ im1 and im2 are filenames relative to the baseline_dir directory.
+
+ tol is the tolerance to pass to compare_images.
+
+ expect_rms is the expected RMS value, or None. If None, the test will
+ succeed if compare_images succeeds. Otherwise, the test will succeed if
+ compare_images fails and returns an RMS error almost equal to this value.
+ """
+ # Change the working directory using monkeypatch to use a temporary
+ # test specific directory
+ monkeypatch.chdir(tmp_path)
+ baseline_dir, result_dir = map(Path, _image_directories(lambda: "dummy"))
+ # Copy "test" image to result_dir, so that compare_images writes
+ # the diff to result_dir, rather than to the source tree
+ result_im2 = result_dir / im1
+ shutil.copyfile(baseline_dir / im2, result_im2)
+ results = compare_images(
+ baseline_dir / im1, result_im2, tol=tol, in_decorator=True)
+
+ if expect_rms is None:
+ assert results is None
+ else:
+ assert results is not None
+ assert results['rms'] == approx(expect_rms, abs=1e-4)
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_constrainedlayout.py b/lib/python3.12/site-packages/matplotlib/tests/test_constrainedlayout.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c7dd43a31158ff38b4e12d86901ee3297fe2917
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_constrainedlayout.py
@@ -0,0 +1,723 @@
+import gc
+import platform
+
+import numpy as np
+import pytest
+
+import matplotlib as mpl
+from matplotlib.testing.decorators import image_comparison
+import matplotlib.pyplot as plt
+import matplotlib.transforms as mtransforms
+from matplotlib import gridspec, ticker
+
+
+pytestmark = [
+ pytest.mark.usefixtures('text_placeholders')
+]
+
+
+def example_plot(ax, fontsize=12, nodec=False):
+ ax.plot([1, 2])
+ ax.locator_params(nbins=3)
+ if not nodec:
+ ax.set_xlabel('x-label', fontsize=fontsize)
+ ax.set_ylabel('y-label', fontsize=fontsize)
+ ax.set_title('Title', fontsize=fontsize)
+ else:
+ ax.set_xticklabels([])
+ ax.set_yticklabels([])
+
+
+def example_pcolor(ax, fontsize=12):
+ dx, dy = 0.6, 0.6
+ y, x = np.mgrid[slice(-3, 3 + dy, dy),
+ slice(-3, 3 + dx, dx)]
+ z = (1 - x / 2. + x ** 5 + y ** 3) * np.exp(-x ** 2 - y ** 2)
+ pcm = ax.pcolormesh(x, y, z[:-1, :-1], cmap='RdBu_r', vmin=-1., vmax=1.,
+ rasterized=True)
+ ax.set_xlabel('x-label', fontsize=fontsize)
+ ax.set_ylabel('y-label', fontsize=fontsize)
+ ax.set_title('Title', fontsize=fontsize)
+ return pcm
+
+
+@image_comparison(['constrained_layout1.png'], style='mpl20')
+def test_constrained_layout1():
+ """Test constrained_layout for a single subplot"""
+ fig = plt.figure(layout="constrained")
+ ax = fig.add_subplot()
+ example_plot(ax, fontsize=24)
+
+
+@image_comparison(['constrained_layout2.png'], style='mpl20')
+def test_constrained_layout2():
+ """Test constrained_layout for 2x2 subplots"""
+ fig, axs = plt.subplots(2, 2, layout="constrained")
+ for ax in axs.flat:
+ example_plot(ax, fontsize=24)
+
+
+@image_comparison(['constrained_layout3.png'], style='mpl20')
+def test_constrained_layout3():
+ """Test constrained_layout for colorbars with subplots"""
+
+ fig, axs = plt.subplots(2, 2, layout="constrained")
+ for nn, ax in enumerate(axs.flat):
+ pcm = example_pcolor(ax, fontsize=24)
+ if nn == 3:
+ pad = 0.08
+ else:
+ pad = 0.02 # default
+ fig.colorbar(pcm, ax=ax, pad=pad)
+
+
+@image_comparison(['constrained_layout4.png'], style='mpl20')
+def test_constrained_layout4():
+ """Test constrained_layout for a single colorbar with subplots"""
+
+ fig, axs = plt.subplots(2, 2, layout="constrained")
+ for ax in axs.flat:
+ pcm = example_pcolor(ax, fontsize=24)
+ fig.colorbar(pcm, ax=axs, pad=0.01, shrink=0.6)
+
+
+@image_comparison(['constrained_layout5.png'], style='mpl20')
+def test_constrained_layout5():
+ """
+ Test constrained_layout for a single colorbar with subplots,
+ colorbar bottom
+ """
+
+ fig, axs = plt.subplots(2, 2, layout="constrained")
+ for ax in axs.flat:
+ pcm = example_pcolor(ax, fontsize=24)
+ fig.colorbar(pcm, ax=axs,
+ use_gridspec=False, pad=0.01, shrink=0.6,
+ location='bottom')
+
+
+@image_comparison(['constrained_layout6.png'], style='mpl20')
+def test_constrained_layout6():
+ """Test constrained_layout for nested gridspecs"""
+ fig = plt.figure(layout="constrained")
+ gs = fig.add_gridspec(1, 2, figure=fig)
+ gsl = gs[0].subgridspec(2, 2)
+ gsr = gs[1].subgridspec(1, 2)
+ axsl = []
+ for gs in gsl:
+ ax = fig.add_subplot(gs)
+ axsl += [ax]
+ example_plot(ax, fontsize=12)
+ ax.set_xlabel('x-label\nMultiLine')
+ axsr = []
+ for gs in gsr:
+ ax = fig.add_subplot(gs)
+ axsr += [ax]
+ pcm = example_pcolor(ax, fontsize=12)
+
+ fig.colorbar(pcm, ax=axsr,
+ pad=0.01, shrink=0.99, location='bottom',
+ ticks=ticker.MaxNLocator(nbins=5))
+
+
+def test_identical_subgridspec():
+
+ fig = plt.figure(constrained_layout=True)
+
+ GS = fig.add_gridspec(2, 1)
+
+ GSA = GS[0].subgridspec(1, 3)
+ GSB = GS[1].subgridspec(1, 3)
+
+ axa = []
+ axb = []
+ for i in range(3):
+ axa += [fig.add_subplot(GSA[i])]
+ axb += [fig.add_subplot(GSB[i])]
+
+ fig.draw_without_rendering()
+ # check first row above second
+ assert axa[0].get_position().y0 > axb[0].get_position().y1
+
+
+def test_constrained_layout7():
+ """Test for proper warning if fig not set in GridSpec"""
+ with pytest.warns(
+ UserWarning, match=('There are no gridspecs with layoutgrids. '
+ 'Possibly did not call parent GridSpec with '
+ 'the "figure" keyword')):
+ fig = plt.figure(layout="constrained")
+ gs = gridspec.GridSpec(1, 2)
+ gsl = gridspec.GridSpecFromSubplotSpec(2, 2, gs[0])
+ gsr = gridspec.GridSpecFromSubplotSpec(1, 2, gs[1])
+ for gs in gsl:
+ fig.add_subplot(gs)
+ # need to trigger a draw to get warning
+ fig.draw_without_rendering()
+
+
+@image_comparison(['constrained_layout8.png'], style='mpl20')
+def test_constrained_layout8():
+ """Test for gridspecs that are not completely full"""
+
+ fig = plt.figure(figsize=(10, 5), layout="constrained")
+ gs = gridspec.GridSpec(3, 5, figure=fig)
+ axs = []
+ for j in [0, 1]:
+ if j == 0:
+ ilist = [1]
+ else:
+ ilist = [0, 4]
+ for i in ilist:
+ ax = fig.add_subplot(gs[j, i])
+ axs += [ax]
+ example_pcolor(ax, fontsize=9)
+ if i > 0:
+ ax.set_ylabel('')
+ if j < 1:
+ ax.set_xlabel('')
+ ax.set_title('')
+ ax = fig.add_subplot(gs[2, :])
+ axs += [ax]
+ pcm = example_pcolor(ax, fontsize=9)
+
+ fig.colorbar(pcm, ax=axs, pad=0.01, shrink=0.6)
+
+
+@image_comparison(['constrained_layout9.png'], style='mpl20')
+def test_constrained_layout9():
+ """Test for handling suptitle and for sharex and sharey"""
+
+ fig, axs = plt.subplots(2, 2, layout="constrained",
+ sharex=False, sharey=False)
+ for ax in axs.flat:
+ pcm = example_pcolor(ax, fontsize=24)
+ ax.set_xlabel('')
+ ax.set_ylabel('')
+ ax.set_aspect(2.)
+ fig.colorbar(pcm, ax=axs, pad=0.01, shrink=0.6)
+ fig.suptitle('Test Suptitle', fontsize=28)
+
+
+@image_comparison(['constrained_layout10.png'], style='mpl20',
+ tol=0 if platform.machine() == 'x86_64' else 0.032)
+def test_constrained_layout10():
+ """Test for handling legend outside axis"""
+ fig, axs = plt.subplots(2, 2, layout="constrained")
+ for ax in axs.flat:
+ ax.plot(np.arange(12), label='This is a label')
+ ax.legend(loc='center left', bbox_to_anchor=(0.8, 0.5))
+
+
+@image_comparison(['constrained_layout11.png'], style='mpl20')
+def test_constrained_layout11():
+ """Test for multiple nested gridspecs"""
+
+ fig = plt.figure(layout="constrained", figsize=(13, 3))
+ gs0 = gridspec.GridSpec(1, 2, figure=fig)
+ gsl = gridspec.GridSpecFromSubplotSpec(1, 2, gs0[0])
+ gsl0 = gridspec.GridSpecFromSubplotSpec(2, 2, gsl[1])
+ ax = fig.add_subplot(gs0[1])
+ example_plot(ax, fontsize=9)
+ axs = []
+ for gs in gsl0:
+ ax = fig.add_subplot(gs)
+ axs += [ax]
+ pcm = example_pcolor(ax, fontsize=9)
+ fig.colorbar(pcm, ax=axs, shrink=0.6, aspect=70.)
+ ax = fig.add_subplot(gsl[0])
+ example_plot(ax, fontsize=9)
+
+
+@image_comparison(['constrained_layout11rat.png'], style='mpl20')
+def test_constrained_layout11rat():
+ """Test for multiple nested gridspecs with width_ratios"""
+
+ fig = plt.figure(layout="constrained", figsize=(10, 3))
+ gs0 = gridspec.GridSpec(1, 2, figure=fig, width_ratios=[6, 1])
+ gsl = gridspec.GridSpecFromSubplotSpec(1, 2, gs0[0])
+ gsl0 = gridspec.GridSpecFromSubplotSpec(2, 2, gsl[1], height_ratios=[2, 1])
+ ax = fig.add_subplot(gs0[1])
+ example_plot(ax, fontsize=9)
+ axs = []
+ for gs in gsl0:
+ ax = fig.add_subplot(gs)
+ axs += [ax]
+ pcm = example_pcolor(ax, fontsize=9)
+ fig.colorbar(pcm, ax=axs, shrink=0.6, aspect=70.)
+ ax = fig.add_subplot(gsl[0])
+ example_plot(ax, fontsize=9)
+
+
+@image_comparison(['constrained_layout12.png'], style='mpl20')
+def test_constrained_layout12():
+ """Test that very unbalanced labeling still works."""
+ fig = plt.figure(layout="constrained", figsize=(6, 8))
+
+ gs0 = gridspec.GridSpec(6, 2, figure=fig)
+
+ ax1 = fig.add_subplot(gs0[:3, 1])
+ ax2 = fig.add_subplot(gs0[3:, 1])
+
+ example_plot(ax1, fontsize=18)
+ example_plot(ax2, fontsize=18)
+
+ ax = fig.add_subplot(gs0[0:2, 0])
+ example_plot(ax, nodec=True)
+ ax = fig.add_subplot(gs0[2:4, 0])
+ example_plot(ax, nodec=True)
+ ax = fig.add_subplot(gs0[4:, 0])
+ example_plot(ax, nodec=True)
+ ax.set_xlabel('x-label')
+
+
+@image_comparison(['constrained_layout13.png'], style='mpl20')
+def test_constrained_layout13():
+ """Test that padding works."""
+ fig, axs = plt.subplots(2, 2, layout="constrained")
+ for ax in axs.flat:
+ pcm = example_pcolor(ax, fontsize=12)
+ fig.colorbar(pcm, ax=ax, shrink=0.6, aspect=20., pad=0.02)
+ with pytest.raises(TypeError):
+ fig.get_layout_engine().set(wpad=1, hpad=2)
+ fig.get_layout_engine().set(w_pad=24./72., h_pad=24./72.)
+
+
+@image_comparison(['constrained_layout14.png'], style='mpl20')
+def test_constrained_layout14():
+ """Test that padding works."""
+ fig, axs = plt.subplots(2, 2, layout="constrained")
+ for ax in axs.flat:
+ pcm = example_pcolor(ax, fontsize=12)
+ fig.colorbar(pcm, ax=ax, shrink=0.6, aspect=20., pad=0.02)
+ fig.get_layout_engine().set(
+ w_pad=3./72., h_pad=3./72.,
+ hspace=0.2, wspace=0.2)
+
+
+@image_comparison(['constrained_layout15.png'], style='mpl20')
+def test_constrained_layout15():
+ """Test that rcparams work."""
+ mpl.rcParams['figure.constrained_layout.use'] = True
+ fig, axs = plt.subplots(2, 2)
+ for ax in axs.flat:
+ example_plot(ax, fontsize=12)
+
+
+@image_comparison(['constrained_layout16.png'], style='mpl20')
+def test_constrained_layout16():
+ """Test ax.set_position."""
+ fig, ax = plt.subplots(layout="constrained")
+ example_plot(ax, fontsize=12)
+ ax2 = fig.add_axes([0.2, 0.2, 0.4, 0.4])
+
+
+@image_comparison(['constrained_layout17.png'], style='mpl20')
+def test_constrained_layout17():
+ """Test uneven gridspecs"""
+ fig = plt.figure(layout="constrained")
+ gs = gridspec.GridSpec(3, 3, figure=fig)
+
+ ax1 = fig.add_subplot(gs[0, 0])
+ ax2 = fig.add_subplot(gs[0, 1:])
+ ax3 = fig.add_subplot(gs[1:, 0:2])
+ ax4 = fig.add_subplot(gs[1:, -1])
+
+ example_plot(ax1)
+ example_plot(ax2)
+ example_plot(ax3)
+ example_plot(ax4)
+
+
+def test_constrained_layout18():
+ """Test twinx"""
+ fig, ax = plt.subplots(layout="constrained")
+ ax2 = ax.twinx()
+ example_plot(ax)
+ example_plot(ax2, fontsize=24)
+ fig.draw_without_rendering()
+ assert all(ax.get_position().extents == ax2.get_position().extents)
+
+
+def test_constrained_layout19():
+ """Test twiny"""
+ fig, ax = plt.subplots(layout="constrained")
+ ax2 = ax.twiny()
+ example_plot(ax)
+ example_plot(ax2, fontsize=24)
+ ax2.set_title('')
+ ax.set_title('')
+ fig.draw_without_rendering()
+ assert all(ax.get_position().extents == ax2.get_position().extents)
+
+
+def test_constrained_layout20():
+ """Smoke test cl does not mess up added Axes"""
+ gx = np.linspace(-5, 5, 4)
+ img = np.hypot(gx, gx[:, None])
+
+ fig = plt.figure()
+ ax = fig.add_axes([0, 0, 1, 1])
+ mesh = ax.pcolormesh(gx, gx, img[:-1, :-1])
+ fig.colorbar(mesh)
+
+
+def test_constrained_layout21():
+ """#11035: repeated calls to suptitle should not alter the layout"""
+ fig, ax = plt.subplots(layout="constrained")
+
+ fig.suptitle("Suptitle0")
+ fig.draw_without_rendering()
+ extents0 = np.copy(ax.get_position().extents)
+
+ fig.suptitle("Suptitle1")
+ fig.draw_without_rendering()
+ extents1 = np.copy(ax.get_position().extents)
+
+ np.testing.assert_allclose(extents0, extents1)
+
+
+def test_constrained_layout22():
+ """#11035: suptitle should not be include in CL if manually positioned"""
+ fig, ax = plt.subplots(layout="constrained")
+
+ fig.draw_without_rendering()
+ extents0 = np.copy(ax.get_position().extents)
+
+ fig.suptitle("Suptitle", y=0.5)
+ fig.draw_without_rendering()
+ extents1 = np.copy(ax.get_position().extents)
+
+ np.testing.assert_allclose(extents0, extents1)
+
+
+def test_constrained_layout23():
+ """
+ Comment in #11035: suptitle used to cause an exception when
+ reusing a figure w/ CL with ``clear=True``.
+ """
+
+ for i in range(2):
+ fig = plt.figure(layout="constrained", clear=True, num="123")
+ gs = fig.add_gridspec(1, 2)
+ sub = gs[0].subgridspec(2, 2)
+ fig.suptitle(f"Suptitle{i}")
+
+
+@image_comparison(['test_colorbar_location.png'],
+ remove_text=True, style='mpl20')
+def test_colorbar_location():
+ """
+ Test that colorbar handling is as expected for various complicated
+ cases...
+ """
+ # Remove this line when this test image is regenerated.
+ plt.rcParams['pcolormesh.snap'] = False
+
+ fig, axs = plt.subplots(4, 5, layout="constrained")
+ for ax in axs.flat:
+ pcm = example_pcolor(ax)
+ ax.set_xlabel('')
+ ax.set_ylabel('')
+ fig.colorbar(pcm, ax=axs[:, 1], shrink=0.4)
+ fig.colorbar(pcm, ax=axs[-1, :2], shrink=0.5, location='bottom')
+ fig.colorbar(pcm, ax=axs[0, 2:], shrink=0.5, location='bottom', pad=0.05)
+ fig.colorbar(pcm, ax=axs[-2, 3:], shrink=0.5, location='top')
+ fig.colorbar(pcm, ax=axs[0, 0], shrink=0.5, location='left')
+ fig.colorbar(pcm, ax=axs[1:3, 2], shrink=0.5, location='right')
+
+
+def test_hidden_axes():
+ # test that if we make an Axes not visible that constrained_layout
+ # still works. Note the axes still takes space in the layout
+ # (as does a gridspec slot that is empty)
+ fig, axs = plt.subplots(2, 2, layout="constrained")
+ axs[0, 1].set_visible(False)
+ fig.draw_without_rendering()
+ extents1 = np.copy(axs[0, 0].get_position().extents)
+
+ np.testing.assert_allclose(
+ extents1, [0.046918, 0.541204, 0.477409, 0.980555], rtol=1e-5)
+
+
+def test_colorbar_align():
+ for location in ['right', 'left', 'top', 'bottom']:
+ fig, axs = plt.subplots(2, 2, layout="constrained")
+ cbs = []
+ for nn, ax in enumerate(axs.flat):
+ ax.tick_params(direction='in')
+ pc = example_pcolor(ax)
+ cb = fig.colorbar(pc, ax=ax, location=location, shrink=0.6,
+ pad=0.04)
+ cbs += [cb]
+ cb.ax.tick_params(direction='in')
+ if nn != 1:
+ cb.ax.xaxis.set_ticks([])
+ cb.ax.yaxis.set_ticks([])
+ ax.set_xticklabels([])
+ ax.set_yticklabels([])
+ fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72,
+ hspace=0.1, wspace=0.1)
+
+ fig.draw_without_rendering()
+ if location in ['left', 'right']:
+ np.testing.assert_allclose(cbs[0].ax.get_position().x0,
+ cbs[2].ax.get_position().x0)
+ np.testing.assert_allclose(cbs[1].ax.get_position().x0,
+ cbs[3].ax.get_position().x0)
+ else:
+ np.testing.assert_allclose(cbs[0].ax.get_position().y0,
+ cbs[1].ax.get_position().y0)
+ np.testing.assert_allclose(cbs[2].ax.get_position().y0,
+ cbs[3].ax.get_position().y0)
+
+
+@image_comparison(['test_colorbars_no_overlapV.png'], style='mpl20')
+def test_colorbars_no_overlapV():
+ fig = plt.figure(figsize=(2, 4), layout="constrained")
+ axs = fig.subplots(2, 1, sharex=True, sharey=True)
+ for ax in axs:
+ ax.yaxis.set_major_formatter(ticker.NullFormatter())
+ ax.tick_params(axis='both', direction='in')
+ im = ax.imshow([[1, 2], [3, 4]])
+ fig.colorbar(im, ax=ax, orientation="vertical")
+ fig.suptitle("foo")
+
+
+@image_comparison(['test_colorbars_no_overlapH.png'], style='mpl20')
+def test_colorbars_no_overlapH():
+ fig = plt.figure(figsize=(4, 2), layout="constrained")
+ fig.suptitle("foo")
+ axs = fig.subplots(1, 2, sharex=True, sharey=True)
+ for ax in axs:
+ ax.yaxis.set_major_formatter(ticker.NullFormatter())
+ ax.tick_params(axis='both', direction='in')
+ im = ax.imshow([[1, 2], [3, 4]])
+ fig.colorbar(im, ax=ax, orientation="horizontal")
+
+
+def test_manually_set_position():
+ fig, axs = plt.subplots(1, 2, layout="constrained")
+ axs[0].set_position([0.2, 0.2, 0.3, 0.3])
+ fig.draw_without_rendering()
+ pp = axs[0].get_position()
+ np.testing.assert_allclose(pp, [[0.2, 0.2], [0.5, 0.5]])
+
+ fig, axs = plt.subplots(1, 2, layout="constrained")
+ axs[0].set_position([0.2, 0.2, 0.3, 0.3])
+ pc = axs[0].pcolormesh(np.random.rand(20, 20))
+ fig.colorbar(pc, ax=axs[0])
+ fig.draw_without_rendering()
+ pp = axs[0].get_position()
+ np.testing.assert_allclose(pp, [[0.2, 0.2], [0.44, 0.5]])
+
+
+@image_comparison(['test_bboxtight.png'],
+ remove_text=True, style='mpl20',
+ savefig_kwarg={'bbox_inches': 'tight'})
+def test_bboxtight():
+ fig, ax = plt.subplots(layout="constrained")
+ ax.set_aspect(1.)
+
+
+@image_comparison(['test_bbox.png'],
+ remove_text=True, style='mpl20',
+ savefig_kwarg={'bbox_inches':
+ mtransforms.Bbox([[0.5, 0], [2.5, 2]])})
+def test_bbox():
+ fig, ax = plt.subplots(layout="constrained")
+ ax.set_aspect(1.)
+
+
+def test_align_labels():
+ """
+ Tests for a bug in which constrained layout and align_ylabels on
+ three unevenly sized subplots, one of whose y tick labels include
+ negative numbers, drives the non-negative subplots' y labels off
+ the edge of the plot
+ """
+ fig, (ax3, ax1, ax2) = plt.subplots(3, 1, layout="constrained",
+ figsize=(6.4, 8),
+ gridspec_kw={"height_ratios": (1, 1,
+ 0.7)})
+
+ ax1.set_ylim(0, 1)
+ ax1.set_ylabel("Label")
+
+ ax2.set_ylim(-1.5, 1.5)
+ ax2.set_ylabel("Label")
+
+ ax3.set_ylim(0, 1)
+ ax3.set_ylabel("Label")
+
+ fig.align_ylabels(axs=(ax3, ax1, ax2))
+
+ fig.draw_without_rendering()
+ after_align = [ax1.yaxis.label.get_window_extent(),
+ ax2.yaxis.label.get_window_extent(),
+ ax3.yaxis.label.get_window_extent()]
+ # ensure labels are approximately aligned
+ np.testing.assert_allclose([after_align[0].x0, after_align[2].x0],
+ after_align[1].x0, rtol=0, atol=1e-05)
+ # ensure labels do not go off the edge
+ assert after_align[0].x0 >= 1
+
+
+def test_suplabels():
+ fig, ax = plt.subplots(layout="constrained")
+ fig.draw_without_rendering()
+ pos0 = ax.get_tightbbox(fig.canvas.get_renderer())
+ fig.supxlabel('Boo')
+ fig.supylabel('Booy')
+ fig.draw_without_rendering()
+ pos = ax.get_tightbbox(fig.canvas.get_renderer())
+ assert pos.y0 > pos0.y0 + 10.0
+ assert pos.x0 > pos0.x0 + 10.0
+
+ fig, ax = plt.subplots(layout="constrained")
+ fig.draw_without_rendering()
+ pos0 = ax.get_tightbbox(fig.canvas.get_renderer())
+ # check that specifying x (y) doesn't ruin the layout
+ fig.supxlabel('Boo', x=0.5)
+ fig.supylabel('Boo', y=0.5)
+ fig.draw_without_rendering()
+ pos = ax.get_tightbbox(fig.canvas.get_renderer())
+ assert pos.y0 > pos0.y0 + 10.0
+ assert pos.x0 > pos0.x0 + 10.0
+
+
+def test_gridspec_addressing():
+ fig = plt.figure()
+ gs = fig.add_gridspec(3, 3)
+ sp = fig.add_subplot(gs[0:, 1:])
+ fig.draw_without_rendering()
+
+
+def test_discouraged_api():
+ fig, ax = plt.subplots(constrained_layout=True)
+ fig.draw_without_rendering()
+
+ with pytest.warns(PendingDeprecationWarning,
+ match="will be deprecated"):
+ fig, ax = plt.subplots()
+ fig.set_constrained_layout(True)
+ fig.draw_without_rendering()
+
+ with pytest.warns(PendingDeprecationWarning,
+ match="will be deprecated"):
+ fig, ax = plt.subplots()
+ fig.set_constrained_layout({'w_pad': 0.02, 'h_pad': 0.02})
+ fig.draw_without_rendering()
+
+
+def test_kwargs():
+ fig, ax = plt.subplots(constrained_layout={'h_pad': 0.02})
+ fig.draw_without_rendering()
+
+
+def test_rect():
+ fig, ax = plt.subplots(layout='constrained')
+ fig.get_layout_engine().set(rect=[0, 0, 0.5, 0.5])
+ fig.draw_without_rendering()
+ ppos = ax.get_position()
+ assert ppos.x1 < 0.5
+ assert ppos.y1 < 0.5
+
+ fig, ax = plt.subplots(layout='constrained')
+ fig.get_layout_engine().set(rect=[0.2, 0.2, 0.3, 0.3])
+ fig.draw_without_rendering()
+ ppos = ax.get_position()
+ assert ppos.x1 < 0.5
+ assert ppos.y1 < 0.5
+ assert ppos.x0 > 0.2
+ assert ppos.y0 > 0.2
+
+
+def test_compressed1():
+ fig, axs = plt.subplots(3, 2, layout='compressed',
+ sharex=True, sharey=True)
+ for ax in axs.flat:
+ pc = ax.imshow(np.random.randn(20, 20))
+
+ fig.colorbar(pc, ax=axs)
+ fig.draw_without_rendering()
+
+ pos = axs[0, 0].get_position()
+ np.testing.assert_allclose(pos.x0, 0.2381, atol=1e-2)
+ pos = axs[0, 1].get_position()
+ np.testing.assert_allclose(pos.x1, 0.7024, atol=1e-3)
+
+ # wider than tall
+ fig, axs = plt.subplots(2, 3, layout='compressed',
+ sharex=True, sharey=True, figsize=(5, 4))
+ for ax in axs.flat:
+ pc = ax.imshow(np.random.randn(20, 20))
+
+ fig.colorbar(pc, ax=axs)
+ fig.draw_without_rendering()
+
+ pos = axs[0, 0].get_position()
+ np.testing.assert_allclose(pos.x0, 0.05653, atol=1e-3)
+ np.testing.assert_allclose(pos.y1, 0.8603, atol=1e-2)
+ pos = axs[1, 2].get_position()
+ np.testing.assert_allclose(pos.x1, 0.8728, atol=1e-3)
+ np.testing.assert_allclose(pos.y0, 0.1808, atol=1e-2)
+
+
+def test_compressed_suptitle():
+ fig, (ax0, ax1) = plt.subplots(
+ nrows=2, figsize=(4, 10), layout="compressed",
+ gridspec_kw={"height_ratios": (1 / 4, 3 / 4), "hspace": 0})
+
+ ax0.axis("equal")
+ ax0.set_box_aspect(1/3)
+
+ ax1.axis("equal")
+ ax1.set_box_aspect(1)
+
+ title = fig.suptitle("Title")
+ fig.draw_without_rendering()
+ assert title.get_position()[1] == pytest.approx(0.7491, abs=1e-3)
+
+ title = fig.suptitle("Title", y=0.98)
+ fig.draw_without_rendering()
+ assert title.get_position()[1] == 0.98
+
+ title = fig.suptitle("Title", in_layout=False)
+ fig.draw_without_rendering()
+ assert title.get_position()[1] == 0.98
+
+
+@pytest.mark.parametrize('arg, state', [
+ (True, True),
+ (False, False),
+ ({}, True),
+ ({'rect': None}, True)
+])
+def test_set_constrained_layout(arg, state):
+ fig, ax = plt.subplots(constrained_layout=arg)
+ assert fig.get_constrained_layout() is state
+
+
+def test_constrained_toggle():
+ fig, ax = plt.subplots()
+ with pytest.warns(PendingDeprecationWarning):
+ fig.set_constrained_layout(True)
+ assert fig.get_constrained_layout()
+ fig.set_constrained_layout(False)
+ assert not fig.get_constrained_layout()
+ fig.set_constrained_layout(True)
+ assert fig.get_constrained_layout()
+
+
+def test_layout_leak():
+ # Make sure there aren't any cyclic references when using LayoutGrid
+ # GH #25853
+ fig = plt.figure(constrained_layout=True, figsize=(10, 10))
+ fig.add_subplot()
+ fig.draw_without_rendering()
+ plt.close("all")
+ del fig
+ gc.collect()
+ assert not any(isinstance(obj, mpl._layoutgrid.LayoutGrid)
+ for obj in gc.get_objects())
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_datetime.py b/lib/python3.12/site-packages/matplotlib/tests/test_datetime.py
new file mode 100644
index 0000000000000000000000000000000000000000..276056d044ae3e114f506a26a2806d20135ecc4d
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_datetime.py
@@ -0,0 +1,865 @@
+import datetime
+import numpy as np
+
+import pytest
+
+import matplotlib.pyplot as plt
+import matplotlib as mpl
+
+
+class TestDatetimePlotting:
+ @mpl.style.context("default")
+ def test_annotate(self):
+ mpl.rcParams["date.converter"] = 'concise'
+ fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, layout="constrained")
+
+ start_date = datetime.datetime(2023, 10, 1)
+ dates = [start_date + datetime.timedelta(days=i) for i in range(31)]
+ data = list(range(1, 32))
+ test_text = "Test Text"
+
+ ax1.plot(dates, data)
+ ax1.annotate(text=test_text, xy=(dates[15], data[15]))
+ ax2.plot(data, dates)
+ ax2.annotate(text=test_text, xy=(data[5], dates[26]))
+ ax3.plot(dates, dates)
+ ax3.annotate(text=test_text, xy=(dates[15], dates[3]))
+ ax4.plot(dates, dates)
+ ax4.annotate(text=test_text, xy=(dates[5], dates[30]),
+ xytext=(dates[1], dates[7]), arrowprops=dict(facecolor='red'))
+
+ @pytest.mark.xfail(reason="Test for arrow not written yet")
+ @mpl.style.context("default")
+ def test_arrow(self):
+ fig, ax = plt.subplots()
+ ax.arrow(...)
+
+ @mpl.style.context("default")
+ def test_axhline(self):
+ mpl.rcParams["date.converter"] = 'concise'
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout='constrained')
+ ax1.set_ylim(bottom=datetime.datetime(2020, 4, 1),
+ top=datetime.datetime(2020, 8, 1))
+ ax2.set_ylim(bottom=np.datetime64('2005-01-01'),
+ top=np.datetime64('2005-04-01'))
+ ax3.set_ylim(bottom=datetime.datetime(2023, 9, 1),
+ top=datetime.datetime(2023, 11, 1))
+ ax1.axhline(y=datetime.datetime(2020, 6, 3), xmin=0.5, xmax=0.7)
+ ax2.axhline(np.datetime64('2005-02-25T03:30'), xmin=0.1, xmax=0.9)
+ ax3.axhline(y=datetime.datetime(2023, 10, 24), xmin=0.4, xmax=0.7)
+
+ @mpl.style.context("default")
+ def test_axhspan(self):
+ mpl.rcParams["date.converter"] = 'concise'
+
+ start_date = datetime.datetime(2023, 1, 1)
+ dates = [start_date + datetime.timedelta(days=i) for i in range(31)]
+ numbers = list(range(1, 32))
+
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1,
+ constrained_layout=True,
+ figsize=(10, 12))
+
+ ax1.plot(dates, numbers, marker='o', color='blue')
+ for i in range(0, 31, 2):
+ ax1.axhspan(ymin=i+1, ymax=i+2, facecolor='green', alpha=0.5)
+ ax1.set_title('Datetime vs. Number')
+ ax1.set_xlabel('Date')
+ ax1.set_ylabel('Number')
+
+ ax2.plot(numbers, dates, marker='o', color='blue')
+ for i in range(0, 31, 2):
+ ymin = start_date + datetime.timedelta(days=i)
+ ymax = ymin + datetime.timedelta(days=1)
+ ax2.axhspan(ymin=ymin, ymax=ymax, facecolor='green', alpha=0.5)
+ ax2.set_title('Number vs. Datetime')
+ ax2.set_xlabel('Number')
+ ax2.set_ylabel('Date')
+
+ ax3.plot(dates, dates, marker='o', color='blue')
+ for i in range(0, 31, 2):
+ ymin = start_date + datetime.timedelta(days=i)
+ ymax = ymin + datetime.timedelta(days=1)
+ ax3.axhspan(ymin=ymin, ymax=ymax, facecolor='green', alpha=0.5)
+ ax3.set_title('Datetime vs. Datetime')
+ ax3.set_xlabel('Date')
+ ax3.set_ylabel('Date')
+
+ @pytest.mark.xfail(reason="Test for axline not written yet")
+ @mpl.style.context("default")
+ def test_axline(self):
+ fig, ax = plt.subplots()
+ ax.axline(...)
+
+ @mpl.style.context("default")
+ def test_axvline(self):
+ mpl.rcParams["date.converter"] = 'concise'
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout='constrained')
+ ax1.set_xlim(left=datetime.datetime(2020, 4, 1),
+ right=datetime.datetime(2020, 8, 1))
+ ax2.set_xlim(left=np.datetime64('2005-01-01'),
+ right=np.datetime64('2005-04-01'))
+ ax3.set_xlim(left=datetime.datetime(2023, 9, 1),
+ right=datetime.datetime(2023, 11, 1))
+ ax1.axvline(x=datetime.datetime(2020, 6, 3), ymin=0.5, ymax=0.7)
+ ax2.axvline(np.datetime64('2005-02-25T03:30'), ymin=0.1, ymax=0.9)
+ ax3.axvline(x=datetime.datetime(2023, 10, 24), ymin=0.4, ymax=0.7)
+
+ @mpl.style.context("default")
+ def test_axvspan(self):
+ mpl.rcParams["date.converter"] = 'concise'
+
+ start_date = datetime.datetime(2023, 1, 1)
+ dates = [start_date + datetime.timedelta(days=i) for i in range(31)]
+ numbers = list(range(1, 32))
+
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1,
+ constrained_layout=True,
+ figsize=(10, 12))
+
+ ax1.plot(dates, numbers, marker='o', color='blue')
+ for i in range(0, 31, 2):
+ xmin = start_date + datetime.timedelta(days=i)
+ xmax = xmin + datetime.timedelta(days=1)
+ ax1.axvspan(xmin=xmin, xmax=xmax, facecolor='red', alpha=0.5)
+ ax1.set_title('Datetime vs. Number')
+ ax1.set_xlabel('Date')
+ ax1.set_ylabel('Number')
+
+ ax2.plot(numbers, dates, marker='o', color='blue')
+ for i in range(0, 31, 2):
+ ax2.axvspan(xmin=i+1, xmax=i+2, facecolor='red', alpha=0.5)
+ ax2.set_title('Number vs. Datetime')
+ ax2.set_xlabel('Number')
+ ax2.set_ylabel('Date')
+
+ ax3.plot(dates, dates, marker='o', color='blue')
+ for i in range(0, 31, 2):
+ xmin = start_date + datetime.timedelta(days=i)
+ xmax = xmin + datetime.timedelta(days=1)
+ ax3.axvspan(xmin=xmin, xmax=xmax, facecolor='red', alpha=0.5)
+ ax3.set_title('Datetime vs. Datetime')
+ ax3.set_xlabel('Date')
+ ax3.set_ylabel('Date')
+
+ @mpl.style.context("default")
+ def test_bar(self):
+ mpl.rcParams["date.converter"] = "concise"
+
+ fig, (ax1, ax2) = plt.subplots(2, 1, layout="constrained")
+
+ x_dates = np.array(
+ [
+ datetime.datetime(2020, 6, 30),
+ datetime.datetime(2020, 7, 22),
+ datetime.datetime(2020, 8, 3),
+ datetime.datetime(2020, 9, 14),
+ ],
+ dtype=np.datetime64,
+ )
+ x_ranges = [8800, 2600, 8500, 7400]
+
+ x = np.datetime64(datetime.datetime(2020, 6, 1))
+ ax1.bar(x_dates, x_ranges, width=np.timedelta64(4, "D"))
+ ax2.bar(np.arange(4), x_dates - x, bottom=x)
+
+ @mpl.style.context("default")
+ def test_bar_label(self):
+ # Generate some example data with dateTime inputs
+ date_list = [datetime.datetime(2023, 1, 1) +
+ datetime.timedelta(days=i) for i in range(5)]
+ values = [10, 20, 15, 25, 30]
+
+ # Creating the plot
+ fig, ax = plt.subplots(1, 1, figsize=(10, 8), layout='constrained')
+ bars = ax.bar(date_list, values)
+
+ # Add labels to the bars using bar_label
+ ax.bar_label(bars, labels=[f'{val}%' for val in values],
+ label_type='edge', color='black')
+
+ @mpl.style.context("default")
+ def test_barbs(self):
+ plt.rcParams["date.converter"] = 'concise'
+
+ start_date = datetime.datetime(2022, 2, 8, 22)
+ dates = [start_date + datetime.timedelta(hours=i) for i in range(12)]
+
+ numbers = np.sin(np.linspace(0, 2 * np.pi, 12))
+
+ u = np.ones(12) * 10
+ v = np.arange(0, 120, 10)
+
+ fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 6))
+
+ axes[0].barbs(dates, numbers, u, v, length=7)
+ axes[0].set_title('Datetime vs. Numeric Data')
+ axes[0].set_xlabel('Datetime')
+ axes[0].set_ylabel('Numeric Data')
+
+ axes[1].barbs(numbers, dates, u, v, length=7)
+ axes[1].set_title('Numeric vs. Datetime Data')
+ axes[1].set_xlabel('Numeric Data')
+ axes[1].set_ylabel('Datetime')
+
+ @mpl.style.context("default")
+ def test_barh(self):
+ mpl.rcParams["date.converter"] = 'concise'
+ fig, (ax1, ax2) = plt.subplots(2, 1, layout='constrained')
+ birth_date = np.array([datetime.datetime(2020, 4, 10),
+ datetime.datetime(2020, 5, 30),
+ datetime.datetime(2020, 10, 12),
+ datetime.datetime(2020, 11, 15)])
+ year_start = datetime.datetime(2020, 1, 1)
+ year_end = datetime.datetime(2020, 12, 31)
+ age = [21, 53, 20, 24]
+ ax1.set_xlabel('Age')
+ ax1.set_ylabel('Birth Date')
+ ax1.barh(birth_date, width=age, height=datetime.timedelta(days=10))
+ ax2.set_xlim(left=year_start, right=year_end)
+ ax2.set_xlabel('Birth Date')
+ ax2.set_ylabel('Order of Birth Dates')
+ ax2.barh(np.arange(4), birth_date-year_start, left=year_start)
+
+ @pytest.mark.xfail(reason="Test for boxplot not written yet")
+ @mpl.style.context("default")
+ def test_boxplot(self):
+ fig, ax = plt.subplots()
+ ax.boxplot(...)
+
+ @mpl.style.context("default")
+ def test_broken_barh(self):
+ # Horizontal bar plot with gaps
+ mpl.rcParams["date.converter"] = 'concise'
+ fig, ax = plt.subplots()
+
+ ax.broken_barh([(datetime.datetime(2023, 1, 4), datetime.timedelta(days=2)),
+ (datetime.datetime(2023, 1, 8), datetime.timedelta(days=3))],
+ (10, 9), facecolors='tab:blue')
+ ax.broken_barh([(datetime.datetime(2023, 1, 2), datetime.timedelta(days=1)),
+ (datetime.datetime(2023, 1, 4), datetime.timedelta(days=4))],
+ (20, 9), facecolors=('tab:red'))
+
+ @mpl.style.context("default")
+ def test_bxp(self):
+ mpl.rcParams["date.converter"] = 'concise'
+ fig, ax = plt.subplots()
+ data = [{
+ "med": datetime.datetime(2020, 1, 15),
+ "q1": datetime.datetime(2020, 1, 10),
+ "q3": datetime.datetime(2020, 1, 20),
+ "whislo": datetime.datetime(2020, 1, 5),
+ "whishi": datetime.datetime(2020, 1, 25),
+ "fliers": [
+ datetime.datetime(2020, 1, 3),
+ datetime.datetime(2020, 1, 27)
+ ]
+ }]
+ ax.bxp(data, orientation='horizontal')
+ ax.xaxis.set_major_formatter(mpl.dates.DateFormatter("%Y-%m-%d"))
+ ax.set_title('Box plot with datetime data')
+
+ @pytest.mark.xfail(reason="Test for clabel not written yet")
+ @mpl.style.context("default")
+ def test_clabel(self):
+ fig, ax = plt.subplots()
+ ax.clabel(...)
+
+ @mpl.style.context("default")
+ def test_contour(self):
+ mpl.rcParams["date.converter"] = "concise"
+ range_threshold = 10
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout="constrained")
+
+ x_dates = np.array(
+ [datetime.datetime(2023, 10, delta) for delta in range(1, range_threshold)]
+ )
+ y_dates = np.array(
+ [datetime.datetime(2023, 10, delta) for delta in range(1, range_threshold)]
+ )
+ x_ranges = np.array(range(1, range_threshold))
+ y_ranges = np.array(range(1, range_threshold))
+
+ X_dates, Y_dates = np.meshgrid(x_dates, y_dates)
+ X_ranges, Y_ranges = np.meshgrid(x_ranges, y_ranges)
+
+ Z_ranges = np.cos(X_ranges / 4) + np.sin(Y_ranges / 4)
+
+ ax1.contour(X_dates, Y_dates, Z_ranges)
+ ax2.contour(X_dates, Y_ranges, Z_ranges)
+ ax3.contour(X_ranges, Y_dates, Z_ranges)
+
+ @mpl.style.context("default")
+ def test_contourf(self):
+ mpl.rcParams["date.converter"] = "concise"
+ range_threshold = 10
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout="constrained")
+
+ x_dates = np.array(
+ [datetime.datetime(2023, 10, delta) for delta in range(1, range_threshold)]
+ )
+ y_dates = np.array(
+ [datetime.datetime(2023, 10, delta) for delta in range(1, range_threshold)]
+ )
+ x_ranges = np.array(range(1, range_threshold))
+ y_ranges = np.array(range(1, range_threshold))
+
+ X_dates, Y_dates = np.meshgrid(x_dates, y_dates)
+ X_ranges, Y_ranges = np.meshgrid(x_ranges, y_ranges)
+
+ Z_ranges = np.cos(X_ranges / 4) + np.sin(Y_ranges / 4)
+
+ ax1.contourf(X_dates, Y_dates, Z_ranges)
+ ax2.contourf(X_dates, Y_ranges, Z_ranges)
+ ax3.contourf(X_ranges, Y_dates, Z_ranges)
+
+ @mpl.style.context("default")
+ def test_errorbar(self):
+ mpl.rcParams["date.converter"] = "concise"
+ fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, layout="constrained")
+ limit = 7
+ start_date = datetime.datetime(2023, 1, 1)
+
+ x_dates = np.array([datetime.datetime(2023, 10, d) for d in range(1, limit)])
+ y_dates = np.array([datetime.datetime(2023, 10, d) for d in range(1, limit)])
+ x_date_error = datetime.timedelta(days=1)
+ y_date_error = datetime.timedelta(days=1)
+
+ x_values = list(range(1, limit))
+ y_values = list(range(1, limit))
+ x_value_error = 0.5
+ y_value_error = 0.5
+
+ ax1.errorbar(x_dates, y_values,
+ yerr=y_value_error,
+ capsize=10,
+ barsabove=True,
+ label='Data')
+ ax2.errorbar(x_values, y_dates,
+ xerr=x_value_error, yerr=y_date_error,
+ errorevery=(1, 2),
+ fmt='-o', label='Data')
+ ax3.errorbar(x_dates, y_dates,
+ xerr=x_date_error, yerr=y_date_error,
+ lolims=True, xlolims=True,
+ label='Data')
+ ax4.errorbar(x_dates, y_values,
+ xerr=x_date_error, yerr=y_value_error,
+ uplims=True, xuplims=True,
+ label='Data')
+
+ @mpl.style.context("default")
+ def test_eventplot(self):
+ mpl.rcParams["date.converter"] = "concise"
+
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout="constrained")
+
+ x_dates1 = np.array([datetime.datetime(2020, 6, 30),
+ datetime.datetime(2020, 7, 22),
+ datetime.datetime(2020, 8, 3),
+ datetime.datetime(2020, 9, 14),],
+ dtype=np.datetime64,
+ )
+
+ ax1.eventplot(x_dates1)
+
+ np.random.seed(19680801)
+
+ start_date = datetime.datetime(2020, 7, 1)
+ end_date = datetime.datetime(2020, 10, 15)
+ date_range = end_date - start_date
+
+ dates1 = start_date + np.random.rand(30) * date_range
+ dates2 = start_date + np.random.rand(10) * date_range
+ dates3 = start_date + np.random.rand(50) * date_range
+
+ colors1 = ['C1', 'C2', 'C3']
+ lineoffsets1 = np.array([1, 6, 8])
+ linelengths1 = [5, 2, 3]
+
+ ax2.eventplot([dates1, dates2, dates3],
+ colors=colors1,
+ lineoffsets=lineoffsets1,
+ linelengths=linelengths1)
+
+ lineoffsets2 = np.array([
+ datetime.datetime(2020, 7, 1),
+ datetime.datetime(2020, 7, 15),
+ datetime.datetime(2020, 8, 1)
+ ], dtype=np.datetime64)
+
+ ax3.eventplot([dates1, dates2, dates3],
+ colors=colors1,
+ lineoffsets=lineoffsets2,
+ linelengths=linelengths1)
+
+ @mpl.style.context("default")
+ def test_fill(self):
+ mpl.rcParams["date.converter"] = "concise"
+ fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, layout="constrained")
+
+ np.random.seed(19680801)
+
+ x_base_date = datetime.datetime(2023, 1, 1)
+ x_dates = [x_base_date]
+ for _ in range(1, 5):
+ x_base_date += datetime.timedelta(days=np.random.randint(1, 5))
+ x_dates.append(x_base_date)
+
+ y_base_date = datetime.datetime(2023, 1, 1)
+ y_dates = [y_base_date]
+ for _ in range(1, 5):
+ y_base_date += datetime.timedelta(days=np.random.randint(1, 5))
+ y_dates.append(y_base_date)
+
+ x_values = np.random.rand(5) * 5
+ y_values = np.random.rand(5) * 5 - 2
+
+ ax1.fill(x_dates, y_values)
+ ax2.fill(x_values, y_dates)
+ ax3.fill(x_values, y_values)
+ ax4.fill(x_dates, y_dates)
+
+ @mpl.style.context("default")
+ def test_fill_between(self):
+ mpl.rcParams["date.converter"] = "concise"
+ np.random.seed(19680801)
+
+ y_base_date = datetime.datetime(2023, 1, 1)
+ y_dates1 = [y_base_date]
+ for i in range(1, 10):
+ y_base_date += datetime.timedelta(days=np.random.randint(1, 5))
+ y_dates1.append(y_base_date)
+
+ y_dates2 = [y_base_date]
+ for i in range(1, 10):
+ y_base_date += datetime.timedelta(days=np.random.randint(1, 5))
+ y_dates2.append(y_base_date)
+ x_values = np.random.rand(10) * 10
+ x_values.sort()
+
+ y_values1 = np.random.rand(10) * 10
+ y_values2 = y_values1 + np.random.rand(10) * 10
+ y_values1.sort()
+ y_values2.sort()
+
+ x_base_date = datetime.datetime(2023, 1, 1)
+ x_dates = [x_base_date]
+ for i in range(1, 10):
+ x_base_date += datetime.timedelta(days=np.random.randint(1, 10))
+ x_dates.append(x_base_date)
+
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout="constrained")
+
+ ax1.fill_between(x_values, y_dates1, y_dates2)
+ ax2.fill_between(x_dates, y_values1, y_values2)
+ ax3.fill_between(x_dates, y_dates1, y_dates2)
+
+ @mpl.style.context("default")
+ def test_fill_betweenx(self):
+ mpl.rcParams["date.converter"] = "concise"
+ np.random.seed(19680801)
+
+ x_base_date = datetime.datetime(2023, 1, 1)
+ x_dates1 = [x_base_date]
+ for i in range(1, 10):
+ x_base_date += datetime.timedelta(days=np.random.randint(1, 5))
+ x_dates1.append(x_base_date)
+
+ x_dates2 = [x_base_date]
+ for i in range(1, 10):
+ x_base_date += datetime.timedelta(days=np.random.randint(1, 5))
+ x_dates2.append(x_base_date)
+ y_values = np.random.rand(10) * 10
+ y_values.sort()
+
+ x_values1 = np.random.rand(10) * 10
+ x_values2 = x_values1 + np.random.rand(10) * 10
+ x_values1.sort()
+ x_values2.sort()
+
+ y_base_date = datetime.datetime(2023, 1, 1)
+ y_dates = [y_base_date]
+ for i in range(1, 10):
+ y_base_date += datetime.timedelta(days=np.random.randint(1, 10))
+ y_dates.append(y_base_date)
+
+ fig, (ax1, ax2, ax3) = plt.subplots(1, 3, layout="constrained")
+
+ ax1.fill_betweenx(y_values, x_dates1, x_dates2)
+ ax2.fill_betweenx(y_dates, x_values1, x_values2)
+ ax3.fill_betweenx(y_dates, x_dates1, x_dates2)
+
+ @pytest.mark.xfail(reason="Test for hexbin not written yet")
+ @mpl.style.context("default")
+ def test_hexbin(self):
+ fig, ax = plt.subplots()
+ ax.hexbin(...)
+
+ @mpl.style.context("default")
+ def test_hist(self):
+ mpl.rcParams["date.converter"] = 'concise'
+
+ start_date = datetime.datetime(2023, 10, 1)
+ time_delta = datetime.timedelta(days=1)
+
+ values1 = np.random.randint(1, 10, 30)
+ values2 = np.random.randint(1, 10, 30)
+ values3 = np.random.randint(1, 10, 30)
+
+ bin_edges = [start_date + i * time_delta for i in range(31)]
+
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, constrained_layout=True)
+ ax1.hist(
+ [start_date + i * time_delta for i in range(30)],
+ bins=10,
+ weights=values1
+ )
+ ax2.hist(
+ [start_date + i * time_delta for i in range(30)],
+ bins=10,
+ weights=values2
+ )
+ ax3.hist(
+ [start_date + i * time_delta for i in range(30)],
+ bins=10,
+ weights=values3
+ )
+
+ fig, (ax4, ax5, ax6) = plt.subplots(3, 1, constrained_layout=True)
+ ax4.hist(
+ [start_date + i * time_delta for i in range(30)],
+ bins=bin_edges,
+ weights=values1
+ )
+ ax5.hist(
+ [start_date + i * time_delta for i in range(30)],
+ bins=bin_edges,
+ weights=values2
+ )
+ ax6.hist(
+ [start_date + i * time_delta for i in range(30)],
+ bins=bin_edges,
+ weights=values3
+ )
+
+ @pytest.mark.xfail(reason="Test for hist2d not written yet")
+ @mpl.style.context("default")
+ def test_hist2d(self):
+ fig, ax = plt.subplots()
+ ax.hist2d(...)
+
+ @mpl.style.context("default")
+ def test_hlines(self):
+ mpl.rcParams["date.converter"] = 'concise'
+ fig, axs = plt.subplots(2, 4, layout='constrained')
+ dateStrs = ['2023-03-08',
+ '2023-04-09',
+ '2023-05-13',
+ '2023-07-28',
+ '2023-12-24']
+ dates = [datetime.datetime(2023, m*2, 10) for m in range(1, 6)]
+ date_start = [datetime.datetime(2023, 6, d) for d in range(5, 30, 5)]
+ date_end = [datetime.datetime(2023, 7, d) for d in range(5, 30, 5)]
+ npDates = [np.datetime64(s) for s in dateStrs]
+ axs[0, 0].hlines(y=dates,
+ xmin=[0.1, 0.2, 0.3, 0.4, 0.5],
+ xmax=[0.5, 0.6, 0.7, 0.8, 0.9])
+ axs[0, 1].hlines(dates,
+ xmin=datetime.datetime(2020, 5, 10),
+ xmax=datetime.datetime(2020, 5, 31))
+ axs[0, 2].hlines(dates,
+ xmin=date_start,
+ xmax=date_end)
+ axs[0, 3].hlines(dates,
+ xmin=0.45,
+ xmax=0.65)
+ axs[1, 0].hlines(y=npDates,
+ xmin=[0.5, 0.6, 0.7, 0.8, 0.9],
+ xmax=[0.1, 0.2, 0.3, 0.4, 0.5])
+ axs[1, 2].hlines(y=npDates,
+ xmin=date_start,
+ xmax=date_end)
+ axs[1, 1].hlines(npDates,
+ xmin=datetime.datetime(2020, 5, 10),
+ xmax=datetime.datetime(2020, 5, 31))
+ axs[1, 3].hlines(npDates,
+ xmin=0.45,
+ xmax=0.65)
+
+ @mpl.style.context("default")
+ def test_imshow(self):
+ fig, ax = plt.subplots()
+ a = np.diag(range(5))
+ dt_start = datetime.datetime(2010, 11, 1)
+ dt_end = datetime.datetime(2010, 11, 11)
+ extent = (dt_start, dt_end, dt_start, dt_end)
+ ax.imshow(a, extent=extent)
+ ax.tick_params(axis="x", labelrotation=90)
+
+ @pytest.mark.xfail(reason="Test for loglog not written yet")
+ @mpl.style.context("default")
+ def test_loglog(self):
+ fig, ax = plt.subplots()
+ ax.loglog(...)
+
+ @mpl.style.context("default")
+ def test_matshow(self):
+ a = np.diag(range(5))
+ dt_start = datetime.datetime(1980, 4, 15)
+ dt_end = datetime.datetime(2020, 11, 11)
+ extent = (dt_start, dt_end, dt_start, dt_end)
+ fig, ax = plt.subplots()
+ ax.matshow(a, extent=extent)
+ for label in ax.get_xticklabels():
+ label.set_rotation(90)
+
+ @pytest.mark.xfail(reason="Test for pcolor not written yet")
+ @mpl.style.context("default")
+ def test_pcolor(self):
+ fig, ax = plt.subplots()
+ ax.pcolor(...)
+
+ @pytest.mark.xfail(reason="Test for pcolorfast not written yet")
+ @mpl.style.context("default")
+ def test_pcolorfast(self):
+ fig, ax = plt.subplots()
+ ax.pcolorfast(...)
+
+ @pytest.mark.xfail(reason="Test for pcolormesh not written yet")
+ @mpl.style.context("default")
+ def test_pcolormesh(self):
+ fig, ax = plt.subplots()
+ ax.pcolormesh(...)
+
+ @mpl.style.context("default")
+ def test_plot(self):
+ mpl.rcParams["date.converter"] = 'concise'
+ N = 6
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout='constrained')
+ x = np.array([datetime.datetime(2023, 9, n) for n in range(1, N)])
+ ax1.plot(x, range(1, N))
+ ax2.plot(range(1, N), x)
+ ax3.plot(x, x)
+
+ @mpl.style.context("default")
+ def test_plot_date(self):
+ mpl.rcParams["date.converter"] = "concise"
+ range_threshold = 10
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout="constrained")
+
+ x_dates = np.array(
+ [datetime.datetime(2023, 10, delta) for delta in range(1, range_threshold)]
+ )
+ y_dates = np.array(
+ [datetime.datetime(2023, 10, delta) for delta in range(1, range_threshold)]
+ )
+ x_ranges = np.array(range(1, range_threshold))
+ y_ranges = np.array(range(1, range_threshold))
+
+ with pytest.warns(mpl.MatplotlibDeprecationWarning):
+ ax1.plot_date(x_dates, y_dates)
+ ax2.plot_date(x_dates, y_ranges)
+ ax3.plot_date(x_ranges, y_dates)
+
+ @pytest.mark.xfail(reason="Test for quiver not written yet")
+ @mpl.style.context("default")
+ def test_quiver(self):
+ fig, ax = plt.subplots()
+ ax.quiver(...)
+
+ @mpl.style.context("default")
+ def test_scatter(self):
+ mpl.rcParams["date.converter"] = 'concise'
+ base = datetime.datetime(2005, 2, 1)
+ dates = [base + datetime.timedelta(hours=(2 * i)) for i in range(10)]
+ N = len(dates)
+ np.random.seed(19680801)
+ y = np.cumsum(np.random.randn(N))
+ fig, axs = plt.subplots(3, 1, layout='constrained', figsize=(6, 6))
+ # datetime array on x axis
+ axs[0].scatter(dates, y)
+ for label in axs[0].get_xticklabels():
+ label.set_rotation(40)
+ label.set_horizontalalignment('right')
+ # datetime on y axis
+ axs[1].scatter(y, dates)
+ # datetime on both x, y axes
+ axs[2].scatter(dates, dates)
+ for label in axs[2].get_xticklabels():
+ label.set_rotation(40)
+ label.set_horizontalalignment('right')
+
+ @pytest.mark.xfail(reason="Test for semilogx not written yet")
+ @mpl.style.context("default")
+ def test_semilogx(self):
+ fig, ax = plt.subplots()
+ ax.semilogx(...)
+
+ @pytest.mark.xfail(reason="Test for semilogy not written yet")
+ @mpl.style.context("default")
+ def test_semilogy(self):
+ fig, ax = plt.subplots()
+ ax.semilogy(...)
+
+ @mpl.style.context("default")
+ def test_stackplot(self):
+ mpl.rcParams["date.converter"] = 'concise'
+ N = 10
+ stacked_nums = np.tile(np.arange(1, N), (4, 1))
+ dates = np.array([datetime.datetime(2020 + i, 1, 1) for i in range(N - 1)])
+
+ fig, ax = plt.subplots(layout='constrained')
+ ax.stackplot(dates, stacked_nums)
+
+ @mpl.style.context("default")
+ def test_stairs(self):
+ mpl.rcParams["date.converter"] = 'concise'
+
+ start_date = datetime.datetime(2023, 12, 1)
+ time_delta = datetime.timedelta(days=1)
+ baseline_date = datetime.datetime(1980, 1, 1)
+
+ bin_edges = [start_date + i * time_delta for i in range(31)]
+ edge_int = np.arange(31)
+ np.random.seed(123456)
+ values1 = np.random.randint(1, 100, 30)
+ values2 = [start_date + datetime.timedelta(days=int(i))
+ for i in np.random.randint(1, 10000, 30)]
+ values3 = [start_date + datetime.timedelta(days=int(i))
+ for i in np.random.randint(-10000, 10000, 30)]
+
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, constrained_layout=True)
+ ax1.stairs(values1, edges=bin_edges)
+ ax2.stairs(values2, edges=edge_int, baseline=baseline_date)
+ ax3.stairs(values3, edges=bin_edges, baseline=baseline_date)
+
+ @mpl.style.context("default")
+ def test_stem(self):
+ mpl.rcParams["date.converter"] = "concise"
+
+ fig, (ax1, ax2, ax3, ax4, ax5, ax6) = plt.subplots(6, 1, layout="constrained")
+
+ limit_value = 10
+ above = datetime.datetime(2023, 9, 18)
+ below = datetime.datetime(2023, 11, 18)
+
+ x_ranges = np.arange(1, limit_value)
+ y_ranges = np.arange(1, limit_value)
+
+ x_dates = np.array(
+ [datetime.datetime(2023, 10, n) for n in range(1, limit_value)]
+ )
+ y_dates = np.array(
+ [datetime.datetime(2023, 10, n) for n in range(1, limit_value)]
+ )
+
+ ax1.stem(x_dates, y_dates, bottom=above)
+ ax2.stem(x_dates, y_ranges, bottom=5)
+ ax3.stem(x_ranges, y_dates, bottom=below)
+
+ ax4.stem(x_ranges, y_dates, orientation="horizontal", bottom=above)
+ ax5.stem(x_dates, y_ranges, orientation="horizontal", bottom=5)
+ ax6.stem(x_ranges, y_dates, orientation="horizontal", bottom=below)
+
+ @mpl.style.context("default")
+ def test_step(self):
+ mpl.rcParams["date.converter"] = "concise"
+ N = 6
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout='constrained')
+ x = np.array([datetime.datetime(2023, 9, n) for n in range(1, N)])
+ ax1.step(x, range(1, N))
+ ax2.step(range(1, N), x)
+ ax3.step(x, x)
+
+ @pytest.mark.xfail(reason="Test for streamplot not written yet")
+ @mpl.style.context("default")
+ def test_streamplot(self):
+ fig, ax = plt.subplots()
+ ax.streamplot(...)
+
+ @mpl.style.context("default")
+ def test_text(self):
+ mpl.rcParams["date.converter"] = 'concise'
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout="constrained")
+
+ limit_value = 10
+ font_properties = {'family': 'serif', 'size': 12, 'weight': 'bold'}
+ test_date = datetime.datetime(2023, 10, 1)
+
+ x_data = np.array(range(1, limit_value))
+ y_data = np.array(range(1, limit_value))
+
+ x_dates = np.array(
+ [datetime.datetime(2023, 10, n) for n in range(1, limit_value)]
+ )
+ y_dates = np.array(
+ [datetime.datetime(2023, 10, n) for n in range(1, limit_value)]
+ )
+
+ ax1.plot(x_dates, y_data)
+ ax1.text(test_date, 5, "Inserted Text", **font_properties)
+
+ ax2.plot(x_data, y_dates)
+ ax2.text(7, test_date, "Inserted Text", **font_properties)
+
+ ax3.plot(x_dates, y_dates)
+ ax3.text(test_date, test_date, "Inserted Text", **font_properties)
+
+ @pytest.mark.xfail(reason="Test for tricontour not written yet")
+ @mpl.style.context("default")
+ def test_tricontour(self):
+ fig, ax = plt.subplots()
+ ax.tricontour(...)
+
+ @pytest.mark.xfail(reason="Test for tricontourf not written yet")
+ @mpl.style.context("default")
+ def test_tricontourf(self):
+ fig, ax = plt.subplots()
+ ax.tricontourf(...)
+
+ @pytest.mark.xfail(reason="Test for tripcolor not written yet")
+ @mpl.style.context("default")
+ def test_tripcolor(self):
+ fig, ax = plt.subplots()
+ ax.tripcolor(...)
+
+ @pytest.mark.xfail(reason="Test for triplot not written yet")
+ @mpl.style.context("default")
+ def test_triplot(self):
+ fig, ax = plt.subplots()
+ ax.triplot(...)
+
+ @pytest.mark.xfail(reason="Test for violin not written yet")
+ @mpl.style.context("default")
+ def test_violin(self):
+ fig, ax = plt.subplots()
+ ax.violin(...)
+
+ @pytest.mark.xfail(reason="Test for violinplot not written yet")
+ @mpl.style.context("default")
+ def test_violinplot(self):
+ fig, ax = plt.subplots()
+ ax.violinplot(...)
+
+ @mpl.style.context("default")
+ def test_vlines(self):
+ mpl.rcParams["date.converter"] = 'concise'
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, layout='constrained')
+ ax1.set_xlim(left=datetime.datetime(2023, 1, 1),
+ right=datetime.datetime(2023, 6, 30))
+ ax1.vlines(x=[datetime.datetime(2023, 2, 10),
+ datetime.datetime(2023, 5, 18),
+ datetime.datetime(2023, 6, 6)],
+ ymin=[0, 0.25, 0.5],
+ ymax=[0.25, 0.5, 0.75])
+ ax2.set_xlim(left=0,
+ right=0.5)
+ ax2.vlines(x=[0.3, 0.35],
+ ymin=[np.datetime64('2023-03-20'), np.datetime64('2023-03-31')],
+ ymax=[np.datetime64('2023-05-01'), np.datetime64('2023-05-16')])
+ ax3.set_xlim(left=datetime.datetime(2023, 7, 1),
+ right=datetime.datetime(2023, 12, 31))
+ ax3.vlines(x=[datetime.datetime(2023, 9, 1), datetime.datetime(2023, 12, 10)],
+ ymin=datetime.datetime(2023, 1, 15),
+ ymax=datetime.datetime(2023, 1, 30))
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_getattr.py b/lib/python3.12/site-packages/matplotlib/tests/test_getattr.py
new file mode 100644
index 0000000000000000000000000000000000000000..f0f5823600ca0378efe611c989ff293ea7740581
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_getattr.py
@@ -0,0 +1,35 @@
+from importlib import import_module
+from pkgutil import walk_packages
+
+import matplotlib
+import pytest
+
+# Get the names of all matplotlib submodules,
+# except for the unit tests and private modules.
+module_names = [
+ m.name
+ for m in walk_packages(
+ path=matplotlib.__path__, prefix=f'{matplotlib.__name__}.'
+ )
+ if not m.name.startswith(__package__)
+ and not any(x.startswith('_') for x in m.name.split('.'))
+]
+
+
+@pytest.mark.parametrize('module_name', module_names)
+@pytest.mark.filterwarnings('ignore::DeprecationWarning')
+@pytest.mark.filterwarnings('ignore::ImportWarning')
+def test_getattr(module_name):
+ """
+ Test that __getattr__ methods raise AttributeError for unknown keys.
+ See #20822, #20855.
+ """
+ try:
+ module = import_module(module_name)
+ except (ImportError, RuntimeError, OSError) as e:
+ # Skip modules that cannot be imported due to missing dependencies
+ pytest.skip(f'Cannot import {module_name} due to {e}')
+
+ key = 'THIS_SYMBOL_SHOULD_NOT_EXIST'
+ if hasattr(module, key):
+ delattr(module, key)
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_matplotlib.py b/lib/python3.12/site-packages/matplotlib/tests/test_matplotlib.py
new file mode 100644
index 0000000000000000000000000000000000000000..37b41fafdb782daec5b023a1d645f625d3f2300c
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_matplotlib.py
@@ -0,0 +1,82 @@
+import os
+import subprocess
+import sys
+
+import pytest
+
+import matplotlib
+from matplotlib.testing import subprocess_run_for_testing
+
+
+@pytest.mark.parametrize('version_str, version_tuple', [
+ ('3.5.0', (3, 5, 0, 'final', 0)),
+ ('3.5.0rc2', (3, 5, 0, 'candidate', 2)),
+ ('3.5.0.dev820+g6768ef8c4c', (3, 5, 0, 'alpha', 820)),
+ ('3.5.0.post820+g6768ef8c4c', (3, 5, 1, 'alpha', 820)),
+])
+def test_parse_to_version_info(version_str, version_tuple):
+ assert matplotlib._parse_to_version_info(version_str) == version_tuple
+
+
+@pytest.mark.skipif(sys.platform == "win32",
+ reason="chmod() doesn't work as is on Windows")
+@pytest.mark.skipif(sys.platform != "win32" and os.geteuid() == 0,
+ reason="chmod() doesn't work as root")
+def test_tmpconfigdir_warning(tmp_path):
+ """Test that a warning is emitted if a temporary configdir must be used."""
+ mode = os.stat(tmp_path).st_mode
+ try:
+ os.chmod(tmp_path, 0)
+ proc = subprocess_run_for_testing(
+ [sys.executable, "-c", "import matplotlib"],
+ env={**os.environ, "MPLCONFIGDIR": str(tmp_path)},
+ stderr=subprocess.PIPE, text=True, check=True)
+ assert "set the MPLCONFIGDIR" in proc.stderr
+ finally:
+ os.chmod(tmp_path, mode)
+
+
+def test_importable_with_no_home(tmp_path):
+ subprocess_run_for_testing(
+ [sys.executable, "-c",
+ "import pathlib; pathlib.Path.home = lambda *args: 1/0; "
+ "import matplotlib.pyplot"],
+ env={**os.environ, "MPLCONFIGDIR": str(tmp_path)}, check=True)
+
+
+def test_use_doc_standard_backends():
+ """
+ Test that the standard backends mentioned in the docstring of
+ matplotlib.use() are the same as in matplotlib.rcsetup.
+ """
+ def parse(key):
+ backends = []
+ for line in matplotlib.use.__doc__.split(key)[1].split('\n'):
+ if not line.strip():
+ break
+ backends += [e.strip().lower() for e in line.split(',') if e]
+ return backends
+
+ from matplotlib.backends import BackendFilter, backend_registry
+
+ assert (set(parse('- interactive backends:\n')) ==
+ set(backend_registry.list_builtin(BackendFilter.INTERACTIVE)))
+ assert (set(parse('- non-interactive backends:\n')) ==
+ set(backend_registry.list_builtin(BackendFilter.NON_INTERACTIVE)))
+
+
+def test_importable_with__OO():
+ """
+ When using -OO or export PYTHONOPTIMIZE=2, docstrings are discarded,
+ this simple test may prevent something like issue #17970.
+ """
+ program = (
+ "import matplotlib as mpl; "
+ "import matplotlib.pyplot as plt; "
+ "import matplotlib.cbook as cbook; "
+ "import matplotlib.patches as mpatches"
+ )
+ subprocess_run_for_testing(
+ [sys.executable, "-OO", "-c", program],
+ env={**os.environ, "MPLBACKEND": ""}, check=True
+ )
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_patches.py b/lib/python3.12/site-packages/matplotlib/tests/test_patches.py
new file mode 100644
index 0000000000000000000000000000000000000000..d98998ef179a506596e100837279c4df3d9f4501
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_patches.py
@@ -0,0 +1,1001 @@
+"""
+Tests specific to the patches module.
+"""
+import platform
+
+import numpy as np
+from numpy.testing import assert_almost_equal, assert_array_equal
+import pytest
+
+import matplotlib as mpl
+from matplotlib.patches import (Annulus, Ellipse, Patch, Polygon, Rectangle,
+ FancyArrowPatch, FancyArrow, BoxStyle, Arc)
+from matplotlib.testing.decorators import image_comparison, check_figures_equal
+from matplotlib.transforms import Bbox
+import matplotlib.pyplot as plt
+from matplotlib import (
+ collections as mcollections, colors as mcolors, patches as mpatches,
+ path as mpath, transforms as mtransforms, rcParams)
+
+
+def test_Polygon_close():
+ #: GitHub issue #1018 identified a bug in the Polygon handling
+ #: of the closed attribute; the path was not getting closed
+ #: when set_xy was used to set the vertices.
+
+ # open set of vertices:
+ xy = [[0, 0], [0, 1], [1, 1]]
+ # closed set:
+ xyclosed = xy + [[0, 0]]
+
+ # start with open path and close it:
+ p = Polygon(xy, closed=True)
+ assert p.get_closed()
+ assert_array_equal(p.get_xy(), xyclosed)
+ p.set_xy(xy)
+ assert_array_equal(p.get_xy(), xyclosed)
+
+ # start with closed path and open it:
+ p = Polygon(xyclosed, closed=False)
+ assert_array_equal(p.get_xy(), xy)
+ p.set_xy(xyclosed)
+ assert_array_equal(p.get_xy(), xy)
+
+ # start with open path and leave it open:
+ p = Polygon(xy, closed=False)
+ assert not p.get_closed()
+ assert_array_equal(p.get_xy(), xy)
+ p.set_xy(xy)
+ assert_array_equal(p.get_xy(), xy)
+
+ # start with closed path and leave it closed:
+ p = Polygon(xyclosed, closed=True)
+ assert_array_equal(p.get_xy(), xyclosed)
+ p.set_xy(xyclosed)
+ assert_array_equal(p.get_xy(), xyclosed)
+
+
+def test_corner_center():
+ loc = [10, 20]
+ width = 1
+ height = 2
+
+ # Rectangle
+ # No rotation
+ corners = ((10, 20), (11, 20), (11, 22), (10, 22))
+ rect = Rectangle(loc, width, height)
+ assert_array_equal(rect.get_corners(), corners)
+ assert_array_equal(rect.get_center(), (10.5, 21))
+
+ # 90 deg rotation
+ corners_rot = ((10, 20), (10, 21), (8, 21), (8, 20))
+ rect.set_angle(90)
+ assert_array_equal(rect.get_corners(), corners_rot)
+ assert_array_equal(rect.get_center(), (9, 20.5))
+
+ # Rotation not a multiple of 90 deg
+ theta = 33
+ t = mtransforms.Affine2D().rotate_around(*loc, np.deg2rad(theta))
+ corners_rot = t.transform(corners)
+ rect.set_angle(theta)
+ assert_almost_equal(rect.get_corners(), corners_rot)
+
+ # Ellipse
+ loc = [loc[0] + width / 2,
+ loc[1] + height / 2]
+ ellipse = Ellipse(loc, width, height)
+
+ # No rotation
+ assert_array_equal(ellipse.get_corners(), corners)
+
+ # 90 deg rotation
+ corners_rot = ((11.5, 20.5), (11.5, 21.5), (9.5, 21.5), (9.5, 20.5))
+ ellipse.set_angle(90)
+ assert_array_equal(ellipse.get_corners(), corners_rot)
+ # Rotation shouldn't change ellipse center
+ assert_array_equal(ellipse.get_center(), loc)
+
+ # Rotation not a multiple of 90 deg
+ theta = 33
+ t = mtransforms.Affine2D().rotate_around(*loc, np.deg2rad(theta))
+ corners_rot = t.transform(corners)
+ ellipse.set_angle(theta)
+ assert_almost_equal(ellipse.get_corners(), corners_rot)
+
+
+def test_ellipse_vertices():
+ # expect 0 for 0 ellipse width, height
+ ellipse = Ellipse(xy=(0, 0), width=0, height=0, angle=0)
+ assert_almost_equal(
+ ellipse.get_vertices(),
+ [(0.0, 0.0), (0.0, 0.0)],
+ )
+ assert_almost_equal(
+ ellipse.get_co_vertices(),
+ [(0.0, 0.0), (0.0, 0.0)],
+ )
+
+ ellipse = Ellipse(xy=(0, 0), width=2, height=1, angle=30)
+ assert_almost_equal(
+ ellipse.get_vertices(),
+ [
+ (
+ ellipse.center[0] + ellipse.width / 4 * np.sqrt(3),
+ ellipse.center[1] + ellipse.width / 4,
+ ),
+ (
+ ellipse.center[0] - ellipse.width / 4 * np.sqrt(3),
+ ellipse.center[1] - ellipse.width / 4,
+ ),
+ ],
+ )
+ assert_almost_equal(
+ ellipse.get_co_vertices(),
+ [
+ (
+ ellipse.center[0] - ellipse.height / 4,
+ ellipse.center[1] + ellipse.height / 4 * np.sqrt(3),
+ ),
+ (
+ ellipse.center[0] + ellipse.height / 4,
+ ellipse.center[1] - ellipse.height / 4 * np.sqrt(3),
+ ),
+ ],
+ )
+ v1, v2 = np.array(ellipse.get_vertices())
+ np.testing.assert_almost_equal((v1 + v2) / 2, ellipse.center)
+ v1, v2 = np.array(ellipse.get_co_vertices())
+ np.testing.assert_almost_equal((v1 + v2) / 2, ellipse.center)
+
+ ellipse = Ellipse(xy=(2.252, -10.859), width=2.265, height=1.98, angle=68.78)
+ v1, v2 = np.array(ellipse.get_vertices())
+ np.testing.assert_almost_equal((v1 + v2) / 2, ellipse.center)
+ v1, v2 = np.array(ellipse.get_co_vertices())
+ np.testing.assert_almost_equal((v1 + v2) / 2, ellipse.center)
+
+
+def test_rotate_rect():
+ loc = np.asarray([1.0, 2.0])
+ width = 2
+ height = 3
+ angle = 30.0
+
+ # A rotated rectangle
+ rect1 = Rectangle(loc, width, height, angle=angle)
+
+ # A non-rotated rectangle
+ rect2 = Rectangle(loc, width, height)
+
+ # Set up an explicit rotation matrix (in radians)
+ angle_rad = np.pi * angle / 180.0
+ rotation_matrix = np.array([[np.cos(angle_rad), -np.sin(angle_rad)],
+ [np.sin(angle_rad), np.cos(angle_rad)]])
+
+ # Translate to origin, rotate each vertex, and then translate back
+ new_verts = np.inner(rotation_matrix, rect2.get_verts() - loc).T + loc
+
+ # They should be the same
+ assert_almost_equal(rect1.get_verts(), new_verts)
+
+
+@check_figures_equal(extensions=['png'])
+def test_rotate_rect_draw(fig_test, fig_ref):
+ ax_test = fig_test.add_subplot()
+ ax_ref = fig_ref.add_subplot()
+
+ loc = (0, 0)
+ width, height = (1, 1)
+ angle = 30
+ rect_ref = Rectangle(loc, width, height, angle=angle)
+ ax_ref.add_patch(rect_ref)
+ assert rect_ref.get_angle() == angle
+
+ # Check that when the angle is updated after adding to an Axes, that the
+ # patch is marked stale and redrawn in the correct location
+ rect_test = Rectangle(loc, width, height)
+ assert rect_test.get_angle() == 0
+ ax_test.add_patch(rect_test)
+ rect_test.set_angle(angle)
+ assert rect_test.get_angle() == angle
+
+
+@check_figures_equal(extensions=['png'])
+def test_dash_offset_patch_draw(fig_test, fig_ref):
+ ax_test = fig_test.add_subplot()
+ ax_ref = fig_ref.add_subplot()
+
+ loc = (0.1, 0.1)
+ width, height = (0.8, 0.8)
+ rect_ref = Rectangle(loc, width, height, linewidth=3, edgecolor='b',
+ linestyle=(0, [6, 6]))
+ # fill the line gaps using a linestyle (0, [0, 6, 6, 0]), which is
+ # equivalent to (6, [6, 6]) but has 0 dash offset
+ rect_ref2 = Rectangle(loc, width, height, linewidth=3, edgecolor='r',
+ linestyle=(0, [0, 6, 6, 0]))
+ assert rect_ref.get_linestyle() == (0, [6, 6])
+ assert rect_ref2.get_linestyle() == (0, [0, 6, 6, 0])
+
+ ax_ref.add_patch(rect_ref)
+ ax_ref.add_patch(rect_ref2)
+
+ # Check that the dash offset of the rect is the same if we pass it in the
+ # init method and if we create two rects with appropriate onoff sequence
+ # of linestyle.
+
+ rect_test = Rectangle(loc, width, height, linewidth=3, edgecolor='b',
+ linestyle=(0, [6, 6]))
+ rect_test2 = Rectangle(loc, width, height, linewidth=3, edgecolor='r',
+ linestyle=(6, [6, 6]))
+ assert rect_test.get_linestyle() == (0, [6, 6])
+ assert rect_test2.get_linestyle() == (6, [6, 6])
+
+ ax_test.add_patch(rect_test)
+ ax_test.add_patch(rect_test2)
+
+
+def test_negative_rect():
+ # These two rectangles have the same vertices, but starting from a
+ # different point. (We also drop the last vertex, which is a duplicate.)
+ pos_vertices = Rectangle((-3, -2), 3, 2).get_verts()[:-1]
+ neg_vertices = Rectangle((0, 0), -3, -2).get_verts()[:-1]
+ assert_array_equal(np.roll(neg_vertices, 2, 0), pos_vertices)
+
+
+@image_comparison(['clip_to_bbox.png'])
+def test_clip_to_bbox():
+ fig, ax = plt.subplots()
+ ax.set_xlim([-18, 20])
+ ax.set_ylim([-150, 100])
+
+ path = mpath.Path.unit_regular_star(8).deepcopy()
+ path.vertices *= [10, 100]
+ path.vertices -= [5, 25]
+
+ path2 = mpath.Path.unit_circle().deepcopy()
+ path2.vertices *= [10, 100]
+ path2.vertices += [10, -25]
+
+ combined = mpath.Path.make_compound_path(path, path2)
+
+ patch = mpatches.PathPatch(
+ combined, alpha=0.5, facecolor='coral', edgecolor='none')
+ ax.add_patch(patch)
+
+ bbox = mtransforms.Bbox([[-12, -77.5], [50, -110]])
+ result_path = combined.clip_to_bbox(bbox)
+ result_patch = mpatches.PathPatch(
+ result_path, alpha=0.5, facecolor='green', lw=4, edgecolor='black')
+
+ ax.add_patch(result_patch)
+
+
+@image_comparison(['patch_alpha_coloring'], remove_text=True)
+def test_patch_alpha_coloring():
+ """
+ Test checks that the patch and collection are rendered with the specified
+ alpha values in their facecolor and edgecolor.
+ """
+ star = mpath.Path.unit_regular_star(6)
+ circle = mpath.Path.unit_circle()
+ # concatenate the star with an internal cutout of the circle
+ verts = np.concatenate([circle.vertices, star.vertices[::-1]])
+ codes = np.concatenate([circle.codes, star.codes])
+ cut_star1 = mpath.Path(verts, codes)
+ cut_star2 = mpath.Path(verts + 1, codes)
+
+ ax = plt.axes()
+ col = mcollections.PathCollection([cut_star2],
+ linewidth=5, linestyles='dashdot',
+ facecolor=(1, 0, 0, 0.5),
+ edgecolor=(0, 0, 1, 0.75))
+ ax.add_collection(col)
+
+ patch = mpatches.PathPatch(cut_star1,
+ linewidth=5, linestyle='dashdot',
+ facecolor=(1, 0, 0, 0.5),
+ edgecolor=(0, 0, 1, 0.75))
+ ax.add_patch(patch)
+
+ ax.set_xlim(-1, 2)
+ ax.set_ylim(-1, 2)
+
+
+@image_comparison(['patch_alpha_override'], remove_text=True)
+def test_patch_alpha_override():
+ #: Test checks that specifying an alpha attribute for a patch or
+ #: collection will override any alpha component of the facecolor
+ #: or edgecolor.
+ star = mpath.Path.unit_regular_star(6)
+ circle = mpath.Path.unit_circle()
+ # concatenate the star with an internal cutout of the circle
+ verts = np.concatenate([circle.vertices, star.vertices[::-1]])
+ codes = np.concatenate([circle.codes, star.codes])
+ cut_star1 = mpath.Path(verts, codes)
+ cut_star2 = mpath.Path(verts + 1, codes)
+
+ ax = plt.axes()
+ col = mcollections.PathCollection([cut_star2],
+ linewidth=5, linestyles='dashdot',
+ alpha=0.25,
+ facecolor=(1, 0, 0, 0.5),
+ edgecolor=(0, 0, 1, 0.75))
+ ax.add_collection(col)
+
+ patch = mpatches.PathPatch(cut_star1,
+ linewidth=5, linestyle='dashdot',
+ alpha=0.25,
+ facecolor=(1, 0, 0, 0.5),
+ edgecolor=(0, 0, 1, 0.75))
+ ax.add_patch(patch)
+
+ ax.set_xlim(-1, 2)
+ ax.set_ylim(-1, 2)
+
+
+@mpl.style.context('default')
+def test_patch_color_none():
+ # Make sure the alpha kwarg does not override 'none' facecolor.
+ # Addresses issue #7478.
+ c = plt.Circle((0, 0), 1, facecolor='none', alpha=1)
+ assert c.get_facecolor()[0] == 0
+
+
+@image_comparison(['patch_custom_linestyle'], remove_text=True)
+def test_patch_custom_linestyle():
+ #: A test to check that patches and collections accept custom dash
+ #: patterns as linestyle and that they display correctly.
+ star = mpath.Path.unit_regular_star(6)
+ circle = mpath.Path.unit_circle()
+ # concatenate the star with an internal cutout of the circle
+ verts = np.concatenate([circle.vertices, star.vertices[::-1]])
+ codes = np.concatenate([circle.codes, star.codes])
+ cut_star1 = mpath.Path(verts, codes)
+ cut_star2 = mpath.Path(verts + 1, codes)
+
+ ax = plt.axes()
+ col = mcollections.PathCollection(
+ [cut_star2],
+ linewidth=5, linestyles=[(0, (5, 7, 10, 7))],
+ facecolor=(1, 0, 0), edgecolor=(0, 0, 1))
+ ax.add_collection(col)
+
+ patch = mpatches.PathPatch(
+ cut_star1,
+ linewidth=5, linestyle=(0, (5, 7, 10, 7)),
+ facecolor=(1, 0, 0), edgecolor=(0, 0, 1))
+ ax.add_patch(patch)
+
+ ax.set_xlim(-1, 2)
+ ax.set_ylim(-1, 2)
+
+
+def test_patch_linestyle_accents():
+ #: Test if linestyle can also be specified with short mnemonics like "--"
+ #: c.f. GitHub issue #2136
+ star = mpath.Path.unit_regular_star(6)
+ circle = mpath.Path.unit_circle()
+ # concatenate the star with an internal cutout of the circle
+ verts = np.concatenate([circle.vertices, star.vertices[::-1]])
+ codes = np.concatenate([circle.codes, star.codes])
+
+ linestyles = ["-", "--", "-.", ":",
+ "solid", "dashed", "dashdot", "dotted"]
+
+ fig, ax = plt.subplots()
+ for i, ls in enumerate(linestyles):
+ star = mpath.Path(verts + i, codes)
+ patch = mpatches.PathPatch(star,
+ linewidth=3, linestyle=ls,
+ facecolor=(1, 0, 0),
+ edgecolor=(0, 0, 1))
+ ax.add_patch(patch)
+
+ ax.set_xlim([-1, i + 1])
+ ax.set_ylim([-1, i + 1])
+ fig.canvas.draw()
+
+
+@check_figures_equal(extensions=['png'])
+def test_patch_linestyle_none(fig_test, fig_ref):
+ circle = mpath.Path.unit_circle()
+
+ ax_test = fig_test.add_subplot()
+ ax_ref = fig_ref.add_subplot()
+ for i, ls in enumerate(['none', 'None', ' ', '']):
+ path = mpath.Path(circle.vertices + i, circle.codes)
+ patch = mpatches.PathPatch(path,
+ linewidth=3, linestyle=ls,
+ facecolor=(1, 0, 0),
+ edgecolor=(0, 0, 1))
+ ax_test.add_patch(patch)
+
+ patch = mpatches.PathPatch(path,
+ linewidth=3, linestyle='-',
+ facecolor=(1, 0, 0),
+ edgecolor='none')
+ ax_ref.add_patch(patch)
+
+ ax_test.set_xlim([-1, i + 1])
+ ax_test.set_ylim([-1, i + 1])
+ ax_ref.set_xlim([-1, i + 1])
+ ax_ref.set_ylim([-1, i + 1])
+
+
+def test_wedge_movement():
+ param_dict = {'center': ((0, 0), (1, 1), 'set_center'),
+ 'r': (5, 8, 'set_radius'),
+ 'width': (2, 3, 'set_width'),
+ 'theta1': (0, 30, 'set_theta1'),
+ 'theta2': (45, 50, 'set_theta2')}
+
+ init_args = {k: v[0] for k, v in param_dict.items()}
+
+ w = mpatches.Wedge(**init_args)
+ for attr, (old_v, new_v, func) in param_dict.items():
+ assert getattr(w, attr) == old_v
+ getattr(w, func)(new_v)
+ assert getattr(w, attr) == new_v
+
+
+@image_comparison(['wedge_range'], remove_text=True,
+ tol=0 if platform.machine() == 'x86_64' else 0.009)
+def test_wedge_range():
+ ax = plt.axes()
+
+ t1 = 2.313869244286224
+
+ args = [[52.31386924, 232.31386924],
+ [52.313869244286224, 232.31386924428622],
+ [t1, t1 + 180.0],
+ [0, 360],
+ [90, 90 + 360],
+ [-180, 180],
+ [0, 380],
+ [45, 46],
+ [46, 45]]
+
+ for i, (theta1, theta2) in enumerate(args):
+ x = i % 3
+ y = i // 3
+
+ wedge = mpatches.Wedge((x * 3, y * 3), 1, theta1, theta2,
+ facecolor='none', edgecolor='k', lw=3)
+
+ ax.add_artist(wedge)
+
+ ax.set_xlim(-2, 8)
+ ax.set_ylim(-2, 9)
+
+
+def test_patch_str():
+ """
+ Check that patches have nice and working `str` representation.
+
+ Note that the logic is that `__str__` is defined such that:
+ str(eval(str(p))) == str(p)
+ """
+ p = mpatches.Circle(xy=(1, 2), radius=3)
+ assert str(p) == 'Circle(xy=(1, 2), radius=3)'
+
+ p = mpatches.Ellipse(xy=(1, 2), width=3, height=4, angle=5)
+ assert str(p) == 'Ellipse(xy=(1, 2), width=3, height=4, angle=5)'
+
+ p = mpatches.Rectangle(xy=(1, 2), width=3, height=4, angle=5)
+ assert str(p) == 'Rectangle(xy=(1, 2), width=3, height=4, angle=5)'
+
+ p = mpatches.Wedge(center=(1, 2), r=3, theta1=4, theta2=5, width=6)
+ assert str(p) == 'Wedge(center=(1, 2), r=3, theta1=4, theta2=5, width=6)'
+
+ p = mpatches.Arc(xy=(1, 2), width=3, height=4, angle=5, theta1=6, theta2=7)
+ expected = 'Arc(xy=(1, 2), width=3, height=4, angle=5, theta1=6, theta2=7)'
+ assert str(p) == expected
+
+ p = mpatches.Annulus(xy=(1, 2), r=(3, 4), width=1, angle=2)
+ expected = "Annulus(xy=(1, 2), r=(3, 4), width=1, angle=2)"
+ assert str(p) == expected
+
+ p = mpatches.RegularPolygon((1, 2), 20, radius=5)
+ assert str(p) == "RegularPolygon((1, 2), 20, radius=5, orientation=0)"
+
+ p = mpatches.CirclePolygon(xy=(1, 2), radius=5, resolution=20)
+ assert str(p) == "CirclePolygon((1, 2), radius=5, resolution=20)"
+
+ p = mpatches.FancyBboxPatch((1, 2), width=3, height=4)
+ assert str(p) == "FancyBboxPatch((1, 2), width=3, height=4)"
+
+ # Further nice __str__ which cannot be `eval`uated:
+ path = mpath.Path([(1, 2), (2, 2), (1, 2)], closed=True)
+ p = mpatches.PathPatch(path)
+ assert str(p) == "PathPatch3((1, 2) ...)"
+
+ p = mpatches.Polygon(np.empty((0, 2)))
+ assert str(p) == "Polygon0()"
+
+ data = [[1, 2], [2, 2], [1, 2]]
+ p = mpatches.Polygon(data)
+ assert str(p) == "Polygon3((1, 2) ...)"
+
+ p = mpatches.FancyArrowPatch(path=path)
+ assert str(p)[:27] == "FancyArrowPatch(Path(array("
+
+ p = mpatches.FancyArrowPatch((1, 2), (3, 4))
+ assert str(p) == "FancyArrowPatch((1, 2)->(3, 4))"
+
+ p = mpatches.ConnectionPatch((1, 2), (3, 4), 'data')
+ assert str(p) == "ConnectionPatch((1, 2), (3, 4))"
+
+ s = mpatches.Shadow(p, 1, 1)
+ assert str(s) == "Shadow(ConnectionPatch((1, 2), (3, 4)))"
+
+ # Not testing Arrow, FancyArrow here
+ # because they seem to exist only for historical reasons.
+
+
+@image_comparison(['multi_color_hatch'], remove_text=True, style='default')
+def test_multi_color_hatch():
+ fig, ax = plt.subplots()
+
+ rects = ax.bar(range(5), range(1, 6))
+ for i, rect in enumerate(rects):
+ rect.set_facecolor('none')
+ rect.set_edgecolor(f'C{i}')
+ rect.set_hatch('/')
+
+ ax.autoscale_view()
+ ax.autoscale(False)
+
+ for i in range(5):
+ with mpl.style.context({'hatch.color': f'C{i}'}):
+ r = Rectangle((i - .8 / 2, 5), .8, 1, hatch='//', fc='none')
+ ax.add_patch(r)
+
+
+@image_comparison(['units_rectangle.png'])
+def test_units_rectangle():
+ import matplotlib.testing.jpl_units as U
+ U.register()
+
+ p = mpatches.Rectangle((5*U.km, 6*U.km), 1*U.km, 2*U.km)
+
+ fig, ax = plt.subplots()
+ ax.add_patch(p)
+ ax.set_xlim([4*U.km, 7*U.km])
+ ax.set_ylim([5*U.km, 9*U.km])
+
+
+@image_comparison(['connection_patch.png'], style='mpl20', remove_text=True,
+ tol=0 if platform.machine() == 'x86_64' else 0.024)
+def test_connection_patch():
+ fig, (ax1, ax2) = plt.subplots(1, 2)
+
+ con = mpatches.ConnectionPatch(xyA=(0.1, 0.1), xyB=(0.9, 0.9),
+ coordsA='data', coordsB='data',
+ axesA=ax2, axesB=ax1,
+ arrowstyle="->")
+ ax2.add_artist(con)
+
+ xyA = (0.6, 1.0) # in axes coordinates
+ xyB = (0.0, 0.2) # x in axes coordinates, y in data coordinates
+ coordsA = "axes fraction"
+ coordsB = ax2.get_yaxis_transform()
+ con = mpatches.ConnectionPatch(xyA=xyA, xyB=xyB, coordsA=coordsA,
+ coordsB=coordsB, arrowstyle="-")
+ ax2.add_artist(con)
+
+
+@check_figures_equal(extensions=["png"])
+def test_connection_patch_fig(fig_test, fig_ref):
+ # Test that connection patch can be added as figure artist, and that figure
+ # pixels count negative values from the top right corner (this API may be
+ # changed in the future).
+ ax1, ax2 = fig_test.subplots(1, 2)
+ con = mpatches.ConnectionPatch(
+ xyA=(.3, .2), coordsA="data", axesA=ax1,
+ xyB=(-30, -20), coordsB="figure pixels",
+ arrowstyle="->", shrinkB=5)
+ fig_test.add_artist(con)
+
+ ax1, ax2 = fig_ref.subplots(1, 2)
+ bb = fig_ref.bbox
+ # Necessary so that pixel counts match on both sides.
+ plt.rcParams["savefig.dpi"] = plt.rcParams["figure.dpi"]
+ con = mpatches.ConnectionPatch(
+ xyA=(.3, .2), coordsA="data", axesA=ax1,
+ xyB=(bb.width - 30, bb.height - 20), coordsB="figure pixels",
+ arrowstyle="->", shrinkB=5)
+ fig_ref.add_artist(con)
+
+
+@check_figures_equal(extensions=["png"])
+def test_connection_patch_pixel_points(fig_test, fig_ref):
+ xyA_pts = (.3, .2)
+ xyB_pts = (-30, -20)
+
+ ax1, ax2 = fig_test.subplots(1, 2)
+ con = mpatches.ConnectionPatch(xyA=xyA_pts, coordsA="axes points", axesA=ax1,
+ xyB=xyB_pts, coordsB="figure points",
+ arrowstyle="->", shrinkB=5)
+ fig_test.add_artist(con)
+
+ plt.rcParams["savefig.dpi"] = plt.rcParams["figure.dpi"]
+
+ ax1, ax2 = fig_ref.subplots(1, 2)
+ xyA_pix = (xyA_pts[0]*(fig_ref.dpi/72), xyA_pts[1]*(fig_ref.dpi/72))
+ xyB_pix = (xyB_pts[0]*(fig_ref.dpi/72), xyB_pts[1]*(fig_ref.dpi/72))
+ con = mpatches.ConnectionPatch(xyA=xyA_pix, coordsA="axes pixels", axesA=ax1,
+ xyB=xyB_pix, coordsB="figure pixels",
+ arrowstyle="->", shrinkB=5)
+ fig_ref.add_artist(con)
+
+
+def test_datetime_rectangle():
+ # Check that creating a rectangle with timedeltas doesn't fail
+ from datetime import datetime, timedelta
+
+ start = datetime(2017, 1, 1, 0, 0, 0)
+ delta = timedelta(seconds=16)
+ patch = mpatches.Rectangle((start, 0), delta, 1)
+
+ fig, ax = plt.subplots()
+ ax.add_patch(patch)
+
+
+def test_datetime_datetime_fails():
+ from datetime import datetime
+
+ start = datetime(2017, 1, 1, 0, 0, 0)
+ dt_delta = datetime(1970, 1, 5) # Will be 5 days if units are done wrong.
+
+ with pytest.raises(TypeError):
+ mpatches.Rectangle((start, 0), dt_delta, 1)
+
+ with pytest.raises(TypeError):
+ mpatches.Rectangle((0, start), 1, dt_delta)
+
+
+def test_contains_point():
+ ell = mpatches.Ellipse((0.5, 0.5), 0.5, 1.0)
+ points = [(0.0, 0.5), (0.2, 0.5), (0.25, 0.5), (0.5, 0.5)]
+ path = ell.get_path()
+ transform = ell.get_transform()
+ radius = ell._process_radius(None)
+ expected = np.array([path.contains_point(point,
+ transform,
+ radius) for point in points])
+ result = np.array([ell.contains_point(point) for point in points])
+ assert np.all(result == expected)
+
+
+def test_contains_points():
+ ell = mpatches.Ellipse((0.5, 0.5), 0.5, 1.0)
+ points = [(0.0, 0.5), (0.2, 0.5), (0.25, 0.5), (0.5, 0.5)]
+ path = ell.get_path()
+ transform = ell.get_transform()
+ radius = ell._process_radius(None)
+ expected = path.contains_points(points, transform, radius)
+ result = ell.contains_points(points)
+ assert np.all(result == expected)
+
+
+# Currently fails with pdf/svg, probably because some parts assume a dpi of 72.
+@check_figures_equal(extensions=["png"])
+def test_shadow(fig_test, fig_ref):
+ xy = np.array([.2, .3])
+ dxy = np.array([.1, .2])
+ # We need to work around the nonsensical (dpi-dependent) interpretation of
+ # offsets by the Shadow class...
+ plt.rcParams["savefig.dpi"] = "figure"
+ # Test image.
+ a1 = fig_test.subplots()
+ rect = mpatches.Rectangle(xy=xy, width=.5, height=.5)
+ shadow = mpatches.Shadow(rect, ox=dxy[0], oy=dxy[1])
+ a1.add_patch(rect)
+ a1.add_patch(shadow)
+ # Reference image.
+ a2 = fig_ref.subplots()
+ rect = mpatches.Rectangle(xy=xy, width=.5, height=.5)
+ shadow = mpatches.Rectangle(
+ xy=xy + fig_ref.dpi / 72 * dxy, width=.5, height=.5,
+ fc=np.asarray(mcolors.to_rgb(rect.get_facecolor())) * .3,
+ ec=np.asarray(mcolors.to_rgb(rect.get_facecolor())) * .3,
+ alpha=.5)
+ a2.add_patch(shadow)
+ a2.add_patch(rect)
+
+
+def test_fancyarrow_units():
+ from datetime import datetime
+ # Smoke test to check that FancyArrowPatch works with units
+ dtime = datetime(2000, 1, 1)
+ fig, ax = plt.subplots()
+ arrow = FancyArrowPatch((0, dtime), (0.01, dtime))
+
+
+def test_fancyarrow_setdata():
+ fig, ax = plt.subplots()
+ arrow = ax.arrow(0, 0, 10, 10, head_length=5, head_width=1, width=.5)
+ expected1 = np.array(
+ [[13.54, 13.54],
+ [10.35, 9.65],
+ [10.18, 9.82],
+ [0.18, -0.18],
+ [-0.18, 0.18],
+ [9.82, 10.18],
+ [9.65, 10.35],
+ [13.54, 13.54]]
+ )
+ assert np.allclose(expected1, np.round(arrow.verts, 2))
+
+ expected2 = np.array(
+ [[16.71, 16.71],
+ [16.71, 15.29],
+ [16.71, 15.29],
+ [1.71, 0.29],
+ [0.29, 1.71],
+ [15.29, 16.71],
+ [15.29, 16.71],
+ [16.71, 16.71]]
+ )
+ arrow.set_data(
+ x=1, y=1, dx=15, dy=15, width=2, head_width=2, head_length=1
+ )
+ assert np.allclose(expected2, np.round(arrow.verts, 2))
+
+
+@image_comparison(["large_arc.svg"], style="mpl20")
+def test_large_arc():
+ fig, (ax1, ax2) = plt.subplots(1, 2)
+ x = 210
+ y = -2115
+ diameter = 4261
+ for ax in [ax1, ax2]:
+ a = Arc((x, y), diameter, diameter, lw=2, color='k')
+ ax.add_patch(a)
+ ax.set_axis_off()
+ ax.set_aspect('equal')
+ # force the high accuracy case
+ ax1.set_xlim(7, 8)
+ ax1.set_ylim(5, 6)
+
+ # force the low accuracy case
+ ax2.set_xlim(-25000, 18000)
+ ax2.set_ylim(-20000, 6600)
+
+
+@image_comparison(["all_quadrants_arcs.svg"], style="mpl20")
+def test_rotated_arcs():
+ fig, ax_arr = plt.subplots(2, 2, squeeze=False, figsize=(10, 10))
+
+ scale = 10_000_000
+ diag_centers = ((-1, -1), (-1, 1), (1, 1), (1, -1))
+ on_axis_centers = ((0, 1), (1, 0), (0, -1), (-1, 0))
+ skews = ((2, 2), (2, 1/10), (2, 1/100), (2, 1/1000))
+
+ for ax, (sx, sy) in zip(ax_arr.ravel(), skews):
+ k = 0
+ for prescale, centers in zip((1 - .0001, (1 - .0001) / np.sqrt(2)),
+ (on_axis_centers, diag_centers)):
+ for j, (x_sign, y_sign) in enumerate(centers, start=k):
+ a = Arc(
+ (x_sign * scale * prescale,
+ y_sign * scale * prescale),
+ scale * sx,
+ scale * sy,
+ lw=4,
+ color=f"C{j}",
+ zorder=1 + j,
+ angle=np.rad2deg(np.arctan2(y_sign, x_sign)) % 360,
+ label=f'big {j}',
+ gid=f'big {j}'
+ )
+ ax.add_patch(a)
+
+ k = j+1
+ ax.set_xlim(-scale / 4000, scale / 4000)
+ ax.set_ylim(-scale / 4000, scale / 4000)
+ ax.axhline(0, color="k")
+ ax.axvline(0, color="k")
+ ax.set_axis_off()
+ ax.set_aspect("equal")
+
+
+def test_fancyarrow_shape_error():
+ with pytest.raises(ValueError, match="Got unknown shape: 'foo'"):
+ FancyArrow(0, 0, 0.2, 0.2, shape='foo')
+
+
+@pytest.mark.parametrize('fmt, match', (
+ ("foo", "Unknown style: 'foo'"),
+ ("Round,foo", "Incorrect style argument: 'Round,foo'"),
+))
+def test_boxstyle_errors(fmt, match):
+ with pytest.raises(ValueError, match=match):
+ BoxStyle(fmt)
+
+
+@image_comparison(baseline_images=['annulus'], extensions=['png'])
+def test_annulus():
+
+ fig, ax = plt.subplots()
+ cir = Annulus((0.5, 0.5), 0.2, 0.05, fc='g') # circular annulus
+ ell = Annulus((0.5, 0.5), (0.5, 0.3), 0.1, 45, # elliptical
+ fc='m', ec='b', alpha=0.5, hatch='xxx')
+ ax.add_patch(cir)
+ ax.add_patch(ell)
+ ax.set_aspect('equal')
+
+
+@image_comparison(baseline_images=['annulus'], extensions=['png'])
+def test_annulus_setters():
+
+ fig, ax = plt.subplots()
+ cir = Annulus((0., 0.), 0.2, 0.01, fc='g') # circular annulus
+ ell = Annulus((0., 0.), (1, 2), 0.1, 0, # elliptical
+ fc='m', ec='b', alpha=0.5, hatch='xxx')
+ ax.add_patch(cir)
+ ax.add_patch(ell)
+ ax.set_aspect('equal')
+
+ cir.center = (0.5, 0.5)
+ cir.radii = 0.2
+ cir.width = 0.05
+
+ ell.center = (0.5, 0.5)
+ ell.radii = (0.5, 0.3)
+ ell.width = 0.1
+ ell.angle = 45
+
+
+@image_comparison(baseline_images=['annulus'], extensions=['png'])
+def test_annulus_setters2():
+
+ fig, ax = plt.subplots()
+ cir = Annulus((0., 0.), 0.2, 0.01, fc='g') # circular annulus
+ ell = Annulus((0., 0.), (1, 2), 0.1, 0, # elliptical
+ fc='m', ec='b', alpha=0.5, hatch='xxx')
+ ax.add_patch(cir)
+ ax.add_patch(ell)
+ ax.set_aspect('equal')
+
+ cir.center = (0.5, 0.5)
+ cir.set_semimajor(0.2)
+ cir.set_semiminor(0.2)
+ assert cir.radii == (0.2, 0.2)
+ cir.width = 0.05
+
+ ell.center = (0.5, 0.5)
+ ell.set_semimajor(0.5)
+ ell.set_semiminor(0.3)
+ assert ell.radii == (0.5, 0.3)
+ ell.width = 0.1
+ ell.angle = 45
+
+
+def test_degenerate_polygon():
+ point = [0, 0]
+ correct_extents = Bbox([point, point]).extents
+ assert np.all(Polygon([point]).get_extents().extents == correct_extents)
+
+
+@pytest.mark.parametrize('kwarg', ('edgecolor', 'facecolor'))
+def test_color_override_warning(kwarg):
+ with pytest.warns(UserWarning,
+ match="Setting the 'color' property will override "
+ "the edgecolor or facecolor properties."):
+ Patch(color='black', **{kwarg: 'black'})
+
+
+def test_empty_verts():
+ poly = Polygon(np.zeros((0, 2)))
+ assert poly.get_verts() == []
+
+
+def test_default_antialiased():
+ patch = Patch()
+
+ patch.set_antialiased(not rcParams['patch.antialiased'])
+ assert patch.get_antialiased() == (not rcParams['patch.antialiased'])
+ # Check that None resets the state
+ patch.set_antialiased(None)
+ assert patch.get_antialiased() == rcParams['patch.antialiased']
+
+
+def test_default_linestyle():
+ patch = Patch()
+ patch.set_linestyle('--')
+ patch.set_linestyle(None)
+ assert patch.get_linestyle() == 'solid'
+
+
+def test_default_capstyle():
+ patch = Patch()
+ assert patch.get_capstyle() == 'butt'
+
+
+def test_default_joinstyle():
+ patch = Patch()
+ assert patch.get_joinstyle() == 'miter'
+
+
+@image_comparison(["autoscale_arc"], extensions=['png', 'svg'],
+ style="mpl20", remove_text=True)
+def test_autoscale_arc():
+ fig, axs = plt.subplots(1, 3, figsize=(4, 1))
+ arc_lists = (
+ [Arc((0, 0), 1, 1, theta1=0, theta2=90)],
+ [Arc((0.5, 0.5), 1.5, 0.5, theta1=10, theta2=20)],
+ [Arc((0.5, 0.5), 1.5, 0.5, theta1=10, theta2=20),
+ Arc((0.5, 0.5), 2.5, 0.5, theta1=110, theta2=120),
+ Arc((0.5, 0.5), 3.5, 0.5, theta1=210, theta2=220),
+ Arc((0.5, 0.5), 4.5, 0.5, theta1=310, theta2=320)])
+
+ for ax, arcs in zip(axs, arc_lists):
+ for arc in arcs:
+ ax.add_patch(arc)
+ ax.autoscale()
+
+
+@check_figures_equal(extensions=["png", 'svg', 'pdf', 'eps'])
+def test_arc_in_collection(fig_test, fig_ref):
+ arc1 = Arc([.5, .5], .5, 1, theta1=0, theta2=60, angle=20)
+ arc2 = Arc([.5, .5], .5, 1, theta1=0, theta2=60, angle=20)
+ col = mcollections.PatchCollection(patches=[arc2], facecolors='none',
+ edgecolors='k')
+ fig_ref.subplots().add_patch(arc1)
+ fig_test.subplots().add_collection(col)
+
+
+@check_figures_equal(extensions=["png", 'svg', 'pdf', 'eps'])
+def test_modifying_arc(fig_test, fig_ref):
+ arc1 = Arc([.5, .5], .5, 1, theta1=0, theta2=60, angle=20)
+ arc2 = Arc([.5, .5], 1.5, 1, theta1=0, theta2=60, angle=10)
+ fig_ref.subplots().add_patch(arc1)
+ fig_test.subplots().add_patch(arc2)
+ arc2.set_width(.5)
+ arc2.set_angle(20)
+
+
+def test_arrow_set_data():
+ fig, ax = plt.subplots()
+ arrow = mpl.patches.Arrow(2, 0, 0, 10)
+ expected1 = np.array(
+ [[1.9, 0.],
+ [2.1, -0.],
+ [2.1, 8.],
+ [2.3, 8.],
+ [2., 10.],
+ [1.7, 8.],
+ [1.9, 8.],
+ [1.9, 0.]]
+ )
+ assert np.allclose(expected1, np.round(arrow.get_verts(), 2))
+
+ expected2 = np.array(
+ [[0.39, 0.04],
+ [0.61, -0.04],
+ [3.01, 6.36],
+ [3.24, 6.27],
+ [3.5, 8.],
+ [2.56, 6.53],
+ [2.79, 6.44],
+ [0.39, 0.04]]
+ )
+ arrow.set_data(x=.5, dx=3, dy=8, width=1.2)
+ assert np.allclose(expected2, np.round(arrow.get_verts(), 2))
+
+
+@check_figures_equal(extensions=["png", "pdf", "svg", "eps"])
+def test_set_and_get_hatch_linewidth(fig_test, fig_ref):
+ ax_test = fig_test.add_subplot()
+ ax_ref = fig_ref.add_subplot()
+
+ lw = 2.0
+
+ with plt.rc_context({"hatch.linewidth": lw}):
+ ax_ref.add_patch(mpatches.Rectangle((0, 0), 1, 1, hatch="x"))
+
+ ax_test.add_patch(mpatches.Rectangle((0, 0), 1, 1, hatch="x"))
+ ax_test.patches[0].set_hatch_linewidth(lw)
+
+ assert ax_ref.patches[0].get_hatch_linewidth() == lw
+ assert ax_test.patches[0].get_hatch_linewidth() == lw
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_polar.py b/lib/python3.12/site-packages/matplotlib/tests/test_polar.py
new file mode 100644
index 0000000000000000000000000000000000000000..87c8efe3045e4968c0df6260da6e731bdcdc151b
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_polar.py
@@ -0,0 +1,508 @@
+import numpy as np
+from numpy.testing import assert_allclose
+import pytest
+
+import matplotlib as mpl
+from matplotlib import pyplot as plt
+from matplotlib.testing.decorators import image_comparison, check_figures_equal
+
+
+@image_comparison(['polar_axes.png'], style='default', tol=0.012)
+def test_polar_annotations():
+ # You can specify the xypoint and the xytext in different positions and
+ # coordinate systems, and optionally turn on a connecting line and mark the
+ # point with a marker. Annotations work on polar axes too. In the example
+ # below, the xy point is in native coordinates (xycoords defaults to
+ # 'data'). For a polar axes, this is in (theta, radius) space. The text
+ # in this example is placed in the fractional figure coordinate system.
+ # Text keyword args like horizontal and vertical alignment are respected.
+
+ # Setup some data
+ r = np.arange(0.0, 1.0, 0.001)
+ theta = 2.0 * 2.0 * np.pi * r
+
+ fig = plt.figure()
+ ax = fig.add_subplot(polar=True)
+ line, = ax.plot(theta, r, color='#ee8d18', lw=3)
+ line, = ax.plot((0, 0), (0, 1), color="#0000ff", lw=1)
+
+ ind = 800
+ thisr, thistheta = r[ind], theta[ind]
+ ax.plot([thistheta], [thisr], 'o')
+ ax.annotate('a polar annotation',
+ xy=(thistheta, thisr), # theta, radius
+ xytext=(0.05, 0.05), # fraction, fraction
+ textcoords='figure fraction',
+ arrowprops=dict(facecolor='black', shrink=0.05),
+ horizontalalignment='left',
+ verticalalignment='baseline',
+ )
+
+ ax.tick_params(axis='x', tick1On=True, tick2On=True, direction='out')
+
+
+@image_comparison(['polar_coords.png'], style='default', remove_text=True,
+ tol=0.014)
+def test_polar_coord_annotations():
+ # You can also use polar notation on a cartesian axes. Here the native
+ # coordinate system ('data') is cartesian, so you need to specify the
+ # xycoords and textcoords as 'polar' if you want to use (theta, radius).
+ el = mpl.patches.Ellipse((0, 0), 10, 20, facecolor='r', alpha=0.5)
+
+ fig = plt.figure()
+ ax = fig.add_subplot(aspect='equal')
+
+ ax.add_artist(el)
+ el.set_clip_box(ax.bbox)
+
+ ax.annotate('the top',
+ xy=(np.pi/2., 10.), # theta, radius
+ xytext=(np.pi/3, 20.), # theta, radius
+ xycoords='polar',
+ textcoords='polar',
+ arrowprops=dict(facecolor='black', shrink=0.05),
+ horizontalalignment='left',
+ verticalalignment='baseline',
+ clip_on=True, # clip to the axes bounding box
+ )
+
+ ax.set_xlim(-20, 20)
+ ax.set_ylim(-20, 20)
+
+
+@image_comparison(['polar_alignment.png'])
+def test_polar_alignment():
+ # Test changing the vertical/horizontal alignment of a polar graph.
+ angles = np.arange(0, 360, 90)
+ grid_values = [0, 0.2, 0.4, 0.6, 0.8, 1]
+
+ fig = plt.figure()
+ rect = [0.1, 0.1, 0.8, 0.8]
+
+ horizontal = fig.add_axes(rect, polar=True, label='horizontal')
+ horizontal.set_thetagrids(angles)
+
+ vertical = fig.add_axes(rect, polar=True, label='vertical')
+ vertical.patch.set_visible(False)
+
+ for i in range(2):
+ fig.axes[i].set_rgrids(
+ grid_values, angle=angles[i],
+ horizontalalignment='left', verticalalignment='top')
+
+
+def test_polar_twice():
+ fig = plt.figure()
+ plt.polar([1, 2], [.1, .2])
+ plt.polar([3, 4], [.3, .4])
+ assert len(fig.axes) == 1, 'More than one polar Axes created.'
+
+
+@check_figures_equal(extensions=['png'])
+def test_polar_wrap(fig_test, fig_ref):
+ ax = fig_test.add_subplot(projection="polar")
+ ax.plot(np.deg2rad([179, -179]), [0.2, 0.1])
+ ax.plot(np.deg2rad([2, -2]), [0.2, 0.1])
+ ax = fig_ref.add_subplot(projection="polar")
+ ax.plot(np.deg2rad([179, 181]), [0.2, 0.1])
+ ax.plot(np.deg2rad([2, 358]), [0.2, 0.1])
+
+
+@check_figures_equal(extensions=['png'])
+def test_polar_units_1(fig_test, fig_ref):
+ import matplotlib.testing.jpl_units as units
+ units.register()
+ xs = [30.0, 45.0, 60.0, 90.0]
+ ys = [1.0, 2.0, 3.0, 4.0]
+
+ plt.figure(fig_test.number)
+ plt.polar([x * units.deg for x in xs], ys)
+
+ ax = fig_ref.add_subplot(projection="polar")
+ ax.plot(np.deg2rad(xs), ys)
+ ax.set(xlabel="deg")
+
+
+@check_figures_equal(extensions=['png'])
+def test_polar_units_2(fig_test, fig_ref):
+ import matplotlib.testing.jpl_units as units
+ units.register()
+ xs = [30.0, 45.0, 60.0, 90.0]
+ xs_deg = [x * units.deg for x in xs]
+ ys = [1.0, 2.0, 3.0, 4.0]
+ ys_km = [y * units.km for y in ys]
+
+ plt.figure(fig_test.number)
+ # test {theta,r}units.
+ plt.polar(xs_deg, ys_km, thetaunits="rad", runits="km")
+ assert isinstance(plt.gca().xaxis.get_major_formatter(),
+ units.UnitDblFormatter)
+
+ ax = fig_ref.add_subplot(projection="polar")
+ ax.plot(np.deg2rad(xs), ys)
+ ax.xaxis.set_major_formatter(mpl.ticker.FuncFormatter("{:.12}".format))
+ ax.set(xlabel="rad", ylabel="km")
+
+
+@image_comparison(['polar_rmin.png'], style='default')
+def test_polar_rmin():
+ r = np.arange(0, 3.0, 0.01)
+ theta = 2*np.pi*r
+
+ fig = plt.figure()
+ ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True)
+ ax.plot(theta, r)
+ ax.set_rmax(2.0)
+ ax.set_rmin(0.5)
+
+
+@image_comparison(['polar_negative_rmin.png'], style='default')
+def test_polar_negative_rmin():
+ r = np.arange(-3.0, 0.0, 0.01)
+ theta = 2*np.pi*r
+
+ fig = plt.figure()
+ ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True)
+ ax.plot(theta, r)
+ ax.set_rmax(0.0)
+ ax.set_rmin(-3.0)
+
+
+@image_comparison(['polar_rorigin.png'], style='default')
+def test_polar_rorigin():
+ r = np.arange(0, 3.0, 0.01)
+ theta = 2*np.pi*r
+
+ fig = plt.figure()
+ ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True)
+ ax.plot(theta, r)
+ ax.set_rmax(2.0)
+ ax.set_rmin(0.5)
+ ax.set_rorigin(0.0)
+
+
+@image_comparison(['polar_invertedylim.png'], style='default')
+def test_polar_invertedylim():
+ fig = plt.figure()
+ ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True)
+ ax.set_ylim(2, 0)
+
+
+@image_comparison(['polar_invertedylim_rorigin.png'], style='default')
+def test_polar_invertedylim_rorigin():
+ fig = plt.figure()
+ ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True)
+ ax.yaxis.set_inverted(True)
+ # Set the rlims to inverted (2, 0) without calling set_rlim, to check that
+ # viewlims are correctly unstaled before draw()ing.
+ ax.plot([0, 0], [0, 2], c="none")
+ ax.margins(0)
+ ax.set_rorigin(3)
+
+
+@image_comparison(['polar_theta_position.png'], style='default')
+def test_polar_theta_position():
+ r = np.arange(0, 3.0, 0.01)
+ theta = 2*np.pi*r
+
+ fig = plt.figure()
+ ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True)
+ ax.plot(theta, r)
+ ax.set_theta_zero_location("NW", 30)
+ ax.set_theta_direction('clockwise')
+
+
+@image_comparison(['polar_rlabel_position.png'], style='default')
+def test_polar_rlabel_position():
+ fig = plt.figure()
+ ax = fig.add_subplot(projection='polar')
+ ax.set_rlabel_position(315)
+ ax.tick_params(rotation='auto')
+
+
+@image_comparison(['polar_title_position.png'], style='mpl20')
+def test_polar_title_position():
+ fig = plt.figure()
+ ax = fig.add_subplot(projection='polar')
+ ax.set_title('foo')
+
+
+@image_comparison(['polar_theta_wedge.png'], style='default')
+def test_polar_theta_limits():
+ r = np.arange(0, 3.0, 0.01)
+ theta = 2*np.pi*r
+
+ theta_mins = np.arange(15.0, 361.0, 90.0)
+ theta_maxs = np.arange(50.0, 361.0, 90.0)
+ DIRECTIONS = ('out', 'in', 'inout')
+
+ fig, axs = plt.subplots(len(theta_mins), len(theta_maxs),
+ subplot_kw={'polar': True},
+ figsize=(8, 6))
+
+ for i, start in enumerate(theta_mins):
+ for j, end in enumerate(theta_maxs):
+ ax = axs[i, j]
+ ax.plot(theta, r)
+ if start < end:
+ ax.set_thetamin(start)
+ ax.set_thetamax(end)
+ else:
+ # Plot with clockwise orientation instead.
+ ax.set_thetamin(end)
+ ax.set_thetamax(start)
+ ax.set_theta_direction('clockwise')
+ ax.tick_params(tick1On=True, tick2On=True,
+ direction=DIRECTIONS[i % len(DIRECTIONS)],
+ rotation='auto')
+ ax.yaxis.set_tick_params(label2On=True, rotation='auto')
+ ax.xaxis.get_major_locator().base.set_params( # backcompat
+ steps=[1, 2, 2.5, 5, 10])
+
+
+@check_figures_equal(extensions=["png"])
+def test_polar_rlim(fig_test, fig_ref):
+ ax = fig_test.subplots(subplot_kw={'polar': True})
+ ax.set_rlim(top=10)
+ ax.set_rlim(bottom=.5)
+
+ ax = fig_ref.subplots(subplot_kw={'polar': True})
+ ax.set_rmax(10.)
+ ax.set_rmin(.5)
+
+
+@check_figures_equal(extensions=["png"])
+def test_polar_rlim_bottom(fig_test, fig_ref):
+ ax = fig_test.subplots(subplot_kw={'polar': True})
+ ax.set_rlim(bottom=[.5, 10])
+
+ ax = fig_ref.subplots(subplot_kw={'polar': True})
+ ax.set_rmax(10.)
+ ax.set_rmin(.5)
+
+
+def test_polar_rlim_zero():
+ ax = plt.figure().add_subplot(projection='polar')
+ ax.plot(np.arange(10), np.arange(10) + .01)
+ assert ax.get_ylim()[0] == 0
+
+
+def test_polar_no_data():
+ plt.subplot(projection="polar")
+ ax = plt.gca()
+ assert ax.get_rmin() == 0 and ax.get_rmax() == 1
+ plt.close("all")
+ # Used to behave differently (by triggering an autoscale with no data).
+ plt.polar()
+ ax = plt.gca()
+ assert ax.get_rmin() == 0 and ax.get_rmax() == 1
+
+
+def test_polar_default_log_lims():
+ plt.subplot(projection='polar')
+ ax = plt.gca()
+ ax.set_rscale('log')
+ assert ax.get_rmin() > 0
+
+
+def test_polar_not_datalim_adjustable():
+ ax = plt.figure().add_subplot(projection="polar")
+ with pytest.raises(ValueError):
+ ax.set_adjustable("datalim")
+
+
+def test_polar_gridlines():
+ fig = plt.figure()
+ ax = fig.add_subplot(polar=True)
+ # make all major grid lines lighter, only x grid lines set in 2.1.0
+ ax.grid(alpha=0.2)
+ # hide y tick labels, no effect in 2.1.0
+ plt.setp(ax.yaxis.get_ticklabels(), visible=False)
+ fig.canvas.draw()
+ assert ax.xaxis.majorTicks[0].gridline.get_alpha() == .2
+ assert ax.yaxis.majorTicks[0].gridline.get_alpha() == .2
+
+
+def test_get_tightbbox_polar():
+ fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
+ fig.canvas.draw()
+ bb = ax.get_tightbbox(fig.canvas.get_renderer())
+ assert_allclose(
+ bb.extents, [107.7778, 29.2778, 539.7847, 450.7222], rtol=1e-03)
+
+
+@check_figures_equal(extensions=["png"])
+def test_polar_interpolation_steps_constant_r(fig_test, fig_ref):
+ # Check that an extra half-turn doesn't make any difference -- modulo
+ # antialiasing, which we disable here.
+ p1 = (fig_test.add_subplot(121, projection="polar")
+ .bar([0], [1], 3*np.pi, edgecolor="none", antialiased=False))
+ p2 = (fig_test.add_subplot(122, projection="polar")
+ .bar([0], [1], -3*np.pi, edgecolor="none", antialiased=False))
+ p3 = (fig_ref.add_subplot(121, projection="polar")
+ .bar([0], [1], 2*np.pi, edgecolor="none", antialiased=False))
+ p4 = (fig_ref.add_subplot(122, projection="polar")
+ .bar([0], [1], -2*np.pi, edgecolor="none", antialiased=False))
+
+
+@check_figures_equal(extensions=["png"])
+def test_polar_interpolation_steps_variable_r(fig_test, fig_ref):
+ l, = fig_test.add_subplot(projection="polar").plot([0, np.pi/2], [1, 2])
+ l.get_path()._interpolation_steps = 100
+ fig_ref.add_subplot(projection="polar").plot(
+ np.linspace(0, np.pi/2, 101), np.linspace(1, 2, 101))
+
+
+def test_thetalim_valid_invalid():
+ ax = plt.subplot(projection='polar')
+ ax.set_thetalim(0, 2 * np.pi) # doesn't raise.
+ ax.set_thetalim(thetamin=800, thetamax=440) # doesn't raise.
+ with pytest.raises(ValueError,
+ match='angle range must be less than a full circle'):
+ ax.set_thetalim(0, 3 * np.pi)
+ with pytest.raises(ValueError,
+ match='angle range must be less than a full circle'):
+ ax.set_thetalim(thetamin=800, thetamax=400)
+
+
+def test_thetalim_args():
+ ax = plt.subplot(projection='polar')
+ ax.set_thetalim(0, 1)
+ assert tuple(np.radians((ax.get_thetamin(), ax.get_thetamax()))) == (0, 1)
+ ax.set_thetalim((2, 3))
+ assert tuple(np.radians((ax.get_thetamin(), ax.get_thetamax()))) == (2, 3)
+
+
+def test_default_thetalocator():
+ # Ideally we would check AAAABBC, but the smallest axes currently puts a
+ # single tick at 150° because MaxNLocator doesn't have a way to accept 15°
+ # while rejecting 150°.
+ fig, axs = plt.subplot_mosaic(
+ "AAAABB.", subplot_kw={"projection": "polar"})
+ for ax in axs.values():
+ ax.set_thetalim(0, np.pi)
+ for ax in axs.values():
+ ticklocs = np.degrees(ax.xaxis.get_majorticklocs()).tolist()
+ assert pytest.approx(90) in ticklocs
+ assert pytest.approx(100) not in ticklocs
+
+
+def test_axvspan():
+ ax = plt.subplot(projection="polar")
+ span = ax.axvspan(0, np.pi/4)
+ assert span.get_path()._interpolation_steps > 1
+
+
+@check_figures_equal(extensions=["png"])
+def test_remove_shared_polar(fig_ref, fig_test):
+ # Removing shared polar axes used to crash. Test removing them, keeping in
+ # both cases just the lower left axes of a grid to avoid running into a
+ # separate issue (now being fixed) of ticklabel visibility for shared axes.
+ axs = fig_ref.subplots(
+ 2, 2, sharex=True, subplot_kw={"projection": "polar"})
+ for i in [0, 1, 3]:
+ axs.flat[i].remove()
+ axs = fig_test.subplots(
+ 2, 2, sharey=True, subplot_kw={"projection": "polar"})
+ for i in [0, 1, 3]:
+ axs.flat[i].remove()
+
+
+def test_shared_polar_keeps_ticklabels():
+ fig, axs = plt.subplots(
+ 2, 2, subplot_kw={"projection": "polar"}, sharex=True, sharey=True)
+ fig.canvas.draw()
+ assert axs[0, 1].xaxis.majorTicks[0].get_visible()
+ assert axs[0, 1].yaxis.majorTicks[0].get_visible()
+ fig, axs = plt.subplot_mosaic(
+ "ab\ncd", subplot_kw={"projection": "polar"}, sharex=True, sharey=True)
+ fig.canvas.draw()
+ assert axs["b"].xaxis.majorTicks[0].get_visible()
+ assert axs["b"].yaxis.majorTicks[0].get_visible()
+
+
+def test_axvline_axvspan_do_not_modify_rlims():
+ ax = plt.subplot(projection="polar")
+ ax.axvspan(0, 1)
+ ax.axvline(.5)
+ ax.plot([.1, .2])
+ assert ax.get_ylim() == (0, .2)
+
+
+def test_cursor_precision():
+ ax = plt.subplot(projection="polar")
+ # Higher radii correspond to higher theta-precisions.
+ assert ax.format_coord(0, 0.005) == "θ=0.0π (0°), r=0.005"
+ assert ax.format_coord(0, .1) == "θ=0.00π (0°), r=0.100"
+ assert ax.format_coord(0, 1) == "θ=0.000π (0.0°), r=1.000"
+ assert ax.format_coord(1, 0.005) == "θ=0.3π (57°), r=0.005"
+ assert ax.format_coord(1, .1) == "θ=0.32π (57°), r=0.100"
+ assert ax.format_coord(1, 1) == "θ=0.318π (57.3°), r=1.000"
+ assert ax.format_coord(2, 0.005) == "θ=0.6π (115°), r=0.005"
+ assert ax.format_coord(2, .1) == "θ=0.64π (115°), r=0.100"
+ assert ax.format_coord(2, 1) == "θ=0.637π (114.6°), r=1.000"
+
+
+def test_custom_fmt_data():
+ ax = plt.subplot(projection="polar")
+ def millions(x):
+ return '$%1.1fM' % (x*1e-6)
+
+ # Test only x formatter
+ ax.fmt_xdata = None
+ ax.fmt_ydata = millions
+ assert ax.format_coord(12, 2e7) == "θ=3.8197186342π (687.54935416°), r=$20.0M"
+ assert ax.format_coord(1234, 2e6) == "θ=392.794399551π (70702.9919191°), r=$2.0M"
+ assert ax.format_coord(3, 100) == "θ=0.95493π (171.887°), r=$0.0M"
+
+ # Test only y formatter
+ ax.fmt_xdata = millions
+ ax.fmt_ydata = None
+ assert ax.format_coord(2e5, 1) == "θ=$0.2M, r=1.000"
+ assert ax.format_coord(1, .1) == "θ=$0.0M, r=0.100"
+ assert ax.format_coord(1e6, 0.005) == "θ=$1.0M, r=0.005"
+
+ # Test both x and y formatters
+ ax.fmt_xdata = millions
+ ax.fmt_ydata = millions
+ assert ax.format_coord(2e6, 2e4*3e5) == "θ=$2.0M, r=$6000.0M"
+ assert ax.format_coord(1e18, 12891328123) == "θ=$1000000000000.0M, r=$12891.3M"
+ assert ax.format_coord(63**7, 1081968*1024) == "θ=$3938980.6M, r=$1107.9M"
+
+
+@image_comparison(['polar_log.png'], style='default')
+def test_polar_log():
+ fig = plt.figure()
+ ax = fig.add_subplot(polar=True)
+
+ ax.set_rscale('log')
+ ax.set_rlim(1, 1000)
+
+ n = 100
+ ax.plot(np.linspace(0, 2 * np.pi, n), np.logspace(0, 2, n))
+
+
+def test_polar_neg_theta_lims():
+ fig = plt.figure()
+ ax = fig.add_subplot(projection='polar')
+ ax.set_thetalim(-np.pi, np.pi)
+ labels = [l.get_text() for l in ax.xaxis.get_ticklabels()]
+ assert labels == ['-180°', '-135°', '-90°', '-45°', '0°', '45°', '90°', '135°']
+
+
+@pytest.mark.parametrize("order", ["before", "after"])
+@image_comparison(baseline_images=['polar_errorbar.png'], remove_text=True,
+ style='mpl20')
+def test_polar_errorbar(order):
+ theta = np.arange(0, 2 * np.pi, np.pi / 8)
+ r = theta / np.pi / 2 + 0.5
+ fig = plt.figure(figsize=(5, 5))
+ ax = fig.add_subplot(projection='polar')
+ if order == "before":
+ ax.set_theta_zero_location("N")
+ ax.set_theta_direction(-1)
+ ax.errorbar(theta, r, xerr=0.1, yerr=0.1, capsize=7, fmt="o", c="seagreen")
+ else:
+ ax.errorbar(theta, r, xerr=0.1, yerr=0.1, capsize=7, fmt="o", c="seagreen")
+ ax.set_theta_zero_location("N")
+ ax.set_theta_direction(-1)
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_pyplot.py b/lib/python3.12/site-packages/matplotlib/tests/test_pyplot.py
new file mode 100644
index 0000000000000000000000000000000000000000..1aaa8dd93ca22671c188757c69f7a59898a3f937
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_pyplot.py
@@ -0,0 +1,485 @@
+import difflib
+
+import numpy as np
+import sys
+from pathlib import Path
+
+import pytest
+
+import matplotlib as mpl
+from matplotlib.testing import subprocess_run_for_testing
+from matplotlib import pyplot as plt
+
+
+def test_pyplot_up_to_date(tmp_path):
+ pytest.importorskip("black")
+
+ gen_script = Path(mpl.__file__).parents[2] / "tools/boilerplate.py"
+ if not gen_script.exists():
+ pytest.skip("boilerplate.py not found")
+ orig_contents = Path(plt.__file__).read_text()
+ plt_file = tmp_path / 'pyplot.py'
+ plt_file.write_text(orig_contents, 'utf-8')
+
+ subprocess_run_for_testing(
+ [sys.executable, str(gen_script), str(plt_file)],
+ check=True)
+ new_contents = plt_file.read_text('utf-8')
+
+ if orig_contents != new_contents:
+ diff_msg = '\n'.join(
+ difflib.unified_diff(
+ orig_contents.split('\n'), new_contents.split('\n'),
+ fromfile='found pyplot.py',
+ tofile='expected pyplot.py',
+ n=0, lineterm=''))
+ pytest.fail(
+ "pyplot.py is not up-to-date. Please run "
+ "'python tools/boilerplate.py' to update pyplot.py. "
+ "This needs to be done from an environment where your "
+ "current working copy is installed (e.g. 'pip install -e'd). "
+ "Here is a diff of unexpected differences:\n%s" % diff_msg
+ )
+
+
+def test_copy_docstring_and_deprecators(recwarn):
+ @mpl._api.rename_parameter(mpl.__version__, "old", "new")
+ @mpl._api.make_keyword_only(mpl.__version__, "kwo")
+ def func(new, kwo=None):
+ pass
+
+ @plt._copy_docstring_and_deprecators(func)
+ def wrapper_func(new, kwo=None):
+ pass
+
+ wrapper_func(None)
+ wrapper_func(new=None)
+ wrapper_func(None, kwo=None)
+ wrapper_func(new=None, kwo=None)
+ assert not recwarn
+ with pytest.warns(mpl.MatplotlibDeprecationWarning):
+ wrapper_func(old=None)
+ with pytest.warns(mpl.MatplotlibDeprecationWarning):
+ wrapper_func(None, None)
+
+
+def test_pyplot_box():
+ fig, ax = plt.subplots()
+ plt.box(False)
+ assert not ax.get_frame_on()
+ plt.box(True)
+ assert ax.get_frame_on()
+ plt.box()
+ assert not ax.get_frame_on()
+ plt.box()
+ assert ax.get_frame_on()
+
+
+def test_stackplot_smoke():
+ # Small smoke test for stackplot (see #12405)
+ plt.stackplot([1, 2, 3], [1, 2, 3])
+
+
+def test_nrows_error():
+ with pytest.raises(TypeError):
+ plt.subplot(nrows=1)
+ with pytest.raises(TypeError):
+ plt.subplot(ncols=1)
+
+
+def test_ioff():
+ plt.ion()
+ assert mpl.is_interactive()
+ with plt.ioff():
+ assert not mpl.is_interactive()
+ assert mpl.is_interactive()
+
+ plt.ioff()
+ assert not mpl.is_interactive()
+ with plt.ioff():
+ assert not mpl.is_interactive()
+ assert not mpl.is_interactive()
+
+
+def test_ion():
+ plt.ioff()
+ assert not mpl.is_interactive()
+ with plt.ion():
+ assert mpl.is_interactive()
+ assert not mpl.is_interactive()
+
+ plt.ion()
+ assert mpl.is_interactive()
+ with plt.ion():
+ assert mpl.is_interactive()
+ assert mpl.is_interactive()
+
+
+def test_nested_ion_ioff():
+ # initial state is interactive
+ plt.ion()
+
+ # mixed ioff/ion
+ with plt.ioff():
+ assert not mpl.is_interactive()
+ with plt.ion():
+ assert mpl.is_interactive()
+ assert not mpl.is_interactive()
+ assert mpl.is_interactive()
+
+ # redundant contexts
+ with plt.ioff():
+ with plt.ioff():
+ assert not mpl.is_interactive()
+ assert mpl.is_interactive()
+
+ with plt.ion():
+ plt.ioff()
+ assert mpl.is_interactive()
+
+ # initial state is not interactive
+ plt.ioff()
+
+ # mixed ioff/ion
+ with plt.ion():
+ assert mpl.is_interactive()
+ with plt.ioff():
+ assert not mpl.is_interactive()
+ assert mpl.is_interactive()
+ assert not mpl.is_interactive()
+
+ # redundant contexts
+ with plt.ion():
+ with plt.ion():
+ assert mpl.is_interactive()
+ assert not mpl.is_interactive()
+
+ with plt.ioff():
+ plt.ion()
+ assert not mpl.is_interactive()
+
+
+def test_close():
+ try:
+ plt.close(1.1)
+ except TypeError as e:
+ assert str(e) == "close() argument must be a Figure, an int, " \
+ "a string, or None, not "
+
+
+def test_subplot_reuse():
+ ax1 = plt.subplot(121)
+ assert ax1 is plt.gca()
+ ax2 = plt.subplot(122)
+ assert ax2 is plt.gca()
+ ax3 = plt.subplot(121)
+ assert ax1 is plt.gca()
+ assert ax1 is ax3
+
+
+def test_axes_kwargs():
+ # plt.axes() always creates new axes, even if axes kwargs differ.
+ plt.figure()
+ ax = plt.axes()
+ ax1 = plt.axes()
+ assert ax is not None
+ assert ax1 is not ax
+ plt.close()
+
+ plt.figure()
+ ax = plt.axes(projection='polar')
+ ax1 = plt.axes(projection='polar')
+ assert ax is not None
+ assert ax1 is not ax
+ plt.close()
+
+ plt.figure()
+ ax = plt.axes(projection='polar')
+ ax1 = plt.axes()
+ assert ax is not None
+ assert ax1.name == 'rectilinear'
+ assert ax1 is not ax
+ plt.close()
+
+
+def test_subplot_replace_projection():
+ # plt.subplot() searches for axes with the same subplot spec, and if one
+ # exists, and the kwargs match returns it, create a new one if they do not
+ fig = plt.figure()
+ ax = plt.subplot(1, 2, 1)
+ ax1 = plt.subplot(1, 2, 1)
+ ax2 = plt.subplot(1, 2, 2)
+ ax3 = plt.subplot(1, 2, 1, projection='polar')
+ ax4 = plt.subplot(1, 2, 1, projection='polar')
+ assert ax is not None
+ assert ax1 is ax
+ assert ax2 is not ax
+ assert ax3 is not ax
+ assert ax3 is ax4
+
+ assert ax in fig.axes
+ assert ax2 in fig.axes
+ assert ax3 in fig.axes
+
+ assert ax.name == 'rectilinear'
+ assert ax2.name == 'rectilinear'
+ assert ax3.name == 'polar'
+
+
+def test_subplot_kwarg_collision():
+ ax1 = plt.subplot(projection='polar', theta_offset=0)
+ ax2 = plt.subplot(projection='polar', theta_offset=0)
+ assert ax1 is ax2
+ ax1.remove()
+ ax3 = plt.subplot(projection='polar', theta_offset=1)
+ assert ax1 is not ax3
+ assert ax1 not in plt.gcf().axes
+
+
+def test_gca():
+ # plt.gca() returns an existing axes, unless there were no axes.
+ plt.figure()
+ ax = plt.gca()
+ ax1 = plt.gca()
+ assert ax is not None
+ assert ax1 is ax
+ plt.close()
+
+
+def test_subplot_projection_reuse():
+ # create an Axes
+ ax1 = plt.subplot(111)
+ # check that it is current
+ assert ax1 is plt.gca()
+ # make sure we get it back if we ask again
+ assert ax1 is plt.subplot(111)
+ # remove it
+ ax1.remove()
+ # create a polar plot
+ ax2 = plt.subplot(111, projection='polar')
+ assert ax2 is plt.gca()
+ # this should have deleted the first axes
+ assert ax1 not in plt.gcf().axes
+ # assert we get it back if no extra parameters passed
+ assert ax2 is plt.subplot(111)
+ ax2.remove()
+ # now check explicitly setting the projection to rectilinear
+ # makes a new axes
+ ax3 = plt.subplot(111, projection='rectilinear')
+ assert ax3 is plt.gca()
+ assert ax3 is not ax2
+ assert ax2 not in plt.gcf().axes
+
+
+def test_subplot_polar_normalization():
+ ax1 = plt.subplot(111, projection='polar')
+ ax2 = plt.subplot(111, polar=True)
+ ax3 = plt.subplot(111, polar=True, projection='polar')
+ assert ax1 is ax2
+ assert ax1 is ax3
+
+ with pytest.raises(ValueError,
+ match="polar=True, yet projection='3d'"):
+ ax2 = plt.subplot(111, polar=True, projection='3d')
+
+
+def test_subplot_change_projection():
+ created_axes = set()
+ ax = plt.subplot()
+ created_axes.add(ax)
+ projections = ('aitoff', 'hammer', 'lambert', 'mollweide',
+ 'polar', 'rectilinear', '3d')
+ for proj in projections:
+ ax.remove()
+ ax = plt.subplot(projection=proj)
+ assert ax is plt.subplot()
+ assert ax.name == proj
+ created_axes.add(ax)
+ # Check that each call created a new Axes.
+ assert len(created_axes) == 1 + len(projections)
+
+
+def test_polar_second_call():
+ # the first call creates the axes with polar projection
+ ln1, = plt.polar(0., 1., 'ro')
+ assert isinstance(ln1, mpl.lines.Line2D)
+ # the second call should reuse the existing axes
+ ln2, = plt.polar(1.57, .5, 'bo')
+ assert isinstance(ln2, mpl.lines.Line2D)
+ assert ln1.axes is ln2.axes
+
+
+def test_fallback_position():
+ # check that position kwarg works if rect not supplied
+ axref = plt.axes([0.2, 0.2, 0.5, 0.5])
+ axtest = plt.axes(position=[0.2, 0.2, 0.5, 0.5])
+ np.testing.assert_allclose(axtest.bbox.get_points(),
+ axref.bbox.get_points())
+
+ # check that position kwarg ignored if rect is supplied
+ axref = plt.axes([0.2, 0.2, 0.5, 0.5])
+ axtest = plt.axes([0.2, 0.2, 0.5, 0.5], position=[0.1, 0.1, 0.8, 0.8])
+ np.testing.assert_allclose(axtest.bbox.get_points(),
+ axref.bbox.get_points())
+
+
+def test_set_current_figure_via_subfigure():
+ fig1 = plt.figure()
+ subfigs = fig1.subfigures(2)
+
+ plt.figure()
+ assert plt.gcf() != fig1
+
+ current = plt.figure(subfigs[1])
+ assert plt.gcf() == fig1
+ assert current == fig1
+
+
+def test_set_current_axes_on_subfigure():
+ fig = plt.figure()
+ subfigs = fig.subfigures(2)
+
+ ax = subfigs[0].subplots(1, squeeze=True)
+ subfigs[1].subplots(1, squeeze=True)
+
+ assert plt.gca() != ax
+ plt.sca(ax)
+ assert plt.gca() == ax
+
+
+def test_pylab_integration():
+ IPython = pytest.importorskip("IPython")
+ mpl.testing.subprocess_run_helper(
+ IPython.start_ipython,
+ "--pylab",
+ "-c",
+ ";".join((
+ "import matplotlib.pyplot as plt",
+ "assert plt._REPL_DISPLAYHOOK == plt._ReplDisplayHook.IPYTHON",
+ )),
+ timeout=60,
+ )
+
+
+def test_doc_pyplot_summary():
+ """Test that pyplot_summary lists all the plot functions."""
+ pyplot_docs = Path(__file__).parent / '../../../doc/api/pyplot_summary.rst'
+ if not pyplot_docs.exists():
+ pytest.skip("Documentation sources not available")
+
+ def extract_documented_functions(lines):
+ """
+ Return a list of all the functions that are mentioned in the
+ autosummary blocks contained in *lines*.
+
+ An autosummary block looks like this::
+
+ .. autosummary::
+ :toctree: _as_gen
+ :template: autosummary.rst
+ :nosignatures:
+
+ plot
+ plot_date
+
+ """
+ functions = []
+ in_autosummary = False
+ for line in lines:
+ if not in_autosummary:
+ if line.startswith(".. autosummary::"):
+ in_autosummary = True
+ else:
+ if not line or line.startswith(" :"):
+ # empty line or autosummary parameter
+ continue
+ if not line[0].isspace():
+ # no more indentation: end of autosummary block
+ in_autosummary = False
+ continue
+ functions.append(line.strip())
+ return functions
+
+ lines = pyplot_docs.read_text().split("\n")
+ doc_functions = set(extract_documented_functions(lines))
+ plot_commands = set(plt._get_pyplot_commands())
+ missing = plot_commands.difference(doc_functions)
+ if missing:
+ raise AssertionError(
+ f"The following pyplot functions are not listed in the "
+ f"documentation. Please add them to doc/api/pyplot_summary.rst: "
+ f"{missing!r}")
+ extra = doc_functions.difference(plot_commands)
+ if extra:
+ raise AssertionError(
+ f"The following functions are listed in the pyplot documentation, "
+ f"but they do not exist in pyplot. "
+ f"Please remove them from doc/api/pyplot_summary.rst: {extra!r}")
+
+
+def test_minor_ticks():
+ plt.figure()
+ plt.plot(np.arange(1, 10))
+ tick_pos, tick_labels = plt.xticks(minor=True)
+ assert np.all(tick_labels == np.array([], dtype=np.float64))
+ assert tick_labels == []
+
+ plt.yticks(ticks=[3.5, 6.5], labels=["a", "b"], minor=True)
+ ax = plt.gca()
+ tick_pos = ax.get_yticks(minor=True)
+ tick_labels = ax.get_yticklabels(minor=True)
+ assert np.all(tick_pos == np.array([3.5, 6.5]))
+ assert [l.get_text() for l in tick_labels] == ['a', 'b']
+
+
+def test_switch_backend_no_close():
+ plt.switch_backend('agg')
+ fig = plt.figure()
+ fig = plt.figure()
+ assert len(plt.get_fignums()) == 2
+ plt.switch_backend('agg')
+ assert len(plt.get_fignums()) == 2
+ plt.switch_backend('svg')
+ assert len(plt.get_fignums()) == 2
+
+
+def figure_hook_example(figure):
+ figure._test_was_here = True
+
+
+def test_figure_hook():
+
+ test_rc = {
+ 'figure.hooks': ['matplotlib.tests.test_pyplot:figure_hook_example']
+ }
+ with mpl.rc_context(test_rc):
+ fig = plt.figure()
+
+ assert fig._test_was_here
+
+
+def test_multiple_same_figure_calls():
+ fig = plt.figure(1, figsize=(1, 2))
+ with pytest.warns(UserWarning, match="Ignoring specified arguments in this call"):
+ fig2 = plt.figure(1, figsize=np.array([3, 4]))
+ with pytest.warns(UserWarning, match="Ignoring specified arguments in this call"):
+ plt.figure(fig, figsize=np.array([5, 6]))
+ assert fig is fig2
+ fig3 = plt.figure(1) # Checks for false warnings
+ assert fig is fig3
+
+
+def test_close_all_warning():
+ fig1 = plt.figure()
+
+ # Check that the warning is issued when 'all' is passed to plt.figure
+ with pytest.warns(UserWarning, match="closes all existing figures"):
+ fig2 = plt.figure("all")
+
+
+def test_matshow():
+ fig = plt.figure()
+ arr = [[0, 1], [1, 2]]
+
+ # Smoke test that matshow does not ask for a new figsize on the existing figure
+ plt.matshow(arr, fignum=fig.number)
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_sankey.py b/lib/python3.12/site-packages/matplotlib/tests/test_sankey.py
new file mode 100644
index 0000000000000000000000000000000000000000..cbb7f516a65c4a9bb745365ade4c0cdfe87fd43b
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_sankey.py
@@ -0,0 +1,105 @@
+import pytest
+from numpy.testing import assert_allclose, assert_array_equal
+
+from matplotlib.sankey import Sankey
+from matplotlib.testing.decorators import check_figures_equal
+
+
+def test_sankey():
+ # lets just create a sankey instance and check the code runs
+ sankey = Sankey()
+ sankey.add()
+
+
+def test_label():
+ s = Sankey(flows=[0.25], labels=['First'], orientations=[-1])
+ assert s.diagrams[0].texts[0].get_text() == 'First\n0.25'
+
+
+def test_format_using_callable():
+ # test using callable by slightly incrementing above label example
+
+ def show_three_decimal_places(value):
+ return f'{value:.3f}'
+
+ s = Sankey(flows=[0.25], labels=['First'], orientations=[-1],
+ format=show_three_decimal_places)
+
+ assert s.diagrams[0].texts[0].get_text() == 'First\n0.250'
+
+
+@pytest.mark.parametrize('kwargs, msg', (
+ ({'gap': -1}, "'gap' is negative"),
+ ({'gap': 1, 'radius': 2}, "'radius' is greater than 'gap'"),
+ ({'head_angle': -1}, "'head_angle' is negative"),
+ ({'tolerance': -1}, "'tolerance' is negative"),
+ ({'flows': [1, -1], 'orientations': [-1, 0, 1]},
+ r"The shapes of 'flows' \(2,\) and 'orientations'"),
+ ({'flows': [1, -1], 'labels': ['a', 'b', 'c']},
+ r"The shapes of 'flows' \(2,\) and 'labels'"),
+ ))
+def test_sankey_errors(kwargs, msg):
+ with pytest.raises(ValueError, match=msg):
+ Sankey(**kwargs)
+
+
+@pytest.mark.parametrize('kwargs, msg', (
+ ({'trunklength': -1}, "'trunklength' is negative"),
+ ({'flows': [0.2, 0.3], 'prior': 0}, "The scaled sum of the connected"),
+ ({'prior': -1}, "The index of the prior diagram is negative"),
+ ({'prior': 1}, "The index of the prior diagram is 1"),
+ ({'connect': (-1, 1), 'prior': 0}, "At least one of the connection"),
+ ({'connect': (2, 1), 'prior': 0}, "The connection index to the source"),
+ ({'connect': (1, 3), 'prior': 0}, "The connection index to this dia"),
+ ({'connect': (1, 1), 'prior': 0, 'flows': [-0.2, 0.2],
+ 'orientations': [2]}, "The value of orientations"),
+ ({'connect': (1, 1), 'prior': 0, 'flows': [-0.2, 0.2],
+ 'pathlengths': [2]}, "The lengths of 'flows'"),
+ ))
+def test_sankey_add_errors(kwargs, msg):
+ sankey = Sankey()
+ with pytest.raises(ValueError, match=msg):
+ sankey.add(flows=[0.2, -0.2])
+ sankey.add(**kwargs)
+
+
+def test_sankey2():
+ s = Sankey(flows=[0.25, -0.25, 0.5, -0.5], labels=['Foo'],
+ orientations=[-1], unit='Bar')
+ sf = s.finish()
+ assert_array_equal(sf[0].flows, [0.25, -0.25, 0.5, -0.5])
+ assert sf[0].angles == [1, 3, 1, 3]
+ assert all([text.get_text()[0:3] == 'Foo' for text in sf[0].texts])
+ assert all([text.get_text()[-3:] == 'Bar' for text in sf[0].texts])
+ assert sf[0].text.get_text() == ''
+ assert_allclose(sf[0].tips,
+ [(-1.375, -0.52011255),
+ (1.375, -0.75506044),
+ (-0.75, -0.41522509),
+ (0.75, -0.8599479)])
+
+ s = Sankey(flows=[0.25, -0.25, 0, 0.5, -0.5], labels=['Foo'],
+ orientations=[-1], unit='Bar')
+ sf = s.finish()
+ assert_array_equal(sf[0].flows, [0.25, -0.25, 0, 0.5, -0.5])
+ assert sf[0].angles == [1, 3, None, 1, 3]
+ assert_allclose(sf[0].tips,
+ [(-1.375, -0.52011255),
+ (1.375, -0.75506044),
+ (0, 0),
+ (-0.75, -0.41522509),
+ (0.75, -0.8599479)])
+
+
+@check_figures_equal(extensions=['png'])
+def test_sankey3(fig_test, fig_ref):
+ ax_test = fig_test.gca()
+ s_test = Sankey(ax=ax_test, flows=[0.25, -0.25, -0.25, 0.25, 0.5, -0.5],
+ orientations=[1, -1, 1, -1, 0, 0])
+ s_test.finish()
+
+ ax_ref = fig_ref.gca()
+ s_ref = Sankey(ax=ax_ref)
+ s_ref.add(flows=[0.25, -0.25, -0.25, 0.25, 0.5, -0.5],
+ orientations=[1, -1, 1, -1, 0, 0])
+ s_ref.finish()
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_scale.py b/lib/python3.12/site-packages/matplotlib/tests/test_scale.py
new file mode 100644
index 0000000000000000000000000000000000000000..be46d9593931cb4d65c83fbc7c3a21723cadaa6b
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_scale.py
@@ -0,0 +1,295 @@
+import copy
+
+import matplotlib.pyplot as plt
+from matplotlib.scale import (
+ AsinhScale, AsinhTransform,
+ LogTransform, InvertedLogTransform,
+ SymmetricalLogTransform)
+import matplotlib.scale as mscale
+from matplotlib.ticker import AsinhLocator, LogFormatterSciNotation
+from matplotlib.testing.decorators import check_figures_equal, image_comparison
+
+import numpy as np
+from numpy.testing import assert_allclose
+import io
+import pytest
+
+
+@check_figures_equal(extensions=['png'])
+def test_log_scales(fig_test, fig_ref):
+ ax_test = fig_test.add_subplot(122, yscale='log', xscale='symlog')
+ ax_test.axvline(24.1)
+ ax_test.axhline(24.1)
+ xlim = ax_test.get_xlim()
+ ylim = ax_test.get_ylim()
+ ax_ref = fig_ref.add_subplot(122, yscale='log', xscale='symlog')
+ ax_ref.set(xlim=xlim, ylim=ylim)
+ ax_ref.plot([24.1, 24.1], ylim, 'b')
+ ax_ref.plot(xlim, [24.1, 24.1], 'b')
+
+
+def test_symlog_mask_nan():
+ # Use a transform round-trip to verify that the forward and inverse
+ # transforms work, and that they respect nans and/or masking.
+ slt = SymmetricalLogTransform(10, 2, 1)
+ slti = slt.inverted()
+
+ x = np.arange(-1.5, 5, 0.5)
+ out = slti.transform_non_affine(slt.transform_non_affine(x))
+ assert_allclose(out, x)
+ assert type(out) is type(x)
+
+ x[4] = np.nan
+ out = slti.transform_non_affine(slt.transform_non_affine(x))
+ assert_allclose(out, x)
+ assert type(out) is type(x)
+
+ x = np.ma.array(x)
+ out = slti.transform_non_affine(slt.transform_non_affine(x))
+ assert_allclose(out, x)
+ assert type(out) is type(x)
+
+ x[3] = np.ma.masked
+ out = slti.transform_non_affine(slt.transform_non_affine(x))
+ assert_allclose(out, x)
+ assert type(out) is type(x)
+
+
+@image_comparison(['logit_scales.png'], remove_text=True)
+def test_logit_scales():
+ fig, ax = plt.subplots()
+
+ # Typical extinction curve for logit
+ x = np.array([0.001, 0.003, 0.01, 0.03, 0.1, 0.2, 0.3, 0.4, 0.5,
+ 0.6, 0.7, 0.8, 0.9, 0.97, 0.99, 0.997, 0.999])
+ y = 1.0 / x
+
+ ax.plot(x, y)
+ ax.set_xscale('logit')
+ ax.grid(True)
+ bbox = ax.get_tightbbox(fig.canvas.get_renderer())
+ assert np.isfinite(bbox.x0)
+ assert np.isfinite(bbox.y0)
+
+
+def test_log_scatter():
+ """Issue #1799"""
+ fig, ax = plt.subplots(1)
+
+ x = np.arange(10)
+ y = np.arange(10) - 1
+
+ ax.scatter(x, y)
+
+ buf = io.BytesIO()
+ fig.savefig(buf, format='pdf')
+
+ buf = io.BytesIO()
+ fig.savefig(buf, format='eps')
+
+ buf = io.BytesIO()
+ fig.savefig(buf, format='svg')
+
+
+def test_logscale_subs():
+ fig, ax = plt.subplots()
+ ax.set_yscale('log', subs=np.array([2, 3, 4]))
+ # force draw
+ fig.canvas.draw()
+
+
+@image_comparison(['logscale_mask.png'], remove_text=True)
+def test_logscale_mask():
+ # Check that zero values are masked correctly on log scales.
+ # See github issue 8045
+ xs = np.linspace(0, 50, 1001)
+
+ fig, ax = plt.subplots()
+ ax.plot(np.exp(-xs**2))
+ fig.canvas.draw()
+ ax.set(yscale="log")
+
+
+def test_extra_kwargs_raise():
+ fig, ax = plt.subplots()
+
+ for scale in ['linear', 'log', 'symlog']:
+ with pytest.raises(TypeError):
+ ax.set_yscale(scale, foo='mask')
+
+
+def test_logscale_invert_transform():
+ fig, ax = plt.subplots()
+ ax.set_yscale('log')
+ # get transformation from data to axes
+ tform = (ax.transAxes + ax.transData.inverted()).inverted()
+
+ # direct test of log transform inversion
+ inverted_transform = LogTransform(base=2).inverted()
+ assert isinstance(inverted_transform, InvertedLogTransform)
+ assert inverted_transform.base == 2
+
+
+def test_logscale_transform_repr():
+ fig, ax = plt.subplots()
+ ax.set_yscale('log')
+ repr(ax.transData)
+ repr(LogTransform(10, nonpositive='clip'))
+
+
+@image_comparison(['logscale_nonpos_values.png'],
+ remove_text=True, tol=0.02, style='mpl20')
+def test_logscale_nonpos_values():
+ np.random.seed(19680801)
+ xs = np.random.normal(size=int(1e3))
+ fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2)
+ ax1.hist(xs, range=(-5, 5), bins=10)
+ ax1.set_yscale('log')
+ ax2.hist(xs, range=(-5, 5), bins=10)
+ ax2.set_yscale('log', nonpositive='mask')
+
+ xdata = np.arange(0, 10, 0.01)
+ ydata = np.exp(-xdata)
+ edata = 0.2*(10-xdata)*np.cos(5*xdata)*np.exp(-xdata)
+
+ ax3.fill_between(xdata, ydata - edata, ydata + edata)
+ ax3.set_yscale('log')
+
+ x = np.logspace(-1, 1)
+ y = x ** 3
+ yerr = x**2
+ ax4.errorbar(x, y, yerr=yerr)
+
+ ax4.set_yscale('log')
+ ax4.set_xscale('log')
+
+
+def test_invalid_log_lims():
+ # Check that invalid log scale limits are ignored
+ fig, ax = plt.subplots()
+ ax.scatter(range(0, 4), range(0, 4))
+
+ ax.set_xscale('log')
+ original_xlim = ax.get_xlim()
+ with pytest.warns(UserWarning):
+ ax.set_xlim(left=0)
+ assert ax.get_xlim() == original_xlim
+ with pytest.warns(UserWarning):
+ ax.set_xlim(right=-1)
+ assert ax.get_xlim() == original_xlim
+
+ ax.set_yscale('log')
+ original_ylim = ax.get_ylim()
+ with pytest.warns(UserWarning):
+ ax.set_ylim(bottom=0)
+ assert ax.get_ylim() == original_ylim
+ with pytest.warns(UserWarning):
+ ax.set_ylim(top=-1)
+ assert ax.get_ylim() == original_ylim
+
+
+@image_comparison(['function_scales.png'], remove_text=True, style='mpl20')
+def test_function_scale():
+ def inverse(x):
+ return x**2
+
+ def forward(x):
+ return x**(1/2)
+
+ fig, ax = plt.subplots()
+
+ x = np.arange(1, 1000)
+
+ ax.plot(x, x)
+ ax.set_xscale('function', functions=(forward, inverse))
+ ax.set_xlim(1, 1000)
+
+
+def test_pass_scale():
+ # test passing a scale object works...
+ fig, ax = plt.subplots()
+ scale = mscale.LogScale(axis=None)
+ ax.set_xscale(scale)
+ scale = mscale.LogScale(axis=None)
+ ax.set_yscale(scale)
+ assert ax.xaxis.get_scale() == 'log'
+ assert ax.yaxis.get_scale() == 'log'
+
+
+def test_scale_deepcopy():
+ sc = mscale.LogScale(axis='x', base=10)
+ sc2 = copy.deepcopy(sc)
+ assert str(sc.get_transform()) == str(sc2.get_transform())
+ assert sc._transform is not sc2._transform
+
+
+class TestAsinhScale:
+ def test_transforms(self):
+ a0 = 17.0
+ a = np.linspace(-50, 50, 100)
+
+ forward = AsinhTransform(a0)
+ inverse = forward.inverted()
+ invinv = inverse.inverted()
+
+ a_forward = forward.transform_non_affine(a)
+ a_inverted = inverse.transform_non_affine(a_forward)
+ assert_allclose(a_inverted, a)
+
+ a_invinv = invinv.transform_non_affine(a)
+ assert_allclose(a_invinv, a0 * np.arcsinh(a / a0))
+
+ def test_init(self):
+ fig, ax = plt.subplots()
+
+ s = AsinhScale(axis=None, linear_width=23.0)
+ assert s.linear_width == 23
+ assert s._base == 10
+ assert s._subs == (2, 5)
+
+ tx = s.get_transform()
+ assert isinstance(tx, AsinhTransform)
+ assert tx.linear_width == s.linear_width
+
+ def test_base_init(self):
+ fig, ax = plt.subplots()
+
+ s3 = AsinhScale(axis=None, base=3)
+ assert s3._base == 3
+ assert s3._subs == (2,)
+
+ s7 = AsinhScale(axis=None, base=7, subs=(2, 4))
+ assert s7._base == 7
+ assert s7._subs == (2, 4)
+
+ def test_fmtloc(self):
+ class DummyAxis:
+ def __init__(self):
+ self.fields = {}
+ def set(self, **kwargs):
+ self.fields.update(**kwargs)
+ def set_major_formatter(self, f):
+ self.fields['major_formatter'] = f
+
+ ax0 = DummyAxis()
+ s0 = AsinhScale(axis=ax0, base=0)
+ s0.set_default_locators_and_formatters(ax0)
+ assert isinstance(ax0.fields['major_locator'], AsinhLocator)
+ assert isinstance(ax0.fields['major_formatter'], str)
+
+ ax5 = DummyAxis()
+ s7 = AsinhScale(axis=ax5, base=5)
+ s7.set_default_locators_and_formatters(ax5)
+ assert isinstance(ax5.fields['major_locator'], AsinhLocator)
+ assert isinstance(ax5.fields['major_formatter'],
+ LogFormatterSciNotation)
+
+ def test_bad_scale(self):
+ fig, ax = plt.subplots()
+
+ with pytest.raises(ValueError):
+ AsinhScale(axis=None, linear_width=0)
+ with pytest.raises(ValueError):
+ AsinhScale(axis=None, linear_width=-1)
+ s0 = AsinhScale(axis=None, )
+ s1 = AsinhScale(axis=None, linear_width=3.0)
diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_simplification.py b/lib/python3.12/site-packages/matplotlib/tests/test_simplification.py
new file mode 100644
index 0000000000000000000000000000000000000000..58b4d7a9b24fea5ef8c54f51c6f7095ce19e33a3
--- /dev/null
+++ b/lib/python3.12/site-packages/matplotlib/tests/test_simplification.py
@@ -0,0 +1,571 @@
+import base64
+import io
+import platform
+
+import numpy as np
+from numpy.testing import assert_array_almost_equal, assert_array_equal
+
+import pytest
+
+from matplotlib.testing.decorators import (
+ check_figures_equal, image_comparison, remove_ticks_and_titles)
+import matplotlib.pyplot as plt
+
+from matplotlib import patches, transforms
+from matplotlib.path import Path
+
+
+# NOTE: All of these tests assume that path.simplify is set to True
+# (the default)
+
+@image_comparison(['clipping'], remove_text=True)
+def test_clipping():
+ t = np.arange(0.0, 2.0, 0.01)
+ s = np.sin(2*np.pi*t)
+
+ fig, ax = plt.subplots()
+ ax.plot(t, s, linewidth=1.0)
+ ax.set_ylim((-0.20, -0.28))
+
+
+@image_comparison(['overflow'], remove_text=True,
+ tol=0 if platform.machine() == 'x86_64' else 0.007)
+def test_overflow():
+ x = np.array([1.0, 2.0, 3.0, 2.0e5])
+ y = np.arange(len(x))
+
+ fig, ax = plt.subplots()
+ ax.plot(x, y)
+ ax.set_xlim(2, 6)
+
+
+@image_comparison(['clipping_diamond'], remove_text=True)
+def test_diamond():
+ x = np.array([0.0, 1.0, 0.0, -1.0, 0.0])
+ y = np.array([1.0, 0.0, -1.0, 0.0, 1.0])
+
+ fig, ax = plt.subplots()
+ ax.plot(x, y)
+ ax.set_xlim(-0.6, 0.6)
+ ax.set_ylim(-0.6, 0.6)
+
+
+def test_clipping_out_of_bounds():
+ # Should work on a Path *without* codes.
+ path = Path([(0, 0), (1, 2), (2, 1)])
+ simplified = path.cleaned(clip=(10, 10, 20, 20))
+ assert_array_equal(simplified.vertices, [(0, 0)])
+ assert simplified.codes == [Path.STOP]
+
+ # Should work on a Path *with* codes, and no curves.
+ path = Path([(0, 0), (1, 2), (2, 1)],
+ [Path.MOVETO, Path.LINETO, Path.LINETO])
+ simplified = path.cleaned(clip=(10, 10, 20, 20))
+ assert_array_equal(simplified.vertices, [(0, 0)])
+ assert simplified.codes == [Path.STOP]
+
+ # A Path with curves does not do any clipping yet.
+ path = Path([(0, 0), (1, 2), (2, 3)],
+ [Path.MOVETO, Path.CURVE3, Path.CURVE3])
+ simplified = path.cleaned()
+ simplified_clipped = path.cleaned(clip=(10, 10, 20, 20))
+ assert_array_equal(simplified.vertices, simplified_clipped.vertices)
+ assert_array_equal(simplified.codes, simplified_clipped.codes)
+
+
+def test_noise():
+ np.random.seed(0)
+ x = np.random.uniform(size=50000) * 50
+
+ fig, ax = plt.subplots()
+ p1 = ax.plot(x, solid_joinstyle='round', linewidth=2.0)
+
+ # Ensure that the path's transform takes the new axes limits into account.
+ fig.canvas.draw()
+ path = p1[0].get_path()
+ transform = p1[0].get_transform()
+ path = transform.transform_path(path)
+ simplified = path.cleaned(simplify=True)
+
+ assert simplified.vertices.size == 25512
+
+
+def test_antiparallel_simplification():
+ def _get_simplified(x, y):
+ fig, ax = plt.subplots()
+ p1 = ax.plot(x, y)
+
+ path = p1[0].get_path()
+ transform = p1[0].get_transform()
+ path = transform.transform_path(path)
+ simplified = path.cleaned(simplify=True)
+ simplified = transform.inverted().transform_path(simplified)
+
+ return simplified
+
+ # test ending on a maximum
+ x = [0, 0, 0, 0, 0, 1]
+ y = [.5, 1, -1, 1, 2, .5]
+
+ simplified = _get_simplified(x, y)
+
+ assert_array_almost_equal([[0., 0.5],
+ [0., -1.],
+ [0., 2.],
+ [1., 0.5]],
+ simplified.vertices[:-2, :])
+
+ # test ending on a minimum
+ x = [0, 0, 0, 0, 0, 1]
+ y = [.5, 1, -1, 1, -2, .5]
+
+ simplified = _get_simplified(x, y)
+
+ assert_array_almost_equal([[0., 0.5],
+ [0., 1.],
+ [0., -2.],
+ [1., 0.5]],
+ simplified.vertices[:-2, :])
+
+ # test ending in between
+ x = [0, 0, 0, 0, 0, 1]
+ y = [.5, 1, -1, 1, 0, .5]
+
+ simplified = _get_simplified(x, y)
+
+ assert_array_almost_equal([[0., 0.5],
+ [0., 1.],
+ [0., -1.],
+ [0., 0.],
+ [1., 0.5]],
+ simplified.vertices[:-2, :])
+
+ # test no anti-parallel ending at max
+ x = [0, 0, 0, 0, 0, 1]
+ y = [.5, 1, 2, 1, 3, .5]
+
+ simplified = _get_simplified(x, y)
+
+ assert_array_almost_equal([[0., 0.5],
+ [0., 3.],
+ [1., 0.5]],
+ simplified.vertices[:-2, :])
+
+ # test no anti-parallel ending in middle
+ x = [0, 0, 0, 0, 0, 1]
+ y = [.5, 1, 2, 1, 1, .5]
+
+ simplified = _get_simplified(x, y)
+
+ assert_array_almost_equal([[0., 0.5],
+ [0., 2.],
+ [0., 1.],
+ [1., 0.5]],
+ simplified.vertices[:-2, :])
+
+
+# Only consider angles in 0 <= angle <= pi/2, otherwise
+# using min/max will get the expected results out of order:
+# min/max for simplification code depends on original vector,
+# and if angle is outside above range then simplification
+# min/max will be opposite from actual min/max.
+@pytest.mark.parametrize('angle', [0, np.pi/4, np.pi/3, np.pi/2])
+@pytest.mark.parametrize('offset', [0, .5])
+def test_angled_antiparallel(angle, offset):
+ scale = 5
+ np.random.seed(19680801)
+ # get 15 random offsets
+ # TODO: guarantee offset > 0 results in some offsets < 0
+ vert_offsets = (np.random.rand(15) - offset) * scale
+ # always start at 0 so rotation makes sense
+ vert_offsets[0] = 0
+ # always take the first step the same direction
+ vert_offsets[1] = 1
+ # compute points along a diagonal line
+ x = np.sin(angle) * vert_offsets
+ y = np.cos(angle) * vert_offsets
+
+ # will check these later
+ x_max = x[1:].max()
+ x_min = x[1:].min()
+
+ y_max = y[1:].max()
+ y_min = y[1:].min()
+
+ if offset > 0:
+ p_expected = Path([[0, 0],
+ [x_max, y_max],
+ [x_min, y_min],
+ [x[-1], y[-1]],
+ [0, 0]],
+ codes=[1, 2, 2, 2, 0])
+
+ else:
+ p_expected = Path([[0, 0],
+ [x_max, y_max],
+ [x[-1], y[-1]],
+ [0, 0]],
+ codes=[1, 2, 2, 0])
+
+ p = Path(np.vstack([x, y]).T)
+ p2 = p.cleaned(simplify=True)
+
+ assert_array_almost_equal(p_expected.vertices,
+ p2.vertices)
+ assert_array_equal(p_expected.codes, p2.codes)
+
+
+def test_sine_plus_noise():
+ np.random.seed(0)
+ x = (np.sin(np.linspace(0, np.pi * 2.0, 50000)) +
+ np.random.uniform(size=50000) * 0.01)
+
+ fig, ax = plt.subplots()
+ p1 = ax.plot(x, solid_joinstyle='round', linewidth=2.0)
+
+ # Ensure that the path's transform takes the new axes limits into account.
+ fig.canvas.draw()
+ path = p1[0].get_path()
+ transform = p1[0].get_transform()
+ path = transform.transform_path(path)
+ simplified = path.cleaned(simplify=True)
+
+ assert simplified.vertices.size == 25240
+
+
+@image_comparison(['simplify_curve'], remove_text=True, tol=0.017)
+def test_simplify_curve():
+ pp1 = patches.PathPatch(
+ Path([(0, 0), (1, 0), (1, 1), (np.nan, 1), (0, 0), (2, 0), (2, 2),
+ (0, 0)],
+ [Path.MOVETO, Path.CURVE3, Path.CURVE3, Path.CURVE3, Path.CURVE3,
+ Path.CURVE3, Path.CURVE3, Path.CLOSEPOLY]),
+ fc="none")
+
+ fig, ax = plt.subplots()
+ ax.add_patch(pp1)
+ ax.set_xlim((0, 2))
+ ax.set_ylim((0, 2))
+
+
+@check_figures_equal()
+def test_closed_path_nan_removal(fig_test, fig_ref):
+ ax_test = fig_test.subplots(2, 2).flatten()
+ ax_ref = fig_ref.subplots(2, 2).flatten()
+
+ # NaN on the first point also removes the last point, because it's closed.
+ path = Path(
+ [[-3, np.nan], [3, -3], [3, 3], [-3, 3], [-3, -3]],
+ [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
+ ax_test[0].add_patch(patches.PathPatch(path, facecolor='none'))
+ path = Path(
+ [[-3, np.nan], [3, -3], [3, 3], [-3, 3], [-3, np.nan]],
+ [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO])
+ ax_ref[0].add_patch(patches.PathPatch(path, facecolor='none'))
+
+ # NaN on second-last point should not re-close.
+ path = Path(
+ [[-2, -2], [2, -2], [2, 2], [-2, np.nan], [-2, -2]],
+ [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
+ ax_test[0].add_patch(patches.PathPatch(path, facecolor='none'))
+ path = Path(
+ [[-2, -2], [2, -2], [2, 2], [-2, np.nan], [-2, -2]],
+ [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO])
+ ax_ref[0].add_patch(patches.PathPatch(path, facecolor='none'))
+
+ # Test multiple loops in a single path (with same paths as above).
+ path = Path(
+ [[-3, np.nan], [3, -3], [3, 3], [-3, 3], [-3, -3],
+ [-2, -2], [2, -2], [2, 2], [-2, np.nan], [-2, -2]],
+ [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY,
+ Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
+ ax_test[1].add_patch(patches.PathPatch(path, facecolor='none'))
+ path = Path(
+ [[-3, np.nan], [3, -3], [3, 3], [-3, 3], [-3, np.nan],
+ [-2, -2], [2, -2], [2, 2], [-2, np.nan], [-2, -2]],
+ [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
+ Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO])
+ ax_ref[1].add_patch(patches.PathPatch(path, facecolor='none'))
+
+ # NaN in first point of CURVE3 should not re-close, and hide entire curve.
+ path = Path(
+ [[-1, -1], [1, -1], [1, np.nan], [0, 1], [-1, 1], [-1, -1]],
+ [Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.LINETO,
+ Path.CLOSEPOLY])
+ ax_test[2].add_patch(patches.PathPatch(path, facecolor='none'))
+ path = Path(
+ [[-1, -1], [1, -1], [1, np.nan], [0, 1], [-1, 1], [-1, -1]],
+ [Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.LINETO,
+ Path.CLOSEPOLY])
+ ax_ref[2].add_patch(patches.PathPatch(path, facecolor='none'))
+
+ # NaN in second point of CURVE3 should not re-close, and hide entire curve
+ # plus next line segment.
+ path = Path(
+ [[-3, -3], [3, -3], [3, 0], [0, np.nan], [-3, 3], [-3, -3]],
+ [Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.LINETO,
+ Path.LINETO])
+ ax_test[2].add_patch(patches.PathPatch(path, facecolor='none'))
+ path = Path(
+ [[-3, -3], [3, -3], [3, 0], [0, np.nan], [-3, 3], [-3, -3]],
+ [Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.LINETO,
+ Path.LINETO])
+ ax_ref[2].add_patch(patches.PathPatch(path, facecolor='none'))
+
+ # NaN in first point of CURVE4 should not re-close, and hide entire curve.
+ path = Path(
+ [[-1, -1], [1, -1], [1, np.nan], [0, 0], [0, 1], [-1, 1], [-1, -1]],
+ [Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4,
+ Path.LINETO, Path.CLOSEPOLY])
+ ax_test[3].add_patch(patches.PathPatch(path, facecolor='none'))
+ path = Path(
+ [[-1, -1], [1, -1], [1, np.nan], [0, 0], [0, 1], [-1, 1], [-1, -1]],
+ [Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4,
+ Path.LINETO, Path.CLOSEPOLY])
+ ax_ref[3].add_patch(patches.PathPatch(path, facecolor='none'))
+
+ # NaN in second point of CURVE4 should not re-close, and hide entire curve.
+ path = Path(
+ [[-2, -2], [2, -2], [2, 0], [0, np.nan], [0, 2], [-2, 2], [-2, -2]],
+ [Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4,
+ Path.LINETO, Path.LINETO])
+ ax_test[3].add_patch(patches.PathPatch(path, facecolor='none'))
+ path = Path(
+ [[-2, -2], [2, -2], [2, 0], [0, np.nan], [0, 2], [-2, 2], [-2, -2]],
+ [Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4,
+ Path.LINETO, Path.LINETO])
+ ax_ref[3].add_patch(patches.PathPatch(path, facecolor='none'))
+
+ # NaN in third point of CURVE4 should not re-close, and hide entire curve
+ # plus next line segment.
+ path = Path(
+ [[-3, -3], [3, -3], [3, 0], [0, 0], [0, np.nan], [-3, 3], [-3, -3]],
+ [Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4,
+ Path.LINETO, Path.LINETO])
+ ax_test[3].add_patch(patches.PathPatch(path, facecolor='none'))
+ path = Path(
+ [[-3, -3], [3, -3], [3, 0], [0, 0], [0, np.nan], [-3, 3], [-3, -3]],
+ [Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4,
+ Path.LINETO, Path.LINETO])
+ ax_ref[3].add_patch(patches.PathPatch(path, facecolor='none'))
+
+ # Keep everything clean.
+ for ax in [*ax_test.flat, *ax_ref.flat]:
+ ax.set(xlim=(-3.5, 3.5), ylim=(-3.5, 3.5))
+ remove_ticks_and_titles(fig_test)
+ remove_ticks_and_titles(fig_ref)
+
+
+@check_figures_equal()
+def test_closed_path_clipping(fig_test, fig_ref):
+ vertices = []
+ for roll in range(8):
+ offset = 0.1 * roll + 0.1
+
+ # A U-like pattern.
+ pattern = [
+ [-0.5, 1.5], [-0.5, -0.5], [1.5, -0.5], [1.5, 1.5], # Outer square
+ # With a notch in the top.
+ [1 - offset / 2, 1.5], [1 - offset / 2, offset],
+ [offset / 2, offset], [offset / 2, 1.5],
+ ]
+
+ # Place the initial/final point anywhere in/out of the clipping area.
+ pattern = np.roll(pattern, roll, axis=0)
+ pattern = np.concatenate((pattern, pattern[:1, :]))
+
+ vertices.append(pattern)
+
+ # Multiple subpaths are used here to ensure they aren't broken by closed
+ # loop clipping.
+ codes = np.full(len(vertices[0]), Path.LINETO)
+ codes[0] = Path.MOVETO
+ codes[-1] = Path.CLOSEPOLY
+ codes = np.tile(codes, len(vertices))
+ vertices = np.concatenate(vertices)
+
+ fig_test.set_size_inches((5, 5))
+ path = Path(vertices, codes)
+ fig_test.add_artist(patches.PathPatch(path, facecolor='none'))
+
+ # For reference, we draw the same thing, but unclosed by using a line to
+ # the last point only.
+ fig_ref.set_size_inches((5, 5))
+ codes = codes.copy()
+ codes[codes == Path.CLOSEPOLY] = Path.LINETO
+ path = Path(vertices, codes)
+ fig_ref.add_artist(patches.PathPatch(path, facecolor='none'))
+
+
+@image_comparison(['hatch_simplify'], remove_text=True)
+def test_hatch():
+ fig, ax = plt.subplots()
+ ax.add_patch(plt.Rectangle((0, 0), 1, 1, fill=False, hatch="/"))
+ ax.set_xlim((0.45, 0.55))
+ ax.set_ylim((0.45, 0.55))
+
+
+@image_comparison(['fft_peaks'], remove_text=True)
+def test_fft_peaks():
+ fig, ax = plt.subplots()
+ t = np.arange(65536)
+ p1 = ax.plot(abs(np.fft.fft(np.sin(2*np.pi*.01*t)*np.blackman(len(t)))))
+
+ # Ensure that the path's transform takes the new axes limits into account.
+ fig.canvas.draw()
+ path = p1[0].get_path()
+ transform = p1[0].get_transform()
+ path = transform.transform_path(path)
+ simplified = path.cleaned(simplify=True)
+
+ assert simplified.vertices.size == 36
+
+
+def test_start_with_moveto():
+ # Should be entirely clipped away to a single MOVETO
+ data = b"""
+ZwAAAAku+v9UAQAA+Tj6/z8CAADpQ/r/KAMAANlO+v8QBAAAyVn6//UEAAC6ZPr/2gUAAKpv+v+8
+BgAAm3r6/50HAACLhfr/ewgAAHyQ+v9ZCQAAbZv6/zQKAABepvr/DgsAAE+x+v/lCwAAQLz6/7wM
+AAAxx/r/kA0AACPS+v9jDgAAFN36/zQPAAAF6Pr/AxAAAPfy+v/QEAAA6f36/5wRAADbCPv/ZhIA
+AMwT+/8uEwAAvh77//UTAACwKfv/uRQAAKM0+/98FQAAlT/7/z0WAACHSvv//RYAAHlV+/+7FwAA
+bGD7/3cYAABea/v/MRkAAFF2+//pGQAARIH7/6AaAAA3jPv/VRsAACmX+/8JHAAAHKL7/7ocAAAP
+rfv/ah0AAAO4+/8YHgAA9sL7/8QeAADpzfv/bx8AANzY+/8YIAAA0OP7/78gAADD7vv/ZCEAALf5
++/8IIgAAqwT8/6kiAACeD/z/SiMAAJIa/P/oIwAAhiX8/4QkAAB6MPz/HyUAAG47/P+4JQAAYkb8
+/1AmAABWUfz/5SYAAEpc/P95JwAAPmf8/wsoAAAzcvz/nCgAACd9/P8qKQAAHIj8/7cpAAAQk/z/
+QyoAAAWe/P/MKgAA+aj8/1QrAADus/z/2isAAOO+/P9eLAAA2Mn8/+AsAADM1Pz/YS0AAMHf/P/g
+LQAAtur8/10uAACr9fz/2C4AAKEA/f9SLwAAlgv9/8ovAACLFv3/QDAAAIAh/f+1MAAAdSz9/ycx
+AABrN/3/mDEAAGBC/f8IMgAAVk39/3UyAABLWP3/4TIAAEFj/f9LMwAANm79/7MzAAAsef3/GjQA
+ACKE/f9+NAAAF4/9/+E0AAANmv3/QzUAAAOl/f+iNQAA+a/9/wA2AADvuv3/XDYAAOXF/f+2NgAA
+29D9/w83AADR2/3/ZjcAAMfm/f+7NwAAvfH9/w44AACz/P3/XzgAAKkH/v+vOAAAnxL+//04AACW
+Hf7/SjkAAIwo/v+UOQAAgjP+/905AAB5Pv7/JDoAAG9J/v9pOgAAZVT+/606AABcX/7/7zoAAFJq
+/v8vOwAASXX+/207AAA/gP7/qjsAADaL/v/lOwAALZb+/x48AAAjof7/VTwAABqs/v+LPAAAELf+
+/788AAAHwv7/8TwAAP7M/v8hPQAA9df+/1A9AADr4v7/fT0AAOLt/v+oPQAA2fj+/9E9AADQA///
++T0AAMYO//8fPgAAvRn//0M+AAC0JP//ZT4AAKsv//+GPgAAojr//6U+AACZRf//wj4AAJBQ///d
+PgAAh1v///c+AAB+Zv//Dz8AAHRx//8lPwAAa3z//zk/AABih///TD8AAFmS//9dPwAAUJ3//2w/
+AABHqP//ej8AAD6z//+FPwAANb7//48/AAAsyf//lz8AACPU//+ePwAAGt///6M/AAAR6v//pj8A
+AAj1//+nPwAA/////w=="""
+
+ verts = np.frombuffer(base64.decodebytes(data), dtype='