diff --git a/lib/python3.12/site-packages/matplotlib/_api/__init__.py b/lib/python3.12/site-packages/matplotlib/_api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..22b58b62ff8e7f83043c9d99cc6d0dcefb0a93d2 --- /dev/null +++ b/lib/python3.12/site-packages/matplotlib/_api/__init__.py @@ -0,0 +1,391 @@ +""" +Helper functions for managing the Matplotlib API. + +This documentation is only relevant for Matplotlib developers, not for users. + +.. warning:: + + This module and its submodules are for internal use only. Do not use them + in your own code. We may change the API at any time with no warning. + +""" + +import functools +import itertools +import pathlib +import re +import sys +import warnings + +from .deprecation import ( # noqa: F401 + deprecated, warn_deprecated, + rename_parameter, delete_parameter, make_keyword_only, + deprecate_method_override, deprecate_privatize_attribute, + suppress_matplotlib_deprecation_warning, + MatplotlibDeprecationWarning) + + +class classproperty: + """ + Like `property`, but also triggers on access via the class, and it is the + *class* that's passed as argument. + + Examples + -------- + :: + + class C: + @classproperty + def foo(cls): + return cls.__name__ + + assert C.foo == "C" + """ + + def __init__(self, fget, fset=None, fdel=None, doc=None): + self._fget = fget + if fset is not None or fdel is not None: + raise ValueError('classproperty only implements fget.') + self.fset = fset + self.fdel = fdel + # docs are ignored for now + self._doc = doc + + def __get__(self, instance, owner): + return self._fget(owner) + + @property + def fget(self): + return self._fget + + +# In the following check_foo() functions, the first parameter is positional-only to make +# e.g. `_api.check_isinstance([...], types=foo)` work. + +def check_isinstance(types, /, **kwargs): + """ + For each *key, value* pair in *kwargs*, check that *value* is an instance + of one of *types*; if not, raise an appropriate TypeError. + + As a special case, a ``None`` entry in *types* is treated as NoneType. + + Examples + -------- + >>> _api.check_isinstance((SomeClass, None), arg=arg) + """ + none_type = type(None) + types = ((types,) if isinstance(types, type) else + (none_type,) if types is None else + tuple(none_type if tp is None else tp for tp in types)) + + def type_name(tp): + return ("None" if tp is none_type + else tp.__qualname__ if tp.__module__ == "builtins" + else f"{tp.__module__}.{tp.__qualname__}") + + for k, v in kwargs.items(): + if not isinstance(v, types): + names = [*map(type_name, types)] + if "None" in names: # Move it to the end for better wording. + names.remove("None") + names.append("None") + raise TypeError( + "{!r} must be an instance of {}, not a {}".format( + k, + ", ".join(names[:-1]) + " or " + names[-1] + if len(names) > 1 else names[0], + type_name(type(v)))) + + +def check_in_list(values, /, *, _print_supported_values=True, **kwargs): + """ + For each *key, value* pair in *kwargs*, check that *value* is in *values*; + if not, raise an appropriate ValueError. + + Parameters + ---------- + values : iterable + Sequence of values to check on. + _print_supported_values : bool, default: True + Whether to print *values* when raising ValueError. + **kwargs : dict + *key, value* pairs as keyword arguments to find in *values*. + + Raises + ------ + ValueError + If any *value* in *kwargs* is not found in *values*. + + Examples + -------- + >>> _api.check_in_list(["foo", "bar"], arg=arg, other_arg=other_arg) + """ + if not kwargs: + raise TypeError("No argument to check!") + for key, val in kwargs.items(): + if val not in values: + msg = f"{val!r} is not a valid value for {key}" + if _print_supported_values: + msg += f"; supported values are {', '.join(map(repr, values))}" + raise ValueError(msg) + + +def check_shape(shape, /, **kwargs): + """ + For each *key, value* pair in *kwargs*, check that *value* has the shape *shape*; + if not, raise an appropriate ValueError. + + *None* in the shape is treated as a "free" size that can have any length. + e.g. (None, 2) -> (N, 2) + + The values checked must be numpy arrays. + + Examples + -------- + To check for (N, 2) shaped arrays + + >>> _api.check_shape((None, 2), arg=arg, other_arg=other_arg) + """ + for k, v in kwargs.items(): + data_shape = v.shape + + if (len(data_shape) != len(shape) + or any(s != t and t is not None for s, t in zip(data_shape, shape))): + dim_labels = iter(itertools.chain( + 'NMLKJIH', + (f"D{i}" for i in itertools.count()))) + text_shape = ", ".join([str(n) if n is not None else next(dim_labels) + for n in shape[::-1]][::-1]) + if len(shape) == 1: + text_shape += "," + + raise ValueError( + f"{k!r} must be {len(shape)}D with shape ({text_shape}), " + f"but your input has shape {v.shape}" + ) + + +def check_getitem(mapping, /, **kwargs): + """ + *kwargs* must consist of a single *key, value* pair. If *key* is in + *mapping*, return ``mapping[value]``; else, raise an appropriate + ValueError. + + Examples + -------- + >>> _api.check_getitem({"foo": "bar"}, arg=arg) + """ + if len(kwargs) != 1: + raise ValueError("check_getitem takes a single keyword argument") + (k, v), = kwargs.items() + try: + return mapping[v] + except KeyError: + raise ValueError( + f"{v!r} is not a valid value for {k}; supported values are " + f"{', '.join(map(repr, mapping))}") from None + + +def caching_module_getattr(cls): + """ + Helper decorator for implementing module-level ``__getattr__`` as a class. + + This decorator must be used at the module toplevel as follows:: + + @caching_module_getattr + class __getattr__: # The class *must* be named ``__getattr__``. + @property # Only properties are taken into account. + def name(self): ... + + The ``__getattr__`` class will be replaced by a ``__getattr__`` + function such that trying to access ``name`` on the module will + resolve the corresponding property (which may be decorated e.g. with + ``_api.deprecated`` for deprecating module globals). The properties are + all implicitly cached. Moreover, a suitable AttributeError is generated + and raised if no property with the given name exists. + """ + + assert cls.__name__ == "__getattr__" + # Don't accidentally export cls dunders. + props = {name: prop for name, prop in vars(cls).items() + if isinstance(prop, property)} + instance = cls() + + @functools.cache + def __getattr__(name): + if name in props: + return props[name].__get__(instance) + raise AttributeError( + f"module {cls.__module__!r} has no attribute {name!r}") + + return __getattr__ + + +def define_aliases(alias_d, cls=None): + """ + Class decorator for defining property aliases. + + Use as :: + + @_api.define_aliases({"property": ["alias", ...], ...}) + class C: ... + + For each property, if the corresponding ``get_property`` is defined in the + class so far, an alias named ``get_alias`` will be defined; the same will + be done for setters. If neither the getter nor the setter exists, an + exception will be raised. + + The alias map is stored as the ``_alias_map`` attribute on the class and + can be used by `.normalize_kwargs` (which assumes that higher priority + aliases come last). + """ + if cls is None: # Return the actual class decorator. + return functools.partial(define_aliases, alias_d) + + def make_alias(name): # Enforce a closure over *name*. + @functools.wraps(getattr(cls, name)) + def method(self, *args, **kwargs): + return getattr(self, name)(*args, **kwargs) + return method + + for prop, aliases in alias_d.items(): + exists = False + for prefix in ["get_", "set_"]: + if prefix + prop in vars(cls): + exists = True + for alias in aliases: + method = make_alias(prefix + prop) + method.__name__ = prefix + alias + method.__doc__ = f"Alias for `{prefix + prop}`." + setattr(cls, prefix + alias, method) + if not exists: + raise ValueError( + f"Neither getter nor setter exists for {prop!r}") + + def get_aliased_and_aliases(d): + return {*d, *(alias for aliases in d.values() for alias in aliases)} + + preexisting_aliases = getattr(cls, "_alias_map", {}) + conflicting = (get_aliased_and_aliases(preexisting_aliases) + & get_aliased_and_aliases(alias_d)) + if conflicting: + # Need to decide on conflict resolution policy. + raise NotImplementedError( + f"Parent class already defines conflicting aliases: {conflicting}") + cls._alias_map = {**preexisting_aliases, **alias_d} + return cls + + +def select_matching_signature(funcs, *args, **kwargs): + """ + Select and call the function that accepts ``*args, **kwargs``. + + *funcs* is a list of functions which should not raise any exception (other + than `TypeError` if the arguments passed do not match their signature). + + `select_matching_signature` tries to call each of the functions in *funcs* + with ``*args, **kwargs`` (in the order in which they are given). Calls + that fail with a `TypeError` are silently skipped. As soon as a call + succeeds, `select_matching_signature` returns its return value. If no + function accepts ``*args, **kwargs``, then the `TypeError` raised by the + last failing call is re-raised. + + Callers should normally make sure that any ``*args, **kwargs`` can only + bind a single *func* (to avoid any ambiguity), although this is not checked + by `select_matching_signature`. + + Notes + ----- + `select_matching_signature` is intended to help implementing + signature-overloaded functions. In general, such functions should be + avoided, except for back-compatibility concerns. A typical use pattern is + :: + + def my_func(*args, **kwargs): + params = select_matching_signature( + [lambda old1, old2: locals(), lambda new: locals()], + *args, **kwargs) + if "old1" in params: + warn_deprecated(...) + old1, old2 = params.values() # note that locals() is ordered. + else: + new, = params.values() + # do things with params + + which allows *my_func* to be called either with two parameters (*old1* and + *old2*) or a single one (*new*). Note that the new signature is given + last, so that callers get a `TypeError` corresponding to the new signature + if the arguments they passed in do not match any signature. + """ + # Rather than relying on locals() ordering, one could have just used func's + # signature (``bound = inspect.signature(func).bind(*args, **kwargs); + # bound.apply_defaults(); return bound``) but that is significantly slower. + for i, func in enumerate(funcs): + try: + return func(*args, **kwargs) + except TypeError: + if i == len(funcs) - 1: + raise + + +def nargs_error(name, takes, given): + """Generate a TypeError to be raised by function calls with wrong arity.""" + return TypeError(f"{name}() takes {takes} positional arguments but " + f"{given} were given") + + +def kwarg_error(name, kw): + """ + Generate a TypeError to be raised by function calls with wrong kwarg. + + Parameters + ---------- + name : str + The name of the calling function. + kw : str or Iterable[str] + Either the invalid keyword argument name, or an iterable yielding + invalid keyword arguments (e.g., a ``kwargs`` dict). + """ + if not isinstance(kw, str): + kw = next(iter(kw)) + return TypeError(f"{name}() got an unexpected keyword argument '{kw}'") + + +def recursive_subclasses(cls): + """Yield *cls* and direct and indirect subclasses of *cls*.""" + yield cls + for subcls in cls.__subclasses__(): + yield from recursive_subclasses(subcls) + + +def warn_external(message, category=None): + """ + `warnings.warn` wrapper that sets *stacklevel* to "outside Matplotlib". + + The original emitter of the warning can be obtained by patching this + function back to `warnings.warn`, i.e. ``_api.warn_external = + warnings.warn`` (or ``functools.partial(warnings.warn, stacklevel=2)``, + etc.). + """ + kwargs = {} + if sys.version_info[:2] >= (3, 12): + # Go to Python's `site-packages` or `lib` from an editable install. + basedir = pathlib.Path(__file__).parents[2] + kwargs['skip_file_prefixes'] = (str(basedir / 'matplotlib'), + str(basedir / 'mpl_toolkits')) + else: + frame = sys._getframe() + for stacklevel in itertools.count(1): + if frame is None: + # when called in embedded context may hit frame is None + kwargs['stacklevel'] = stacklevel + break + if not re.match(r"\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))", + # Work around sphinx-gallery not setting __name__. + frame.f_globals.get("__name__", "")): + kwargs['stacklevel'] = stacklevel + break + frame = frame.f_back + # preemptively break reference cycle between locals and the frame + del frame + warnings.warn(message, category, **kwargs) diff --git a/lib/python3.12/site-packages/matplotlib/_api/__init__.pyi b/lib/python3.12/site-packages/matplotlib/_api/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..ea7076feac3c415be6d789e92f87020ea89db7a1 --- /dev/null +++ b/lib/python3.12/site-packages/matplotlib/_api/__init__.pyi @@ -0,0 +1,59 @@ +from collections.abc import Callable, Generator, Iterable, Mapping, Sequence +from typing import Any, TypeVar, overload +from typing_extensions import Self # < Py 3.11 + +from numpy.typing import NDArray + +from .deprecation import ( # noqa: F401, re-exported API + deprecated as deprecated, + warn_deprecated as warn_deprecated, + rename_parameter as rename_parameter, + delete_parameter as delete_parameter, + make_keyword_only as make_keyword_only, + deprecate_method_override as deprecate_method_override, + deprecate_privatize_attribute as deprecate_privatize_attribute, + suppress_matplotlib_deprecation_warning as suppress_matplotlib_deprecation_warning, + MatplotlibDeprecationWarning as MatplotlibDeprecationWarning, +) + +_T = TypeVar("_T") + +class classproperty(Any): + def __init__( + self, + fget: Callable[[_T], Any], + fset: None = ..., + fdel: None = ..., + doc: str | None = None, + ): ... + @overload + def __get__(self, instance: None, owner: None) -> Self: ... + @overload + def __get__(self, instance: object, owner: type[object]) -> Any: ... + @property + def fget(self) -> Callable[[_T], Any]: ... + +def check_isinstance( + types: type | tuple[type | None, ...], /, **kwargs: Any +) -> None: ... +def check_in_list( + values: Sequence[Any], /, *, _print_supported_values: bool = ..., **kwargs: Any +) -> None: ... +def check_shape(shape: tuple[int | None, ...], /, **kwargs: NDArray) -> None: ... +def check_getitem(mapping: Mapping[Any, Any], /, **kwargs: Any) -> Any: ... +def caching_module_getattr(cls: type) -> Callable[[str], Any]: ... +@overload +def define_aliases( + alias_d: dict[str, list[str]], cls: None = ... +) -> Callable[[type[_T]], type[_T]]: ... +@overload +def define_aliases(alias_d: dict[str, list[str]], cls: type[_T]) -> type[_T]: ... +def select_matching_signature( + funcs: list[Callable], *args: Any, **kwargs: Any +) -> Any: ... +def nargs_error(name: str, takes: int | str, given: int) -> TypeError: ... +def kwarg_error(name: str, kw: str | Iterable[str]) -> TypeError: ... +def recursive_subclasses(cls: type) -> Generator[type, None, None]: ... +def warn_external( + message: str | Warning, category: type[Warning] | None = ... +) -> None: ... diff --git a/lib/python3.12/site-packages/matplotlib/sphinxext/__init__.py b/lib/python3.12/site-packages/matplotlib/sphinxext/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f3ff5f939eec15cec63f236890f0cd72fde74fd1 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/figmpl_directive.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/figmpl_directive.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..149ba0db9183ea615dded092bddaf748d37b6f86 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/figmpl_directive.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/mathmpl.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/mathmpl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc959d8570f329ceeaacb61e990893927b08a4e4 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/mathmpl.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/plot_directive.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/plot_directive.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d175d51b10f622bf95074ad21848559112662b3 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/plot_directive.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/roles.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/roles.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a59aa7e7f76a652c10b7e49300eb490a0dcf00a Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/sphinxext/__pycache__/roles.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/sphinxext/figmpl_directive.py b/lib/python3.12/site-packages/matplotlib/sphinxext/figmpl_directive.py new file mode 100644 index 0000000000000000000000000000000000000000..7cb9c6c04e8a130fe9d61c3a4c66f0ba4d80b1ad --- /dev/null +++ b/lib/python3.12/site-packages/matplotlib/sphinxext/figmpl_directive.py @@ -0,0 +1,286 @@ +""" +Add a ``figure-mpl`` directive that is a responsive version of ``figure``. + +This implementation is very similar to ``.. figure::``, except it also allows a +``srcset=`` argument to be passed to the image tag, hence allowing responsive +resolution images. + +There is no particular reason this could not be used standalone, but is meant +to be used with :doc:`/api/sphinxext_plot_directive_api`. + +Note that the directory organization is a bit different than ``.. figure::``. +See the *FigureMpl* documentation below. + +""" +import os +from os.path import relpath +from pathlib import PurePath, Path +import shutil + +from docutils import nodes +from docutils.parsers.rst import directives +from docutils.parsers.rst.directives.images import Figure, Image +from sphinx.errors import ExtensionError + +import matplotlib + + +class figmplnode(nodes.General, nodes.Element): + pass + + +class FigureMpl(Figure): + """ + Implements a directive to allow an optional hidpi image. + + Meant to be used with the *plot_srcset* configuration option in conf.py, + and gets set in the TEMPLATE of plot_directive.py + + e.g.:: + + .. figure-mpl:: plot_directive/some_plots-1.png + :alt: bar + :srcset: plot_directive/some_plots-1.png, + plot_directive/some_plots-1.2x.png 2.00x + :class: plot-directive + + The resulting html (at ``some_plots.html``) is:: + + bar + + Note that the handling of subdirectories is different than that used by the sphinx + figure directive:: + + .. figure-mpl:: plot_directive/nestedpage/index-1.png + :alt: bar + :srcset: plot_directive/nestedpage/index-1.png + plot_directive/nestedpage/index-1.2x.png 2.00x + :class: plot_directive + + The resulting html (at ``nestedpage/index.html``):: + + bar + + where the subdirectory is included in the image name for uniqueness. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 2 + final_argument_whitespace = False + option_spec = { + 'alt': directives.unchanged, + 'height': directives.length_or_unitless, + 'width': directives.length_or_percentage_or_unitless, + 'scale': directives.nonnegative_int, + 'align': Image.align, + 'class': directives.class_option, + 'caption': directives.unchanged, + 'srcset': directives.unchanged, + } + + def run(self): + + image_node = figmplnode() + + imagenm = self.arguments[0] + image_node['alt'] = self.options.get('alt', '') + image_node['align'] = self.options.get('align', None) + image_node['class'] = self.options.get('class', None) + image_node['width'] = self.options.get('width', None) + image_node['height'] = self.options.get('height', None) + image_node['scale'] = self.options.get('scale', None) + image_node['caption'] = self.options.get('caption', None) + + # we would like uri to be the highest dpi version so that + # latex etc will use that. But for now, lets just make + # imagenm... maybe pdf one day? + + image_node['uri'] = imagenm + image_node['srcset'] = self.options.get('srcset', None) + + return [image_node] + + +def _parse_srcsetNodes(st): + """ + parse srcset... + """ + entries = st.split(',') + srcset = {} + for entry in entries: + spl = entry.strip().split(' ') + if len(spl) == 1: + srcset[0] = spl[0] + elif len(spl) == 2: + mult = spl[1][:-1] + srcset[float(mult)] = spl[0] + else: + raise ExtensionError(f'srcset argument "{entry}" is invalid.') + return srcset + + +def _copy_images_figmpl(self, node): + + # these will be the temporary place the plot-directive put the images eg: + # ../../../build/html/plot_directive/users/explain/artists/index-1.png + if node['srcset']: + srcset = _parse_srcsetNodes(node['srcset']) + else: + srcset = None + + # the rst file's location: eg /Users/username/matplotlib/doc/users/explain/artists + docsource = PurePath(self.document['source']).parent + + # get the relpath relative to root: + srctop = self.builder.srcdir + rel = relpath(docsource, srctop).replace('.', '').replace(os.sep, '-') + if len(rel): + rel += '-' + # eg: users/explain/artists + + imagedir = PurePath(self.builder.outdir, self.builder.imagedir) + # eg: /Users/username/matplotlib/doc/build/html/_images/users/explain/artists + + Path(imagedir).mkdir(parents=True, exist_ok=True) + + # copy all the sources to the imagedir: + if srcset: + for src in srcset.values(): + # the entries in srcset are relative to docsource's directory + abspath = PurePath(docsource, src) + name = rel + abspath.name + shutil.copyfile(abspath, imagedir / name) + else: + abspath = PurePath(docsource, node['uri']) + name = rel + abspath.name + shutil.copyfile(abspath, imagedir / name) + + return imagedir, srcset, rel + + +def visit_figmpl_html(self, node): + + imagedir, srcset, rel = _copy_images_figmpl(self, node) + + # /doc/examples/subd/plot_1.rst + docsource = PurePath(self.document['source']) + # /doc/ + # make sure to add the trailing slash: + srctop = PurePath(self.builder.srcdir, '') + # examples/subd/plot_1.rst + relsource = relpath(docsource, srctop) + # /doc/build/html + desttop = PurePath(self.builder.outdir, '') + # /doc/build/html/examples/subd + dest = desttop / relsource + + # ../../_images/ for dirhtml and ../_images/ for html + imagerel = PurePath(relpath(imagedir, dest.parent)).as_posix() + if self.builder.name == "dirhtml": + imagerel = f'..{imagerel}' + + # make uri also be relative... + nm = PurePath(node['uri'][1:]).name + uri = f'{imagerel}/{rel}{nm}' + img_attrs = {'src': uri, 'alt': node['alt']} + + # make srcset str. Need to change all the prefixes! + maxsrc = uri + if srcset: + maxmult = -1 + srcsetst = '' + for mult, src in srcset.items(): + nm = PurePath(src[1:]).name + # ../../_images/plot_1_2_0x.png + path = f'{imagerel}/{rel}{nm}' + srcsetst += path + if mult == 0: + srcsetst += ', ' + else: + srcsetst += f' {mult:1.2f}x, ' + + if mult > maxmult: + maxmult = mult + maxsrc = path + + # trim trailing comma and space... + img_attrs['srcset'] = srcsetst[:-2] + + if node['class'] is not None: + img_attrs['class'] = ' '.join(node['class']) + for style in ['width', 'height', 'scale']: + if node[style]: + if 'style' not in img_attrs: + img_attrs['style'] = f'{style}: {node[style]};' + else: + img_attrs['style'] += f'{style}: {node[style]};' + + #
+ # + # _images/index-1.2x.png + # + #
+ #

Figure caption is here.... + # #

+ #
+ #
+ self.body.append( + self.starttag( + node, 'figure', + CLASS=f'align-{node["align"]}' if node['align'] else 'align-center')) + self.body.append( + self.starttag(node, 'a', CLASS='reference internal image-reference', + href=maxsrc) + + self.emptytag(node, 'img', **img_attrs) + + '\n') + if node['caption']: + self.body.append(self.starttag(node, 'figcaption')) + self.body.append(self.starttag(node, 'p')) + self.body.append(self.starttag(node, 'span', CLASS='caption-text')) + self.body.append(node['caption']) + self.body.append('

\n') + self.body.append('\n') + + +def visit_figmpl_latex(self, node): + + if node['srcset'] is not None: + imagedir, srcset = _copy_images_figmpl(self, node) + maxmult = -1 + # choose the highest res version for latex: + maxmult = max(srcset, default=-1) + node['uri'] = PurePath(srcset[maxmult]).name + + self.visit_figure(node) + + +def depart_figmpl_html(self, node): + pass + + +def depart_figmpl_latex(self, node): + self.depart_figure(node) + + +def figurempl_addnode(app): + app.add_node(figmplnode, + html=(visit_figmpl_html, depart_figmpl_html), + latex=(visit_figmpl_latex, depart_figmpl_latex)) + + +def setup(app): + app.add_directive("figure-mpl", FigureMpl) + figurempl_addnode(app) + metadata = {'parallel_read_safe': True, 'parallel_write_safe': True, + 'version': matplotlib.__version__} + return metadata diff --git a/lib/python3.12/site-packages/matplotlib/sphinxext/mathmpl.py b/lib/python3.12/site-packages/matplotlib/sphinxext/mathmpl.py new file mode 100644 index 0000000000000000000000000000000000000000..30f0245242583c5ea162de9e5443da67d7111465 --- /dev/null +++ b/lib/python3.12/site-packages/matplotlib/sphinxext/mathmpl.py @@ -0,0 +1,242 @@ +r""" +A role and directive to display mathtext in Sphinx +================================================== + +The ``mathmpl`` Sphinx extension creates a mathtext image in Matplotlib and +shows it in html output. Thus, it is a true and faithful representation of what +you will see if you pass a given LaTeX string to Matplotlib (see +:ref:`mathtext`). + +.. warning:: + In most cases, you will likely want to use one of `Sphinx's builtin Math + extensions + `__ + instead of this one. The builtin Sphinx math directive uses MathJax to + render mathematical expressions, and addresses accessibility concerns that + ``mathmpl`` doesn't address. + +Mathtext may be included in two ways: + +1. Inline, using the role:: + + This text uses inline math: :mathmpl:`\alpha > \beta`. + + which produces: + + This text uses inline math: :mathmpl:`\alpha > \beta`. + +2. Standalone, using the directive:: + + Here is some standalone math: + + .. mathmpl:: + + \alpha > \beta + + which produces: + + Here is some standalone math: + + .. mathmpl:: + + \alpha > \beta + +Options +------- + +The ``mathmpl`` role and directive both support the following options: + +fontset : str, default: 'cm' + The font set to use when displaying math. See :rc:`mathtext.fontset`. + +fontsize : float + The font size, in points. Defaults to the value from the extension + configuration option defined below. + +Configuration options +--------------------- + +The mathtext extension has the following configuration options: + +mathmpl_fontsize : float, default: 10.0 + Default font size, in points. + +mathmpl_srcset : list of str, default: [] + Additional image sizes to generate when embedding in HTML, to support + `responsive resolution images + `__. + The list should contain additional x-descriptors (``'1.5x'``, ``'2x'``, + etc.) to generate (1x is the default and always included.) + +""" + +import hashlib +from pathlib import Path + +from docutils import nodes +from docutils.parsers.rst import Directive, directives +import sphinx +from sphinx.errors import ConfigError, ExtensionError + +import matplotlib as mpl +from matplotlib import _api, mathtext +from matplotlib.rcsetup import validate_float_or_None + + +# Define LaTeX math node: +class latex_math(nodes.General, nodes.Element): + pass + + +def fontset_choice(arg): + return directives.choice(arg, mathtext.MathTextParser._font_type_mapping) + + +def math_role(role, rawtext, text, lineno, inliner, + options={}, content=[]): + i = rawtext.find('`') + latex = rawtext[i+1:-1] + node = latex_math(rawtext) + node['latex'] = latex + node['fontset'] = options.get('fontset', 'cm') + node['fontsize'] = options.get('fontsize', + setup.app.config.mathmpl_fontsize) + return [node], [] +math_role.options = {'fontset': fontset_choice, + 'fontsize': validate_float_or_None} + + +class MathDirective(Directive): + """ + The ``.. mathmpl::`` directive, as documented in the module's docstring. + """ + has_content = True + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = {'fontset': fontset_choice, + 'fontsize': validate_float_or_None} + + def run(self): + latex = ''.join(self.content) + node = latex_math(self.block_text) + node['latex'] = latex + node['fontset'] = self.options.get('fontset', 'cm') + node['fontsize'] = self.options.get('fontsize', + setup.app.config.mathmpl_fontsize) + return [node] + + +# This uses mathtext to render the expression +def latex2png(latex, filename, fontset='cm', fontsize=10, dpi=100): + with mpl.rc_context({'mathtext.fontset': fontset, 'font.size': fontsize}): + try: + depth = mathtext.math_to_image( + f"${latex}$", filename, dpi=dpi, format="png") + except Exception: + _api.warn_external(f"Could not render math expression {latex}") + depth = 0 + return depth + + +# LaTeX to HTML translation stuff: +def latex2html(node, source): + inline = isinstance(node.parent, nodes.TextElement) + latex = node['latex'] + fontset = node['fontset'] + fontsize = node['fontsize'] + name = 'math-{}'.format( + hashlib.sha256( + f'{latex}{fontset}{fontsize}'.encode(), + usedforsecurity=False, + ).hexdigest()[-10:]) + + destdir = Path(setup.app.builder.outdir, '_images', 'mathmpl') + destdir.mkdir(parents=True, exist_ok=True) + + dest = destdir / f'{name}.png' + depth = latex2png(latex, dest, fontset, fontsize=fontsize) + + srcset = [] + for size in setup.app.config.mathmpl_srcset: + filename = f'{name}-{size.replace(".", "_")}.png' + latex2png(latex, destdir / filename, fontset, fontsize=fontsize, + dpi=100 * float(size[:-1])) + srcset.append( + f'{setup.app.builder.imgpath}/mathmpl/{filename} {size}') + if srcset: + srcset = (f'srcset="{setup.app.builder.imgpath}/mathmpl/{name}.png, ' + + ', '.join(srcset) + '" ') + + if inline: + cls = '' + else: + cls = 'class="center" ' + if inline and depth != 0: + style = 'style="position: relative; bottom: -%dpx"' % (depth + 1) + else: + style = '' + + return (f'') + + +def _config_inited(app, config): + # Check for srcset hidpi images + for i, size in enumerate(app.config.mathmpl_srcset): + if size[-1] == 'x': # "2x" = "2.0" + try: + float(size[:-1]) + except ValueError: + raise ConfigError( + f'Invalid value for mathmpl_srcset parameter: {size!r}. ' + 'Must be a list of strings with the multiplicative ' + 'factor followed by an "x". e.g. ["2.0x", "1.5x"]') + else: + raise ConfigError( + f'Invalid value for mathmpl_srcset parameter: {size!r}. ' + 'Must be a list of strings with the multiplicative ' + 'factor followed by an "x". e.g. ["2.0x", "1.5x"]') + + +def setup(app): + setup.app = app + app.add_config_value('mathmpl_fontsize', 10.0, True) + app.add_config_value('mathmpl_srcset', [], True) + try: + app.connect('config-inited', _config_inited) # Sphinx 1.8+ + except ExtensionError: + app.connect('env-updated', lambda app, env: _config_inited(app, None)) + + # Add visit/depart methods to HTML-Translator: + def visit_latex_math_html(self, node): + source = self.document.attributes['source'] + self.body.append(latex2html(node, source)) + + def depart_latex_math_html(self, node): + pass + + # Add visit/depart methods to LaTeX-Translator: + def visit_latex_math_latex(self, node): + inline = isinstance(node.parent, nodes.TextElement) + if inline: + self.body.append('$%s$' % node['latex']) + else: + self.body.extend(['\\begin{equation}', + node['latex'], + '\\end{equation}']) + + def depart_latex_math_latex(self, node): + pass + + app.add_node(latex_math, + html=(visit_latex_math_html, depart_latex_math_html), + latex=(visit_latex_math_latex, depart_latex_math_latex)) + app.add_role('mathmpl', math_role) + app.add_directive('mathmpl', MathDirective) + if sphinx.version_info < (1, 8): + app.add_role('math', math_role) + app.add_directive('math', MathDirective) + + metadata = {'parallel_read_safe': True, 'parallel_write_safe': True} + return metadata diff --git a/lib/python3.12/site-packages/matplotlib/sphinxext/plot_directive.py b/lib/python3.12/site-packages/matplotlib/sphinxext/plot_directive.py new file mode 100644 index 0000000000000000000000000000000000000000..ab9b7d76955015186d148e1a04fa32f3ab245a3e --- /dev/null +++ b/lib/python3.12/site-packages/matplotlib/sphinxext/plot_directive.py @@ -0,0 +1,936 @@ +""" +A directive for including a Matplotlib plot in a Sphinx document +================================================================ + +This is a Sphinx extension providing a reStructuredText directive +``.. plot::`` for including a plot in a Sphinx document. + +In HTML output, ``.. plot::`` will include a .png file with a link +to a high-res .png and .pdf. In LaTeX output, it will include a .pdf. + +The plot content may be defined in one of three ways: + +1. **A path to a source file** as the argument to the directive:: + + .. plot:: path/to/plot.py + + When a path to a source file is given, the content of the + directive may optionally contain a caption for the plot:: + + .. plot:: path/to/plot.py + + The plot caption. + + Additionally, one may specify the name of a function to call (with + no arguments) immediately after importing the module:: + + .. plot:: path/to/plot.py plot_function1 + +2. Included as **inline content** to the directive:: + + .. plot:: + + import matplotlib.pyplot as plt + plt.plot([1, 2, 3], [4, 5, 6]) + plt.title("A plotting exammple") + +3. Using **doctest** syntax:: + + .. plot:: + + A plotting example: + >>> import matplotlib.pyplot as plt + >>> plt.plot([1, 2, 3], [4, 5, 6]) + +Options +------- + +The ``.. plot::`` directive supports the following options: + +``:format:`` : {'python', 'doctest'} + The format of the input. If unset, the format is auto-detected. + +``:include-source:`` : bool + Whether to display the source code. The default can be changed using + the ``plot_include_source`` variable in :file:`conf.py` (which itself + defaults to False). + +``:show-source-link:`` : bool + Whether to show a link to the source in HTML. The default can be + changed using the ``plot_html_show_source_link`` variable in + :file:`conf.py` (which itself defaults to True). + +``:context:`` : bool or str + If provided, the code will be run in the context of all previous plot + directives for which the ``:context:`` option was specified. This only + applies to inline code plot directives, not those run from files. If + the ``:context: reset`` option is specified, the context is reset + for this and future plots, and previous figures are closed prior to + running the code. ``:context: close-figs`` keeps the context but closes + previous figures before running the code. + +``:nofigs:`` : bool + If specified, the code block will be run, but no figures will be + inserted. This is usually useful with the ``:context:`` option. + +``:caption:`` : str + If specified, the option's argument will be used as a caption for the + figure. This overwrites the caption given in the content, when the plot + is generated from a file. + +Additionally, this directive supports all the options of the `image directive +`_, +except for ``:target:`` (since plot will add its own target). These include +``:alt:``, ``:height:``, ``:width:``, ``:scale:``, ``:align:`` and ``:class:``. + +Configuration options +--------------------- + +The plot directive has the following configuration options: + +plot_include_source + Default value for the include-source option (default: False). + +plot_html_show_source_link + Whether to show a link to the source in HTML (default: True). + +plot_pre_code + Code that should be executed before each plot. If None (the default), + it will default to a string containing:: + + import numpy as np + from matplotlib import pyplot as plt + +plot_basedir + Base directory, to which ``plot::`` file names are relative to. + If None or empty (the default), file names are relative to the + directory where the file containing the directive is. + +plot_formats + File formats to generate (default: ['png', 'hires.png', 'pdf']). + List of tuples or strings:: + + [(suffix, dpi), suffix, ...] + + that determine the file format and the DPI. For entries whose + DPI was omitted, sensible defaults are chosen. When passing from + the command line through sphinx_build the list should be passed as + suffix:dpi,suffix:dpi, ... + +plot_html_show_formats + Whether to show links to the files in HTML (default: True). + +plot_rcparams + A dictionary containing any non-standard rcParams that should + be applied before each plot (default: {}). + +plot_apply_rcparams + By default, rcParams are applied when ``:context:`` option is not used + in a plot directive. If set, this configuration option overrides this + behavior and applies rcParams before each plot. + +plot_working_directory + By default, the working directory will be changed to the directory of + the example, so the code can get at its data files, if any. Also its + path will be added to `sys.path` so it can import any helper modules + sitting beside it. This configuration option can be used to specify + a central directory (also added to `sys.path`) where data files and + helper modules for all code are located. + +plot_template + Provide a customized template for preparing restructured text. + +plot_srcset + Allow the srcset image option for responsive image resolutions. List of + strings with the multiplicative factors followed by an "x". + e.g. ["2.0x", "1.5x"]. "2.0x" will create a png with the default "png" + resolution from plot_formats, multiplied by 2. If plot_srcset is + specified, the plot directive uses the + :doc:`/api/sphinxext_figmpl_directive_api` (instead of the usual figure + directive) in the intermediary rst file that is generated. + The plot_srcset option is incompatible with *singlehtml* builds, and an + error will be raised. + +Notes on how it works +--------------------- + +The plot directive runs the code it is given, either in the source file or the +code under the directive. The figure created (if any) is saved in the sphinx +build directory under a subdirectory named ``plot_directive``. It then creates +an intermediate rst file that calls a ``.. figure:`` directive (or +``.. figmpl::`` directive if ``plot_srcset`` is being used) and has links to +the ``*.png`` files in the ``plot_directive`` directory. These translations can +be customized by changing the *plot_template*. See the source of +:doc:`/api/sphinxext_plot_directive_api` for the templates defined in *TEMPLATE* +and *TEMPLATE_SRCSET*. +""" + +import contextlib +import doctest +from io import StringIO +import itertools +import os +from os.path import relpath +from pathlib import Path +import re +import shutil +import sys +import textwrap +import traceback + +from docutils.parsers.rst import directives, Directive +from docutils.parsers.rst.directives.images import Image +import jinja2 # Sphinx dependency. + +from sphinx.errors import ExtensionError + +import matplotlib +from matplotlib.backend_bases import FigureManagerBase +import matplotlib.pyplot as plt +from matplotlib import _pylab_helpers, cbook + +matplotlib.use("agg") + +__version__ = 2 + + +# ----------------------------------------------------------------------------- +# Registration hook +# ----------------------------------------------------------------------------- + + +def _option_boolean(arg): + if not arg or not arg.strip(): + # no argument given, assume used as a flag + return True + elif arg.strip().lower() in ('no', '0', 'false'): + return False + elif arg.strip().lower() in ('yes', '1', 'true'): + return True + else: + raise ValueError(f'{arg!r} unknown boolean') + + +def _option_context(arg): + if arg in [None, 'reset', 'close-figs']: + return arg + raise ValueError("Argument should be None or 'reset' or 'close-figs'") + + +def _option_format(arg): + return directives.choice(arg, ('python', 'doctest')) + + +def mark_plot_labels(app, document): + """ + To make plots referenceable, we need to move the reference from the + "htmlonly" (or "latexonly") node to the actual figure node itself. + """ + for name, explicit in document.nametypes.items(): + if not explicit: + continue + labelid = document.nameids[name] + if labelid is None: + continue + node = document.ids[labelid] + if node.tagname in ('html_only', 'latex_only'): + for n in node: + if n.tagname == 'figure': + sectname = name + for c in n: + if c.tagname == 'caption': + sectname = c.astext() + break + + node['ids'].remove(labelid) + node['names'].remove(name) + n['ids'].append(labelid) + n['names'].append(name) + document.settings.env.labels[name] = \ + document.settings.env.docname, labelid, sectname + break + + +class PlotDirective(Directive): + """The ``.. plot::`` directive, as documented in the module's docstring.""" + + has_content = True + required_arguments = 0 + optional_arguments = 2 + final_argument_whitespace = False + option_spec = { + 'alt': directives.unchanged, + 'height': directives.length_or_unitless, + 'width': directives.length_or_percentage_or_unitless, + 'scale': directives.nonnegative_int, + 'align': Image.align, + 'class': directives.class_option, + 'include-source': _option_boolean, + 'show-source-link': _option_boolean, + 'format': _option_format, + 'context': _option_context, + 'nofigs': directives.flag, + 'caption': directives.unchanged, + } + + def run(self): + """Run the plot directive.""" + try: + return run(self.arguments, self.content, self.options, + self.state_machine, self.state, self.lineno) + except Exception as e: + raise self.error(str(e)) + + +def _copy_css_file(app, exc): + if exc is None and app.builder.format == 'html': + src = cbook._get_data_path('plot_directive/plot_directive.css') + dst = app.outdir / Path('_static') + dst.mkdir(exist_ok=True) + # Use copyfile because we do not want to copy src's permissions. + shutil.copyfile(src, dst / Path('plot_directive.css')) + + +def setup(app): + setup.app = app + setup.config = app.config + setup.confdir = app.confdir + app.add_directive('plot', PlotDirective) + app.add_config_value('plot_pre_code', None, True) + app.add_config_value('plot_include_source', False, True) + app.add_config_value('plot_html_show_source_link', True, True) + app.add_config_value('plot_formats', ['png', 'hires.png', 'pdf'], True) + app.add_config_value('plot_basedir', None, True) + app.add_config_value('plot_html_show_formats', True, True) + app.add_config_value('plot_rcparams', {}, True) + app.add_config_value('plot_apply_rcparams', False, True) + app.add_config_value('plot_working_directory', None, True) + app.add_config_value('plot_template', None, True) + app.add_config_value('plot_srcset', [], True) + app.connect('doctree-read', mark_plot_labels) + app.add_css_file('plot_directive.css') + app.connect('build-finished', _copy_css_file) + metadata = {'parallel_read_safe': True, 'parallel_write_safe': True, + 'version': matplotlib.__version__} + return metadata + + +# ----------------------------------------------------------------------------- +# Doctest handling +# ----------------------------------------------------------------------------- + + +def contains_doctest(text): + try: + # check if it's valid Python as-is + compile(text, '', 'exec') + return False + except SyntaxError: + pass + r = re.compile(r'^\s*>>>', re.M) + m = r.search(text) + return bool(m) + + +def _split_code_at_show(text, function_name): + """Split code at plt.show().""" + + is_doctest = contains_doctest(text) + if function_name is None: + parts = [] + part = [] + for line in text.split("\n"): + if ((not is_doctest and line.startswith('plt.show(')) or + (is_doctest and line.strip() == '>>> plt.show()')): + part.append(line) + parts.append("\n".join(part)) + part = [] + else: + part.append(line) + if "\n".join(part).strip(): + parts.append("\n".join(part)) + else: + parts = [text] + return is_doctest, parts + + +# ----------------------------------------------------------------------------- +# Template +# ----------------------------------------------------------------------------- + +_SOURCECODE = """ +{{ source_code }} + +.. only:: html + + {% if src_name or (html_show_formats and not multi_image) %} + ( + {%- if src_name -%} + :download:`Source code <{{ build_dir }}/{{ src_name }}>` + {%- endif -%} + {%- if html_show_formats and not multi_image -%} + {%- for img in images -%} + {%- for fmt in img.formats -%} + {%- if src_name or not loop.first -%}, {% endif -%} + :download:`{{ fmt }} <{{ build_dir }}/{{ img.basename }}.{{ fmt }}>` + {%- endfor -%} + {%- endfor -%} + {%- endif -%} + ) + {% endif %} +""" + +TEMPLATE_SRCSET = _SOURCECODE + """ + {% for img in images %} + .. figure-mpl:: {{ build_dir }}/{{ img.basename }}.{{ default_fmt }} + {% for option in options -%} + {{ option }} + {% endfor %} + {%- if caption -%} + {{ caption }} {# appropriate leading whitespace added beforehand #} + {% endif -%} + {%- if srcset -%} + :srcset: {{ build_dir }}/{{ img.basename }}.{{ default_fmt }} + {%- for sr in srcset -%} + , {{ build_dir }}/{{ img.basename }}.{{ sr }}.{{ default_fmt }} {{sr}} + {%- endfor -%} + {% endif %} + + {% if html_show_formats and multi_image %} + ( + {%- for fmt in img.formats -%} + {%- if not loop.first -%}, {% endif -%} + :download:`{{ fmt }} <{{ build_dir }}/{{ img.basename }}.{{ fmt }}>` + {%- endfor -%} + ) + {% endif %} + + + {% endfor %} + +.. only:: not html + + {% for img in images %} + .. figure-mpl:: {{ build_dir }}/{{ img.basename }}.* + {% for option in options -%} + {{ option }} + {% endfor -%} + + {{ caption }} {# appropriate leading whitespace added beforehand #} + {% endfor %} + +""" + +TEMPLATE = _SOURCECODE + """ + + {% for img in images %} + .. figure:: {{ build_dir }}/{{ img.basename }}.{{ default_fmt }} + {% for option in options -%} + {{ option }} + {% endfor %} + + {% if html_show_formats and multi_image -%} + ( + {%- for fmt in img.formats -%} + {%- if not loop.first -%}, {% endif -%} + :download:`{{ fmt }} <{{ build_dir }}/{{ img.basename }}.{{ fmt }}>` + {%- endfor -%} + ) + {%- endif -%} + + {{ caption }} {# appropriate leading whitespace added beforehand #} + {% endfor %} + +.. only:: not html + + {% for img in images %} + .. figure:: {{ build_dir }}/{{ img.basename }}.* + {% for option in options -%} + {{ option }} + {% endfor -%} + + {{ caption }} {# appropriate leading whitespace added beforehand #} + {% endfor %} + +""" + +exception_template = """ +.. only:: html + + [`source code <%(linkdir)s/%(basename)s.py>`__] + +Exception occurred rendering plot. + +""" + +# the context of the plot for all directives specified with the +# :context: option +plot_context = dict() + + +class ImageFile: + def __init__(self, basename, dirname): + self.basename = basename + self.dirname = dirname + self.formats = [] + + def filename(self, format): + return os.path.join(self.dirname, f"{self.basename}.{format}") + + def filenames(self): + return [self.filename(fmt) for fmt in self.formats] + + +def out_of_date(original, derived, includes=None): + """ + Return whether *derived* is out-of-date relative to *original* or any of + the RST files included in it using the RST include directive (*includes*). + *derived* and *original* are full paths, and *includes* is optionally a + list of full paths which may have been included in the *original*. + """ + if not os.path.exists(derived): + return True + + if includes is None: + includes = [] + files_to_check = [original, *includes] + + def out_of_date_one(original, derived_mtime): + return (os.path.exists(original) and + derived_mtime < os.stat(original).st_mtime) + + derived_mtime = os.stat(derived).st_mtime + return any(out_of_date_one(f, derived_mtime) for f in files_to_check) + + +class PlotError(RuntimeError): + pass + + +def _run_code(code, code_path, ns=None, function_name=None): + """ + Import a Python module from a path, and run the function given by + name, if function_name is not None. + """ + + # Change the working directory to the directory of the example, so + # it can get at its data files, if any. Add its path to sys.path + # so it can import any helper modules sitting beside it. + pwd = os.getcwd() + if setup.config.plot_working_directory is not None: + try: + os.chdir(setup.config.plot_working_directory) + except OSError as err: + raise OSError(f'{err}\n`plot_working_directory` option in ' + f'Sphinx configuration file must be a valid ' + f'directory path') from err + except TypeError as err: + raise TypeError(f'{err}\n`plot_working_directory` option in ' + f'Sphinx configuration file must be a string or ' + f'None') from err + elif code_path is not None: + dirname = os.path.abspath(os.path.dirname(code_path)) + os.chdir(dirname) + + with cbook._setattr_cm( + sys, argv=[code_path], path=[os.getcwd(), *sys.path]), \ + contextlib.redirect_stdout(StringIO()): + try: + if ns is None: + ns = {} + if not ns: + if setup.config.plot_pre_code is None: + exec('import numpy as np\n' + 'from matplotlib import pyplot as plt\n', ns) + else: + exec(str(setup.config.plot_pre_code), ns) + if "__main__" in code: + ns['__name__'] = '__main__' + + # Patch out non-interactive show() to avoid triggering a warning. + with cbook._setattr_cm(FigureManagerBase, show=lambda self: None): + exec(code, ns) + if function_name is not None: + exec(function_name + "()", ns) + + except (Exception, SystemExit) as err: + raise PlotError(traceback.format_exc()) from err + finally: + os.chdir(pwd) + return ns + + +def clear_state(plot_rcparams, close=True): + if close: + plt.close('all') + matplotlib.rc_file_defaults() + matplotlib.rcParams.update(plot_rcparams) + + +def get_plot_formats(config): + default_dpi = {'png': 80, 'hires.png': 200, 'pdf': 200} + formats = [] + plot_formats = config.plot_formats + for fmt in plot_formats: + if isinstance(fmt, str): + if ':' in fmt: + suffix, dpi = fmt.split(':') + formats.append((str(suffix), int(dpi))) + else: + formats.append((fmt, default_dpi.get(fmt, 80))) + elif isinstance(fmt, (tuple, list)) and len(fmt) == 2: + formats.append((str(fmt[0]), int(fmt[1]))) + else: + raise PlotError('invalid image format "%r" in plot_formats' % fmt) + return formats + + +def _parse_srcset(entries): + """ + Parse srcset for multiples... + """ + srcset = {} + for entry in entries: + entry = entry.strip() + if len(entry) >= 2: + mult = entry[:-1] + srcset[float(mult)] = entry + else: + raise ExtensionError(f'srcset argument {entry!r} is invalid.') + return srcset + + +def render_figures(code, code_path, output_dir, output_base, context, + function_name, config, context_reset=False, + close_figs=False, + code_includes=None): + """ + Run a pyplot script and save the images in *output_dir*. + + Save the images under *output_dir* with file names derived from + *output_base* + """ + + if function_name is not None: + output_base = f'{output_base}_{function_name}' + formats = get_plot_formats(config) + + # Try to determine if all images already exist + + is_doctest, code_pieces = _split_code_at_show(code, function_name) + # Look for single-figure output files first + img = ImageFile(output_base, output_dir) + for format, dpi in formats: + if context or out_of_date(code_path, img.filename(format), + includes=code_includes): + all_exists = False + break + img.formats.append(format) + else: + all_exists = True + + if all_exists: + return [(code, [img])] + + # Then look for multi-figure output files + results = [] + for i, code_piece in enumerate(code_pieces): + images = [] + for j in itertools.count(): + if len(code_pieces) > 1: + img = ImageFile('%s_%02d_%02d' % (output_base, i, j), + output_dir) + else: + img = ImageFile('%s_%02d' % (output_base, j), output_dir) + for fmt, dpi in formats: + if context or out_of_date(code_path, img.filename(fmt), + includes=code_includes): + all_exists = False + break + img.formats.append(fmt) + + # assume that if we have one, we have them all + if not all_exists: + all_exists = (j > 0) + break + images.append(img) + if not all_exists: + break + results.append((code_piece, images)) + else: + all_exists = True + + if all_exists: + return results + + # We didn't find the files, so build them + + results = [] + ns = plot_context if context else {} + + if context_reset: + clear_state(config.plot_rcparams) + plot_context.clear() + + close_figs = not context or close_figs + + for i, code_piece in enumerate(code_pieces): + + if not context or config.plot_apply_rcparams: + clear_state(config.plot_rcparams, close_figs) + elif close_figs: + plt.close('all') + + _run_code(doctest.script_from_examples(code_piece) if is_doctest + else code_piece, + code_path, ns, function_name) + + images = [] + fig_managers = _pylab_helpers.Gcf.get_all_fig_managers() + for j, figman in enumerate(fig_managers): + if len(fig_managers) == 1 and len(code_pieces) == 1: + img = ImageFile(output_base, output_dir) + elif len(code_pieces) == 1: + img = ImageFile("%s_%02d" % (output_base, j), output_dir) + else: + img = ImageFile("%s_%02d_%02d" % (output_base, i, j), + output_dir) + images.append(img) + + for fmt, dpi in formats: + try: + figman.canvas.figure.savefig(img.filename(fmt), dpi=dpi) + if fmt == formats[0][0] and config.plot_srcset: + # save a 2x, 3x etc version of the default... + srcset = _parse_srcset(config.plot_srcset) + for mult, suffix in srcset.items(): + fm = f'{suffix}.{fmt}' + img.formats.append(fm) + figman.canvas.figure.savefig(img.filename(fm), + dpi=int(dpi * mult)) + except Exception as err: + raise PlotError(traceback.format_exc()) from err + img.formats.append(fmt) + + results.append((code_piece, images)) + + if not context or config.plot_apply_rcparams: + clear_state(config.plot_rcparams, close=not context) + + return results + + +def run(arguments, content, options, state_machine, state, lineno): + document = state_machine.document + config = document.settings.env.config + nofigs = 'nofigs' in options + + if config.plot_srcset and setup.app.builder.name == 'singlehtml': + raise ExtensionError( + 'plot_srcset option not compatible with single HTML writer') + + formats = get_plot_formats(config) + default_fmt = formats[0][0] + + options.setdefault('include-source', config.plot_include_source) + options.setdefault('show-source-link', config.plot_html_show_source_link) + + if 'class' in options: + # classes are parsed into a list of string, and output by simply + # printing the list, abusing the fact that RST guarantees to strip + # non-conforming characters + options['class'] = ['plot-directive'] + options['class'] + else: + options.setdefault('class', ['plot-directive']) + keep_context = 'context' in options + context_opt = None if not keep_context else options['context'] + + rst_file = document.attributes['source'] + rst_dir = os.path.dirname(rst_file) + + if len(arguments): + if not config.plot_basedir: + source_file_name = os.path.join(setup.app.builder.srcdir, + directives.uri(arguments[0])) + else: + source_file_name = os.path.join(setup.confdir, config.plot_basedir, + directives.uri(arguments[0])) + # If there is content, it will be passed as a caption. + caption = '\n'.join(content) + + # Enforce unambiguous use of captions. + if "caption" in options: + if caption: + raise ValueError( + 'Caption specified in both content and options.' + ' Please remove ambiguity.' + ) + # Use caption option + caption = options["caption"] + + # If the optional function name is provided, use it + if len(arguments) == 2: + function_name = arguments[1] + else: + function_name = None + + code = Path(source_file_name).read_text(encoding='utf-8') + output_base = os.path.basename(source_file_name) + else: + source_file_name = rst_file + code = textwrap.dedent("\n".join(map(str, content))) + counter = document.attributes.get('_plot_counter', 0) + 1 + document.attributes['_plot_counter'] = counter + base, ext = os.path.splitext(os.path.basename(source_file_name)) + output_base = '%s-%d.py' % (base, counter) + function_name = None + caption = options.get('caption', '') + + base, source_ext = os.path.splitext(output_base) + if source_ext in ('.py', '.rst', '.txt'): + output_base = base + else: + source_ext = '' + + # ensure that LaTeX includegraphics doesn't choke in foo.bar.pdf filenames + output_base = output_base.replace('.', '-') + + # is it in doctest format? + is_doctest = contains_doctest(code) + if 'format' in options: + if options['format'] == 'python': + is_doctest = False + else: + is_doctest = True + + # determine output directory name fragment + source_rel_name = relpath(source_file_name, setup.confdir) + source_rel_dir = os.path.dirname(source_rel_name).lstrip(os.path.sep) + + # build_dir: where to place output files (temporarily) + build_dir = os.path.join(os.path.dirname(setup.app.doctreedir), + 'plot_directive', + source_rel_dir) + # get rid of .. in paths, also changes pathsep + # see note in Python docs for warning about symbolic links on Windows. + # need to compare source and dest paths at end + build_dir = os.path.normpath(build_dir) + os.makedirs(build_dir, exist_ok=True) + + # how to link to files from the RST file + try: + build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, '/') + except ValueError: + # on Windows, relpath raises ValueError when path and start are on + # different mounts/drives + build_dir_link = build_dir + + # get list of included rst files so that the output is updated when any + # plots in the included files change. These attributes are modified by the + # include directive (see the docutils.parsers.rst.directives.misc module). + try: + source_file_includes = [os.path.join(os.getcwd(), t[0]) + for t in state.document.include_log] + except AttributeError: + # the document.include_log attribute only exists in docutils >=0.17, + # before that we need to inspect the state machine + possible_sources = {os.path.join(setup.confdir, t[0]) + for t in state_machine.input_lines.items} + source_file_includes = [f for f in possible_sources + if os.path.isfile(f)] + # remove the source file itself from the includes + try: + source_file_includes.remove(source_file_name) + except ValueError: + pass + + # save script (if necessary) + if options['show-source-link']: + Path(build_dir, output_base + source_ext).write_text( + doctest.script_from_examples(code) + if source_file_name == rst_file and is_doctest + else code, + encoding='utf-8') + + # make figures + try: + results = render_figures(code=code, + code_path=source_file_name, + output_dir=build_dir, + output_base=output_base, + context=keep_context, + function_name=function_name, + config=config, + context_reset=context_opt == 'reset', + close_figs=context_opt == 'close-figs', + code_includes=source_file_includes) + errors = [] + except PlotError as err: + reporter = state.memo.reporter + sm = reporter.system_message( + 2, "Exception occurred in plotting {}\n from {}:\n{}".format( + output_base, source_file_name, err), + line=lineno) + results = [(code, [])] + errors = [sm] + + # Properly indent the caption + if caption and config.plot_srcset: + caption = ':caption: ' + caption.replace('\n', ' ') + elif caption: + caption = '\n' + '\n'.join(' ' + line.strip() + for line in caption.split('\n')) + # generate output restructuredtext + total_lines = [] + for j, (code_piece, images) in enumerate(results): + if options['include-source']: + if is_doctest: + lines = ['', *code_piece.splitlines()] + else: + lines = ['.. code-block:: python', '', + *textwrap.indent(code_piece, ' ').splitlines()] + source_code = "\n".join(lines) + else: + source_code = "" + + if nofigs: + images = [] + + if 'alt' in options: + options['alt'] = options['alt'].replace('\n', ' ') + + opts = [ + f':{key}: {val}' for key, val in options.items() + if key in ('alt', 'height', 'width', 'scale', 'align', 'class')] + + # Not-None src_name signals the need for a source download in the + # generated html + if j == 0 and options['show-source-link']: + src_name = output_base + source_ext + else: + src_name = None + if config.plot_srcset: + srcset = [*_parse_srcset(config.plot_srcset).values()] + template = TEMPLATE_SRCSET + else: + srcset = None + template = TEMPLATE + + result = jinja2.Template(config.plot_template or template).render( + default_fmt=default_fmt, + build_dir=build_dir_link, + src_name=src_name, + multi_image=len(images) > 1, + options=opts, + srcset=srcset, + images=images, + source_code=source_code, + html_show_formats=config.plot_html_show_formats and len(images), + caption=caption) + total_lines.extend(result.split("\n")) + total_lines.extend("\n") + + if total_lines: + state_machine.insert_input(total_lines, source=source_file_name) + + return errors diff --git a/lib/python3.12/site-packages/matplotlib/sphinxext/roles.py b/lib/python3.12/site-packages/matplotlib/sphinxext/roles.py new file mode 100644 index 0000000000000000000000000000000000000000..301adcd8a5f5e0b18d28df297ee2025947e5c90d --- /dev/null +++ b/lib/python3.12/site-packages/matplotlib/sphinxext/roles.py @@ -0,0 +1,147 @@ +""" +Custom roles for the Matplotlib documentation. + +.. warning:: + + These roles are considered semi-public. They are only intended to be used in + the Matplotlib documentation. + +However, it can happen that downstream packages end up pulling these roles into +their documentation, which will result in documentation build errors. The following +describes the exact mechanism and how to fix the errors. + +There are two ways, Matplotlib docstrings can end up in downstream documentation. +You have to subclass a Matplotlib class and either use the ``:inherited-members:`` +option in your autodoc configuration, or you have to override a method without +specifying a new docstring; the new method will inherit the original docstring and +still render in your autodoc. If the docstring contains one of the custom sphinx +roles, you'll see one of the following error messages: + +.. code-block:: none + + Unknown interpreted text role "mpltype". + Unknown interpreted text role "rc". + +To fix this, you can add this module as extension to your sphinx :file:`conf.py`:: + + extensions = [ + 'matplotlib.sphinxext.roles', + # Other extensions. + ] + +.. warning:: + + Direct use of these roles in other packages is not officially supported. We + reserve the right to modify or remove these roles without prior notification. +""" + +from urllib.parse import urlsplit, urlunsplit + +from docutils import nodes + +import matplotlib +from matplotlib import rcParamsDefault + + +class _QueryReference(nodes.Inline, nodes.TextElement): + """ + Wraps a reference or pending reference to add a query string. + + The query string is generated from the attributes added to this node. + + Also equivalent to a `~docutils.nodes.literal` node. + """ + + def to_query_string(self): + """Generate query string from node attributes.""" + return '&'.join(f'{name}={value}' for name, value in self.attlist()) + + +def _visit_query_reference_node(self, node): + """ + Resolve *node* into query strings on its ``reference`` children. + + Then act as if this is a `~docutils.nodes.literal`. + """ + query = node.to_query_string() + for refnode in node.findall(nodes.reference): + uri = urlsplit(refnode['refuri'])._replace(query=query) + refnode['refuri'] = urlunsplit(uri) + + self.visit_literal(node) + + +def _depart_query_reference_node(self, node): + """ + Act as if this is a `~docutils.nodes.literal`. + """ + self.depart_literal(node) + + +def _rcparam_role(name, rawtext, text, lineno, inliner, options=None, content=None): + """ + Sphinx role ``:rc:`` to highlight and link ``rcParams`` entries. + + Usage: Give the desired ``rcParams`` key as parameter. + + :code:`:rc:`figure.dpi`` will render as: :rc:`figure.dpi` + """ + # Generate a pending cross-reference so that Sphinx will ensure this link + # isn't broken at some point in the future. + title = f'rcParams["{text}"]' + target = 'matplotlibrc-sample' + ref_nodes, messages = inliner.interpreted(title, f'{title} <{target}>', + 'ref', lineno) + + qr = _QueryReference(rawtext, highlight=text) + qr += ref_nodes + node_list = [qr] + + # The default backend would be printed as "agg", but that's not correct (as + # the default is actually determined by fallback). + if text in rcParamsDefault and text != "backend": + node_list.extend([ + nodes.Text(' (default: '), + nodes.literal('', repr(rcParamsDefault[text])), + nodes.Text(')'), + ]) + + return node_list, messages + + +def _mpltype_role(name, rawtext, text, lineno, inliner, options=None, content=None): + """ + Sphinx role ``:mpltype:`` for custom matplotlib types. + + In Matplotlib, there are a number of type-like concepts that do not have a + direct type representation; example: color. This role allows to properly + highlight them in the docs and link to their definition. + + Currently supported values: + + - :code:`:mpltype:`color`` will render as: :mpltype:`color` + + """ + mpltype = text + type_to_link_target = { + 'color': 'colors_def', + } + if mpltype not in type_to_link_target: + raise ValueError(f"Unknown mpltype: {mpltype!r}") + + node_list, messages = inliner.interpreted( + mpltype, f'{mpltype} <{type_to_link_target[mpltype]}>', 'ref', lineno) + return node_list, messages + + +def setup(app): + app.add_role("rc", _rcparam_role) + app.add_role("mpltype", _mpltype_role) + app.add_node( + _QueryReference, + html=(_visit_query_reference_node, _depart_query_reference_node), + latex=(_visit_query_reference_node, _depart_query_reference_node), + text=(_visit_query_reference_node, _depart_query_reference_node), + ) + return {"version": matplotlib.__version__, + "parallel_read_safe": True, "parallel_write_safe": True} diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9751f7166f4a3587f6247aba6e0a1f8925ff8a67 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/conftest.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/conftest.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79ea3e2696227ac7a47e35dd4c6116c058c9efcb Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/conftest.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_afm.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_afm.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09da40f5c3ec73072d526e173818b11f77731723 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_afm.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_agg.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_agg.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6deba12ef17edb92b3feec6df867e1f827d62436 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_agg.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_agg_filter.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_agg_filter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..256c18d29f6107dd32faa1ab97bb4aeb262e08dc Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_agg_filter.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_animation.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_animation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6f0a87cdabf52cf37eb5d26660a0b48cc125504 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_animation.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_api.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_api.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff8c40dd6e2369afbb6d8bba1869ec5f40747e31 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_api.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_arrow_patches.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_arrow_patches.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..873c3bbab93eb98802fdbd0a8228bc99c26bb3c5 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_arrow_patches.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_artist.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_artist.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af8071deded08dec8897abd86aaef237b1fd2ab7 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_artist.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_axis.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_axis.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f233b599b94617e49c05b4c3c37d5eef33c6bcbd Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_axis.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_bases.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_bases.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..29baca12270aaf807e1021d78de582ac7cd4b6e5 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_bases.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_cairo.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_cairo.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..80933b5ea2367ac9e5641f43d513205a0b50b897 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_cairo.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_gtk3.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_gtk3.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78dcc28d1625658f236d4fda3b4d30953cc089e0 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_gtk3.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_inline.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_inline.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4725ca82ec29936c476dac02e45a917da4838b25 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_inline.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_macosx.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_macosx.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..49498a5ecd1a93ba0dbbd1c86cb582d68d73f5dd Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_macosx.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_nbagg.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_nbagg.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7cbaa3333b207d35b6b05b4892705340485a8a2c Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_nbagg.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_pdf.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_pdf.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8899a8ab71c44d2d5b8d1d4596e4911e271416a1 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_pdf.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_pgf.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_pgf.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0699adb854174fab5e7cdb7fde58852065fe95f9 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_pgf.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_ps.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_ps.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2543dfdd4ec9e4798909d7753def8d121bb4ea71 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_ps.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_qt.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_qt.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e2b3383cddf575416ca8dea265e39d432af29a5 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_qt.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_registry.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_registry.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cdcc78559beaa526e69511a108db4e55d4143a5f Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_registry.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_svg.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_svg.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3498939b7824ef5fd77493b86c49895d8b3f52e2 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_svg.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_template.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_template.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..462cb12170380cf5aa82efd5ff12520889f3e975 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_template.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_tk.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_tk.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4de68fd54f26f45707abbe7dce0d3c73868fbfb5 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_tk.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_tools.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_tools.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..460f0982ffb7a372c285155355c1831b674edf00 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_tools.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_webagg.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_webagg.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6cb7428177e8f4c00bf2c8c4d0f544ecff179326 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backend_webagg.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backends_interactive.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backends_interactive.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..245e071ba13d828905ff7b938d658efa13d2d838 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_backends_interactive.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_basic.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_basic.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9946e4feafb138f47e9b2820928d05d9f873b8be Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_basic.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_bbox_tight.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_bbox_tight.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f6d605ac465d2d2686a3dee7d746bb4dc5ff242 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_bbox_tight.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_bezier.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_bezier.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c9a51fe98db1ae58d08fde746263c91917ad196 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_bezier.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_category.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_category.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a11b9ec2373aac565e2363686d1d798c1772265 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_category.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_cbook.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_cbook.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..40e51b6dfcae9526199c0a6e755988bcbab7d2bf Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_cbook.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_collections.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_collections.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..23317a467de17c1f69e2f18fc5ee091b67f6db25 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_collections.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_colorbar.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_colorbar.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..985775852dbc5eb3799c39cf0bc8068a0fe1499d Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_colorbar.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_colors.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_colors.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aba359a273b67ec336748e460c8264d2e1215558 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_colors.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_compare_images.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_compare_images.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e263f3cd68b899377739a2b90e5b4bf188f10ba Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_compare_images.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_constrainedlayout.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_constrainedlayout.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3379f63cf0fd6088471157fc30e336a241dbd8cd Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_constrainedlayout.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_container.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_container.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..debd368bda91d0c82fa80ebbf2e68fe8dae1350e Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_container.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_contour.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_contour.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20266f5216946057fd63a843d5829ede100ff390 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_contour.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_cycles.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_cycles.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d42dd1cf3e7f1b9763e05d8ab76f940b8ac4d038 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_cycles.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_dates.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_dates.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b03fec66115544588236c2dcee929efc92ba15d Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_dates.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_datetime.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_datetime.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6f9cd2648cb7ac24de85b73388159d062824a7a Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_datetime.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_determinism.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_determinism.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da4fe19d76bd8d695b19c32a13a7c269dc8f3760 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_determinism.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_doc.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_doc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5923726da480a9b1a7dcd6bfd9158bac293fa50 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_doc.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_dviread.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_dviread.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..29b81cfda1cbd08e05a0da575246279d8791e992 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_dviread.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_font_manager.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_font_manager.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e5342e93bc115059d6e369e8795faf0d8a00ddf Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_font_manager.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_fontconfig_pattern.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_fontconfig_pattern.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ddf2f1f7b2f97e55dc6c357de2547cf46e4ee128 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_fontconfig_pattern.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_ft2font.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_ft2font.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b75b720fe611f69bb31b8762ee2a9d25b682896b Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_ft2font.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_getattr.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_getattr.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2c00a933c9d7004abbf319106ae01ae386d3407 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_getattr.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_gridspec.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_gridspec.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9777911ea423c56a36a1993a79c942b8dcf06008 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_gridspec.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_legend.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_legend.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..506ee51974bbc55b07ae8fcc17acb3b0196889ef Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_legend.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_lines.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_lines.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d8bff1a64ee4f130842c4468149dae7958c6f64f Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_lines.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_marker.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_marker.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a300eadd625ebf13caf71c72c64fd9b95f43896a Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_marker.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_mathtext.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_mathtext.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bbc4dd91ab66fbf2b7bd6d435e8da8dab831bc00 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_mathtext.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_matplotlib.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_matplotlib.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae4ad93b12f4808fb463f5a2c33a7e94afaba2d5 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_matplotlib.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_mlab.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_mlab.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e42d33d947c89e39de70c98da2bb3a2d3393b9a Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_mlab.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_multivariate_colormaps.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_multivariate_colormaps.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..008e107995a3145c2b8a9f3eec520e623badb300 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_multivariate_colormaps.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_offsetbox.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_offsetbox.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..73188e6e66a9a22a376f352ba9a087271fb139ec Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_offsetbox.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_patches.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_patches.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55f0fe8d001094f86663e072162c481806c175d7 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_patches.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_path.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_path.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35611a201025af1acb951dfd1e6737e117afddaf Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_path.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_patheffects.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_patheffects.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8e4a9fe12faba6d067c1fa4605017ccac956712 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_patheffects.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_pickle.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_pickle.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d0bb60cf067757f0302ce97b51a0690cb986094 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_pickle.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_png.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_png.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad232d3607d559b9143d21c72e330c8ead1d007d Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_png.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_polar.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_polar.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4fd9e8efef4a7a4e47bd3e9fc2f55511c4699bbb Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_polar.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_preprocess_data.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_preprocess_data.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b780056e4859a6b00b93d1eb405f1b896f3f07ca Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_preprocess_data.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_pyplot.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_pyplot.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22846858f9c0e1777a171181a47bb7a3c58a3087 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_pyplot.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_quiver.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_quiver.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f303aac7d1f4262b9a8c59dc38d4b267309aeda8 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_quiver.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_rcparams.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_rcparams.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81ec8f70dafaed5ce8b5d62ec5abc8aba4ef0fe5 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_rcparams.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_sankey.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_sankey.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..909a516dc2adfff1008db8d961b9e3aaf2ac05e2 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_sankey.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_scale.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_scale.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07cd039e9b6b65796f5ff4da01cea7c4707ec496 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_scale.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_simplification.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_simplification.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86f339968dd14dbd07997d5b20dde155d0c1b1cd Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_simplification.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_skew.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_skew.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cea0f6ac2cd4a9f31ee20249730b6fa9560f7dad Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_skew.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_sphinxext.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_sphinxext.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86dd1a8a96f9cf84ee62ba9bf46347f0ab87287b Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_sphinxext.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_spines.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_spines.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..40cd270c4cbb592ae73ccacc66ad9bbe45c60aec Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_spines.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_streamplot.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_streamplot.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..daa4c45e2e3fa83da4355a63c930d23dad09d06c Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_streamplot.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_style.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_style.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62538fa12ec17653b2aa395ee2c2fac6614235fe Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_style.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_subplots.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_subplots.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..12a79d3d02c1c4a11897a6d08f804a3e31640154 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_subplots.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_table.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_table.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ea61be5a22ec35311af9686948ba78e2edf3230 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_table.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_testing.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_testing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e098c3a581a5e169c6dda8d6b9730f18173e1052 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_testing.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_texmanager.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_texmanager.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ad548b784acda3a30dd22047d52d7012d8c08bb Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_texmanager.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_text.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_text.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..706f132356f7e012a8450c192e0b445277edeefa Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_text.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_textpath.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_textpath.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e24c27df091d888559a83a395b1c044259aa5fe3 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_textpath.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_tightlayout.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_tightlayout.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..314b79662908e5b79b739140b0bf0b96886262c2 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_tightlayout.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_transforms.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_transforms.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c7173f6e541d97ecccb7ced3032e03039b49bd48 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_transforms.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_triangulation.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_triangulation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..03025f78ca2360ac0ea3c495620826c071d8d801 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_triangulation.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_type1font.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_type1font.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b010f9916968939167f89f2bd36747e0ddcd90ac Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_type1font.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_units.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_units.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83970ea60acd910004a081a35ec39e913de5e2a3 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_units.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_usetex.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_usetex.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..155d78d8915458fa0b6318aded5b662e33a6efe6 Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_usetex.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_widgets.cpython-312.pyc b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_widgets.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..203d081c9c6201e1382b7e60c014790dcf881bcb Binary files /dev/null and b/lib/python3.12/site-packages/matplotlib/tests/__pycache__/test_widgets.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_axes.py b/lib/python3.12/site-packages/matplotlib/tests/test_axes.py new file mode 100644 index 0000000000000000000000000000000000000000..e3fc1ada99414eb5066f9cdc40b9c1e1c612dbd3 --- /dev/null +++ b/lib/python3.12/site-packages/matplotlib/tests/test_axes.py @@ -0,0 +1,9620 @@ +import contextlib +from collections import namedtuple, deque +import datetime +from decimal import Decimal +from functools import partial +import gc +import inspect +import io +from itertools import product +import platform +import sys +from types import SimpleNamespace + +import dateutil.tz + +import numpy as np +from numpy import ma +from cycler import cycler +import pytest + +import matplotlib +import matplotlib as mpl +from matplotlib import rc_context, patheffects +import matplotlib.colors as mcolors +import matplotlib.dates as mdates +from matplotlib.figure import Figure +from matplotlib.axes import Axes +from matplotlib.lines import Line2D +from matplotlib.collections import PathCollection +import matplotlib.font_manager as mfont_manager +import matplotlib.markers as mmarkers +import matplotlib.patches as mpatches +import matplotlib.path as mpath +from matplotlib.projections.geo import HammerAxes +from matplotlib.projections.polar import PolarAxes +import matplotlib.pyplot as plt +import matplotlib.text as mtext +import matplotlib.ticker as mticker +import matplotlib.transforms as mtransforms +import mpl_toolkits.axisartist as AA # type: ignore[import] +from numpy.testing import ( + assert_allclose, assert_array_equal, assert_array_almost_equal) +from matplotlib.testing.decorators import ( + image_comparison, check_figures_equal, remove_ticks_and_titles) +from matplotlib.testing._markers import needs_usetex + +# Note: Some test cases are run twice: once normally and once with labeled data +# These two must be defined in the same test function or need to have +# different baseline images to prevent race conditions when pytest runs +# the tests with multiple threads. + + +@check_figures_equal(extensions=["png"]) +def test_invisible_axes(fig_test, fig_ref): + ax = fig_test.subplots() + ax.set_visible(False) + + +def test_get_labels(): + fig, ax = plt.subplots() + ax.set_xlabel('x label') + ax.set_ylabel('y label') + assert ax.get_xlabel() == 'x label' + assert ax.get_ylabel() == 'y label' + + +def test_repr(): + fig, ax = plt.subplots() + ax.set_label('label') + ax.set_title('title') + ax.set_xlabel('x') + ax.set_ylabel('y') + assert repr(ax) == ( + "") + + +@check_figures_equal(extensions=['png']) +def test_label_loc_vertical(fig_test, fig_ref): + ax = fig_test.subplots() + sc = ax.scatter([1, 2], [1, 2], c=[1, 2], label='scatter') + ax.legend() + ax.set_ylabel('Y Label', loc='top') + ax.set_xlabel('X Label', loc='right') + cbar = fig_test.colorbar(sc) + cbar.set_label("Z Label", loc='top') + + ax = fig_ref.subplots() + sc = ax.scatter([1, 2], [1, 2], c=[1, 2], label='scatter') + ax.legend() + ax.set_ylabel('Y Label', y=1, ha='right') + ax.set_xlabel('X Label', x=1, ha='right') + cbar = fig_ref.colorbar(sc) + cbar.set_label("Z Label", y=1, ha='right') + + +@check_figures_equal(extensions=['png']) +def test_label_loc_horizontal(fig_test, fig_ref): + ax = fig_test.subplots() + sc = ax.scatter([1, 2], [1, 2], c=[1, 2], label='scatter') + ax.legend() + ax.set_ylabel('Y Label', loc='bottom') + ax.set_xlabel('X Label', loc='left') + cbar = fig_test.colorbar(sc, orientation='horizontal') + cbar.set_label("Z Label", loc='left') + + ax = fig_ref.subplots() + sc = ax.scatter([1, 2], [1, 2], c=[1, 2], label='scatter') + ax.legend() + ax.set_ylabel('Y Label', y=0, ha='left') + ax.set_xlabel('X Label', x=0, ha='left') + cbar = fig_ref.colorbar(sc, orientation='horizontal') + cbar.set_label("Z Label", x=0, ha='left') + + +@check_figures_equal(extensions=['png']) +def test_label_loc_rc(fig_test, fig_ref): + with matplotlib.rc_context({"xaxis.labellocation": "right", + "yaxis.labellocation": "top"}): + ax = fig_test.subplots() + sc = ax.scatter([1, 2], [1, 2], c=[1, 2], label='scatter') + ax.legend() + ax.set_ylabel('Y Label') + ax.set_xlabel('X Label') + cbar = fig_test.colorbar(sc, orientation='horizontal') + cbar.set_label("Z Label") + + ax = fig_ref.subplots() + sc = ax.scatter([1, 2], [1, 2], c=[1, 2], label='scatter') + ax.legend() + ax.set_ylabel('Y Label', y=1, ha='right') + ax.set_xlabel('X Label', x=1, ha='right') + cbar = fig_ref.colorbar(sc, orientation='horizontal') + cbar.set_label("Z Label", x=1, ha='right') + + +def test_label_shift(): + fig, ax = plt.subplots() + + # Test label re-centering on x-axis + ax.set_xlabel("Test label", loc="left") + ax.set_xlabel("Test label", loc="center") + assert ax.xaxis.label.get_horizontalalignment() == "center" + ax.set_xlabel("Test label", loc="right") + assert ax.xaxis.label.get_horizontalalignment() == "right" + ax.set_xlabel("Test label", loc="center") + assert ax.xaxis.label.get_horizontalalignment() == "center" + + # Test label re-centering on y-axis + ax.set_ylabel("Test label", loc="top") + ax.set_ylabel("Test label", loc="center") + assert ax.yaxis.label.get_horizontalalignment() == "center" + ax.set_ylabel("Test label", loc="bottom") + assert ax.yaxis.label.get_horizontalalignment() == "left" + ax.set_ylabel("Test label", loc="center") + assert ax.yaxis.label.get_horizontalalignment() == "center" + + +@check_figures_equal(extensions=["png"]) +def test_acorr(fig_test, fig_ref): + np.random.seed(19680801) + Nx = 512 + x = np.random.normal(0, 1, Nx).cumsum() + maxlags = Nx-1 + + ax_test = fig_test.subplots() + ax_test.acorr(x, maxlags=maxlags) + + ax_ref = fig_ref.subplots() + # Normalized autocorrelation + norm_auto_corr = np.correlate(x, x, mode="full")/np.dot(x, x) + lags = np.arange(-maxlags, maxlags+1) + norm_auto_corr = norm_auto_corr[Nx-1-maxlags:Nx+maxlags] + ax_ref.vlines(lags, [0], norm_auto_corr) + ax_ref.axhline(y=0, xmin=0, xmax=1) + + +@check_figures_equal(extensions=["png"]) +def test_acorr_integers(fig_test, fig_ref): + np.random.seed(19680801) + Nx = 51 + x = (np.random.rand(Nx) * 10).cumsum() + x = (np.ceil(x)).astype(np.int64) + maxlags = Nx-1 + + ax_test = fig_test.subplots() + ax_test.acorr(x, maxlags=maxlags) + + ax_ref = fig_ref.subplots() + + # Normalized autocorrelation + norm_auto_corr = np.correlate(x, x, mode="full")/np.dot(x, x) + lags = np.arange(-maxlags, maxlags+1) + norm_auto_corr = norm_auto_corr[Nx-1-maxlags:Nx+maxlags] + ax_ref.vlines(lags, [0], norm_auto_corr) + ax_ref.axhline(y=0, xmin=0, xmax=1) + + +@check_figures_equal(extensions=["png"]) +def test_spy(fig_test, fig_ref): + np.random.seed(19680801) + a = np.ones(32 * 32) + a[:16 * 32] = 0 + np.random.shuffle(a) + a = a.reshape((32, 32)) + + axs_test = fig_test.subplots(2) + axs_test[0].spy(a) + axs_test[1].spy(a, marker=".", origin="lower") + + axs_ref = fig_ref.subplots(2) + axs_ref[0].imshow(a, cmap="gray_r", interpolation="nearest") + axs_ref[0].xaxis.tick_top() + axs_ref[1].plot(*np.nonzero(a)[::-1], ".", markersize=10) + axs_ref[1].set( + aspect=1, xlim=axs_ref[0].get_xlim(), ylim=axs_ref[0].get_ylim()[::-1]) + for ax in axs_ref: + ax.xaxis.set_ticks_position("both") + + +def test_spy_invalid_kwargs(): + fig, ax = plt.subplots() + for unsupported_kw in [{'interpolation': 'nearest'}, + {'marker': 'o', 'linestyle': 'solid'}]: + with pytest.raises(TypeError): + ax.spy(np.eye(3, 3), **unsupported_kw) + + +@check_figures_equal(extensions=["png"]) +def test_matshow(fig_test, fig_ref): + mpl.style.use("mpl20") + a = np.random.rand(32, 32) + fig_test.add_subplot().matshow(a) + ax_ref = fig_ref.add_subplot() + ax_ref.imshow(a) + ax_ref.xaxis.tick_top() + ax_ref.xaxis.set_ticks_position('both') + + +@image_comparison([f'formatter_ticker_{i:03d}.png' for i in range(1, 6)], + tol=0 if platform.machine() == 'x86_64' else 0.031) +def test_formatter_ticker(): + import matplotlib.testing.jpl_units as units + units.register() + + # This should affect the tick size. (Tests issue #543) + matplotlib.rcParams['lines.markeredgewidth'] = 30 + + # This essentially test to see if user specified labels get overwritten + # by the auto labeler functionality of the axes. + xdata = [x*units.sec for x in range(10)] + ydata1 = [(1.5*y - 0.5)*units.km for y in range(10)] + ydata2 = [(1.75*y - 1.0)*units.km for y in range(10)] + + ax = plt.figure().subplots() + ax.set_xlabel("x-label 001") + + ax = plt.figure().subplots() + ax.set_xlabel("x-label 001") + ax.plot(xdata, ydata1, color='blue', xunits="sec") + + ax = plt.figure().subplots() + ax.set_xlabel("x-label 001") + ax.plot(xdata, ydata1, color='blue', xunits="sec") + ax.set_xlabel("x-label 003") + + ax = plt.figure().subplots() + ax.plot(xdata, ydata1, color='blue', xunits="sec") + ax.plot(xdata, ydata2, color='green', xunits="hour") + ax.set_xlabel("x-label 004") + + # See SF bug 2846058 + # https://sourceforge.net/tracker/?func=detail&aid=2846058&group_id=80706&atid=560720 + ax = plt.figure().subplots() + ax.plot(xdata, ydata1, color='blue', xunits="sec") + ax.plot(xdata, ydata2, color='green', xunits="hour") + ax.set_xlabel("x-label 005") + ax.autoscale_view() + + +def test_funcformatter_auto_formatter(): + def _formfunc(x, pos): + return '' + + ax = plt.figure().subplots() + + assert ax.xaxis.isDefault_majfmt + assert ax.xaxis.isDefault_minfmt + assert ax.yaxis.isDefault_majfmt + assert ax.yaxis.isDefault_minfmt + + ax.xaxis.set_major_formatter(_formfunc) + + assert not ax.xaxis.isDefault_majfmt + assert ax.xaxis.isDefault_minfmt + assert ax.yaxis.isDefault_majfmt + assert ax.yaxis.isDefault_minfmt + + targ_funcformatter = mticker.FuncFormatter(_formfunc) + + assert isinstance(ax.xaxis.get_major_formatter(), + mticker.FuncFormatter) + + assert ax.xaxis.get_major_formatter().func == targ_funcformatter.func + + +def test_strmethodformatter_auto_formatter(): + formstr = '{x}_{pos}' + + ax = plt.figure().subplots() + + assert ax.xaxis.isDefault_majfmt + assert ax.xaxis.isDefault_minfmt + assert ax.yaxis.isDefault_majfmt + assert ax.yaxis.isDefault_minfmt + + ax.yaxis.set_minor_formatter(formstr) + + assert ax.xaxis.isDefault_majfmt + assert ax.xaxis.isDefault_minfmt + assert ax.yaxis.isDefault_majfmt + assert not ax.yaxis.isDefault_minfmt + + targ_strformatter = mticker.StrMethodFormatter(formstr) + + assert isinstance(ax.yaxis.get_minor_formatter(), + mticker.StrMethodFormatter) + + assert ax.yaxis.get_minor_formatter().fmt == targ_strformatter.fmt + + +@image_comparison(["twin_axis_locators_formatters.png"]) +def test_twin_axis_locators_formatters(): + vals = np.linspace(0, 1, num=5, endpoint=True) + locs = np.sin(np.pi * vals / 2.0) + + majl = plt.FixedLocator(locs) + minl = plt.FixedLocator([0.1, 0.2, 0.3]) + + fig = plt.figure() + ax1 = fig.add_subplot(1, 1, 1) + ax1.plot([0.1, 100], [0, 1]) + ax1.yaxis.set_major_locator(majl) + ax1.yaxis.set_minor_locator(minl) + ax1.yaxis.set_major_formatter(plt.FormatStrFormatter('%08.2lf')) + ax1.yaxis.set_minor_formatter(plt.FixedFormatter(['tricks', 'mind', + 'jedi'])) + + ax1.xaxis.set_major_locator(plt.LinearLocator()) + ax1.xaxis.set_minor_locator(plt.FixedLocator([15, 35, 55, 75])) + ax1.xaxis.set_major_formatter(plt.FormatStrFormatter('%05.2lf')) + ax1.xaxis.set_minor_formatter(plt.FixedFormatter(['c', '3', 'p', 'o'])) + ax1.twiny() + ax1.twinx() + + +def test_twinx_cla(): + fig, ax = plt.subplots() + ax2 = ax.twinx() + ax3 = ax2.twiny() + plt.draw() + assert not ax2.xaxis.get_visible() + assert not ax2.patch.get_visible() + ax2.cla() + ax3.cla() + + assert not ax2.xaxis.get_visible() + assert not ax2.patch.get_visible() + assert ax2.yaxis.get_visible() + + assert ax3.xaxis.get_visible() + assert not ax3.patch.get_visible() + assert not ax3.yaxis.get_visible() + + assert ax.xaxis.get_visible() + assert ax.patch.get_visible() + assert ax.yaxis.get_visible() + + +@pytest.mark.parametrize('twin', ('x', 'y')) +def test_twin_units(twin): + axis_name = f'{twin}axis' + twin_func = f'twin{twin}' + + a = ['0', '1'] + b = ['a', 'b'] + + fig = Figure() + ax1 = fig.subplots() + ax1.plot(a, b) + assert getattr(ax1, axis_name).units is not None + ax2 = getattr(ax1, twin_func)() + assert getattr(ax2, axis_name).units is not None + assert getattr(ax2, axis_name).units is getattr(ax1, axis_name).units + + +@pytest.mark.parametrize('twin', ('x', 'y')) +@check_figures_equal(extensions=['png'], tol=0.19) +def test_twin_logscale(fig_test, fig_ref, twin): + twin_func = f'twin{twin}' # test twinx or twiny + set_scale = f'set_{twin}scale' + x = np.arange(1, 100) + + # Change scale after twinning. + ax_test = fig_test.add_subplot(2, 1, 1) + ax_twin = getattr(ax_test, twin_func)() + getattr(ax_test, set_scale)('log') + ax_twin.plot(x, x) + + # Twin after changing scale. + ax_test = fig_test.add_subplot(2, 1, 2) + getattr(ax_test, set_scale)('log') + ax_twin = getattr(ax_test, twin_func)() + ax_twin.plot(x, x) + + for i in [1, 2]: + ax_ref = fig_ref.add_subplot(2, 1, i) + getattr(ax_ref, set_scale)('log') + ax_ref.plot(x, x) + + # This is a hack because twinned Axes double-draw the frame. + # Remove this when that is fixed. + Path = matplotlib.path.Path + fig_ref.add_artist( + matplotlib.patches.PathPatch( + Path([[0, 0], [0, 1], + [0, 1], [1, 1], + [1, 1], [1, 0], + [1, 0], [0, 0]], + [Path.MOVETO, Path.LINETO] * 4), + transform=ax_ref.transAxes, + facecolor='none', + edgecolor=mpl.rcParams['axes.edgecolor'], + linewidth=mpl.rcParams['axes.linewidth'], + capstyle='projecting')) + + remove_ticks_and_titles(fig_test) + remove_ticks_and_titles(fig_ref) + + +@image_comparison(['twin_autoscale.png'], + tol=0 if platform.machine() == 'x86_64' else 0.009) +def test_twinx_axis_scales(): + x = np.array([0, 0.5, 1]) + y = 0.5 * x + x2 = np.array([0, 1, 2]) + y2 = 2 * x2 + + fig = plt.figure() + ax = fig.add_axes((0, 0, 1, 1), autoscalex_on=False, autoscaley_on=False) + ax.plot(x, y, color='blue', lw=10) + + ax2 = plt.twinx(ax) + ax2.plot(x2, y2, 'r--', lw=5) + + ax.margins(0, 0) + ax2.margins(0, 0) + + +def test_twin_inherit_autoscale_setting(): + fig, ax = plt.subplots() + ax_x_on = ax.twinx() + ax.set_autoscalex_on(False) + ax_x_off = ax.twinx() + + assert ax_x_on.get_autoscalex_on() + assert not ax_x_off.get_autoscalex_on() + + ax_y_on = ax.twiny() + ax.set_autoscaley_on(False) + ax_y_off = ax.twiny() + + assert ax_y_on.get_autoscaley_on() + assert not ax_y_off.get_autoscaley_on() + + +def test_inverted_cla(): + # GitHub PR #5450. Setting autoscale should reset + # axes to be non-inverted. + # plotting an image, then 1d graph, axis is now down + fig = plt.figure(0) + ax = fig.gca() + # 1. test that a new axis is not inverted per default + assert not ax.xaxis_inverted() + assert not ax.yaxis_inverted() + img = np.random.random((100, 100)) + ax.imshow(img) + # 2. test that a image axis is inverted + assert not ax.xaxis_inverted() + assert ax.yaxis_inverted() + # 3. test that clearing and plotting a line, axes are + # not inverted + ax.cla() + x = np.linspace(0, 2*np.pi, 100) + ax.plot(x, np.cos(x)) + assert not ax.xaxis_inverted() + assert not ax.yaxis_inverted() + + # 4. autoscaling should not bring back axes to normal + ax.cla() + ax.imshow(img) + plt.autoscale() + assert not ax.xaxis_inverted() + assert ax.yaxis_inverted() + + for ax in fig.axes: + ax.remove() + # 5. two shared axes. Inverting the leader axis should invert the shared + # axes; clearing the leader axis should bring axes in shared + # axes back to normal. + ax0 = plt.subplot(211) + ax1 = plt.subplot(212, sharey=ax0) + ax0.yaxis.set_inverted(True) + assert ax1.yaxis_inverted() + ax1.plot(x, np.cos(x)) + ax0.cla() + assert not ax1.yaxis_inverted() + ax1.cla() + # 6. clearing the follower should not touch limits + ax0.imshow(img) + ax1.plot(x, np.cos(x)) + ax1.cla() + assert ax.yaxis_inverted() + + # clean up + plt.close(fig) + + +def test_subclass_clear_cla(): + # Ensure that subclasses of Axes call cla/clear correctly. + # Note, we cannot use mocking here as we want to be sure that the + # superclass fallback does not recurse. + + with pytest.warns(PendingDeprecationWarning, + match='Overriding `Axes.cla`'): + class ClaAxes(Axes): + def cla(self): + nonlocal called + called = True + + with pytest.warns(PendingDeprecationWarning, + match='Overriding `Axes.cla`'): + class ClaSuperAxes(Axes): + def cla(self): + nonlocal called + called = True + super().cla() + + class SubClaAxes(ClaAxes): + pass + + class ClearAxes(Axes): + def clear(self): + nonlocal called + called = True + + class ClearSuperAxes(Axes): + def clear(self): + nonlocal called + called = True + super().clear() + + class SubClearAxes(ClearAxes): + pass + + fig = Figure() + for axes_class in [ClaAxes, ClaSuperAxes, SubClaAxes, + ClearAxes, ClearSuperAxes, SubClearAxes]: + called = False + ax = axes_class(fig, [0, 0, 1, 1]) + # Axes.__init__ has already called clear (which aliases to cla or is in + # the subclass). + assert called + + called = False + ax.cla() + assert called + + +def test_cla_not_redefined_internally(): + for klass in Axes.__subclasses__(): + # Check that cla does not get redefined in our Axes subclasses, except + # for in the above test function. + if 'test_subclass_clear_cla' not in klass.__qualname__: + assert 'cla' not in klass.__dict__ + + +@check_figures_equal(extensions=["png"]) +def test_minorticks_on_rcParams_both(fig_test, fig_ref): + with matplotlib.rc_context({"xtick.minor.visible": True, + "ytick.minor.visible": True}): + ax_test = fig_test.subplots() + ax_test.plot([0, 1], [0, 1]) + ax_ref = fig_ref.subplots() + ax_ref.plot([0, 1], [0, 1]) + ax_ref.minorticks_on() + + +@image_comparison(["autoscale_tiny_range.png"], remove_text=True) +def test_autoscale_tiny_range(): + # github pull #904 + fig, axs = plt.subplots(2, 2) + for i, ax in enumerate(axs.flat): + y1 = 10**(-11 - i) + ax.plot([0, 1], [1, 1 + y1]) + + +@mpl.style.context('default') +def test_autoscale_tight(): + fig, ax = plt.subplots(1, 1) + ax.plot([1, 2, 3, 4]) + ax.autoscale(enable=True, axis='x', tight=False) + ax.autoscale(enable=True, axis='y', tight=True) + assert_allclose(ax.get_xlim(), (-0.15, 3.15)) + assert_allclose(ax.get_ylim(), (1.0, 4.0)) + + # Check that autoscale is on + assert ax.get_autoscalex_on() + assert ax.get_autoscaley_on() + assert ax.get_autoscale_on() + # Set enable to None + ax.autoscale(enable=None) + # Same limits + assert_allclose(ax.get_xlim(), (-0.15, 3.15)) + assert_allclose(ax.get_ylim(), (1.0, 4.0)) + # autoscale still on + assert ax.get_autoscalex_on() + assert ax.get_autoscaley_on() + assert ax.get_autoscale_on() + + +@mpl.style.context('default') +def test_autoscale_log_shared(): + # related to github #7587 + # array starts at zero to trigger _minpos handling + x = np.arange(100, dtype=float) + fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True) + ax1.loglog(x, x) + ax2.semilogx(x, x) + ax1.autoscale(tight=True) + ax2.autoscale(tight=True) + plt.draw() + lims = (x[1], x[-1]) + assert_allclose(ax1.get_xlim(), lims) + assert_allclose(ax1.get_ylim(), lims) + assert_allclose(ax2.get_xlim(), lims) + assert_allclose(ax2.get_ylim(), (x[0], x[-1])) + + +@mpl.style.context('default') +def test_use_sticky_edges(): + fig, ax = plt.subplots() + ax.imshow([[0, 1], [2, 3]], origin='lower') + assert_allclose(ax.get_xlim(), (-0.5, 1.5)) + assert_allclose(ax.get_ylim(), (-0.5, 1.5)) + ax.use_sticky_edges = False + ax.autoscale() + xlim = (-0.5 - 2 * ax._xmargin, 1.5 + 2 * ax._xmargin) + ylim = (-0.5 - 2 * ax._ymargin, 1.5 + 2 * ax._ymargin) + assert_allclose(ax.get_xlim(), xlim) + assert_allclose(ax.get_ylim(), ylim) + # Make sure it is reversible: + ax.use_sticky_edges = True + ax.autoscale() + assert_allclose(ax.get_xlim(), (-0.5, 1.5)) + assert_allclose(ax.get_ylim(), (-0.5, 1.5)) + + +@check_figures_equal(extensions=["png"]) +def test_sticky_shared_axes(fig_test, fig_ref): + # Check that sticky edges work whether they are set in an Axes that is a + # "leader" in a share, or an Axes that is a "follower". + Z = np.arange(15).reshape(3, 5) + + ax0 = fig_test.add_subplot(211) + ax1 = fig_test.add_subplot(212, sharex=ax0) + ax1.pcolormesh(Z) + + ax0 = fig_ref.add_subplot(212) + ax1 = fig_ref.add_subplot(211, sharex=ax0) + ax0.pcolormesh(Z) + + +@image_comparison(['sticky_tolerance.png'], remove_text=True, style="mpl20") +def test_sticky_tolerance(): + fig, axs = plt.subplots(2, 2) + + width = .1 + + axs.flat[0].bar(x=0, height=width, bottom=20000.6) + axs.flat[0].bar(x=1, height=width, bottom=20000.1) + + axs.flat[1].bar(x=0, height=-width, bottom=20000.6) + axs.flat[1].bar(x=1, height=-width, bottom=20000.1) + + axs.flat[2].barh(y=0, width=-width, left=-20000.6) + axs.flat[2].barh(y=1, width=-width, left=-20000.1) + + axs.flat[3].barh(y=0, width=width, left=-20000.6) + axs.flat[3].barh(y=1, width=width, left=-20000.1) + + +@image_comparison(['sticky_tolerance_cf.png'], remove_text=True, style="mpl20") +def test_sticky_tolerance_contourf(): + fig, ax = plt.subplots() + + x = y = [14496.71, 14496.75] + data = [[0, 1], [2, 3]] + + ax.contourf(x, y, data) + + +def test_nargs_stem(): + with pytest.raises(TypeError, match='0 were given'): + # stem() takes 1-3 arguments. + plt.stem() + + +def test_nargs_legend(): + with pytest.raises(TypeError, match='3 were given'): + ax = plt.subplot() + # legend() takes 0-2 arguments. + ax.legend(['First'], ['Second'], 3) + + +def test_nargs_pcolorfast(): + with pytest.raises(TypeError, match='2 were given'): + ax = plt.subplot() + # pcolorfast() takes 1 or 3 arguments, + # not passing any arguments fails at C = args[-1] + # before nargs_err is raised. + ax.pcolorfast([(0, 1), (0, 2)], [[1, 2, 3], [1, 2, 3]]) + + +@image_comparison(['offset_points'], remove_text=True) +def test_basic_annotate(): + # Setup some data + t = np.arange(0.0, 5.0, 0.01) + s = np.cos(2.0*np.pi * t) + + # Offset Points + + fig = plt.figure() + ax = fig.add_subplot(autoscale_on=False, xlim=(-1, 5), ylim=(-3, 5)) + line, = ax.plot(t, s, lw=3, color='purple') + + ax.annotate('local max', xy=(3, 1), xycoords='data', + xytext=(3, 3), textcoords='offset points') + + +@image_comparison(['arrow_simple.png'], remove_text=True) +def test_arrow_simple(): + # Simple image test for ax.arrow + # kwargs that take discrete values + length_includes_head = (True, False) + shape = ('full', 'left', 'right') + head_starts_at_zero = (True, False) + # Create outer product of values + kwargs = product(length_includes_head, shape, head_starts_at_zero) + + fig, axs = plt.subplots(3, 4) + for i, (ax, kwarg) in enumerate(zip(axs.flat, kwargs)): + ax.set_xlim(-2, 2) + ax.set_ylim(-2, 2) + # Unpack kwargs + (length_includes_head, shape, head_starts_at_zero) = kwarg + theta = 2 * np.pi * i / 12 + # Draw arrow + ax.arrow(0, 0, np.sin(theta), np.cos(theta), + width=theta/100, + length_includes_head=length_includes_head, + shape=shape, + head_starts_at_zero=head_starts_at_zero, + head_width=theta / 10, + head_length=theta / 10) + + +def test_arrow_empty(): + _, ax = plt.subplots() + # Create an empty FancyArrow + ax.arrow(0, 0, 0, 0, head_length=0) + + +def test_arrow_in_view(): + _, ax = plt.subplots() + ax.arrow(1, 1, 1, 1) + assert ax.get_xlim() == (0.8, 2.2) + assert ax.get_ylim() == (0.8, 2.2) + + +def test_annotate_default_arrow(): + # Check that we can make an annotation arrow with only default properties. + fig, ax = plt.subplots() + ann = ax.annotate("foo", (0, 1), xytext=(2, 3)) + assert ann.arrow_patch is None + ann = ax.annotate("foo", (0, 1), xytext=(2, 3), arrowprops={}) + assert ann.arrow_patch is not None + + +def test_annotate_signature(): + """Check that the signature of Axes.annotate() matches Annotation.""" + fig, ax = plt.subplots() + annotate_params = inspect.signature(ax.annotate).parameters + annotation_params = inspect.signature(mtext.Annotation).parameters + assert list(annotate_params.keys()) == list(annotation_params.keys()) + for p1, p2 in zip(annotate_params.values(), annotation_params.values()): + assert p1 == p2 + + +@image_comparison(['fill_units.png'], savefig_kwarg={'dpi': 60}) +def test_fill_units(): + import matplotlib.testing.jpl_units as units + units.register() + + # generate some data + t = units.Epoch("ET", dt=datetime.datetime(2009, 4, 27)) + value = 10.0 * units.deg + day = units.Duration("ET", 24.0 * 60.0 * 60.0) + dt = np.arange('2009-04-27', '2009-04-29', dtype='datetime64[D]') + dtn = mdates.date2num(dt) + + fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2) + + ax1.plot([t], [value], yunits='deg', color='red') + ind = [0, 0, 1, 1] + ax1.fill(dtn[ind], [0.0, 0.0, 90.0, 0.0], 'b') + + ax2.plot([t], [value], yunits='deg', color='red') + ax2.fill([t, t, t + day, t + day], + [0.0, 0.0, 90.0, 0.0], 'b') + + ax3.plot([t], [value], yunits='deg', color='red') + ax3.fill(dtn[ind], + [0 * units.deg, 0 * units.deg, 90 * units.deg, 0 * units.deg], + 'b') + + ax4.plot([t], [value], yunits='deg', color='red') + ax4.fill([t, t, t + day, t + day], + [0 * units.deg, 0 * units.deg, 90 * units.deg, 0 * units.deg], + facecolor="blue") + fig.autofmt_xdate() + + +def test_plot_format_kwarg_redundant(): + with pytest.warns(UserWarning, match="marker .* redundantly defined"): + plt.plot([0], [0], 'o', marker='x') + with pytest.warns(UserWarning, match="linestyle .* redundantly defined"): + plt.plot([0], [0], '-', linestyle='--') + with pytest.warns(UserWarning, match="color .* redundantly defined"): + plt.plot([0], [0], 'r', color='blue') + # smoke-test: should not warn + plt.errorbar([0], [0], fmt='none', color='blue') + + +@check_figures_equal(extensions=["png"]) +def test_errorbar_dashes(fig_test, fig_ref): + x = [1, 2, 3, 4] + y = np.sin(x) + + ax_ref = fig_ref.gca() + ax_test = fig_test.gca() + + line, *_ = ax_ref.errorbar(x, y, xerr=np.abs(y), yerr=np.abs(y)) + line.set_dashes([2, 2]) + + ax_test.errorbar(x, y, xerr=np.abs(y), yerr=np.abs(y), dashes=[2, 2]) + + +def test_errorbar_mapview_kwarg(): + D = {ii: ii for ii in range(10)} + fig, ax = plt.subplots() + ax.errorbar(x=D.keys(), y=D.values(), xerr=D.values()) + + +@image_comparison(['single_point', 'single_point']) +def test_single_point(): + # Issue #1796: don't let lines.marker affect the grid + matplotlib.rcParams['lines.marker'] = 'o' + matplotlib.rcParams['axes.grid'] = True + + fig, (ax1, ax2) = plt.subplots(2) + ax1.plot([0], [0], 'o') + ax2.plot([1], [1], 'o') + + # Reuse testcase from above for a labeled data test + data = {'a': [0], 'b': [1]} + + fig, (ax1, ax2) = plt.subplots(2) + ax1.plot('a', 'a', 'o', data=data) + ax2.plot('b', 'b', 'o', data=data) + + +@image_comparison(['single_date.png'], style='mpl20') +def test_single_date(): + + # use former defaults to match existing baseline image + plt.rcParams['axes.formatter.limits'] = -7, 7 + dt = mdates.date2num(np.datetime64('0000-12-31')) + + time1 = [721964.0] + data1 = [-65.54] + + fig, ax = plt.subplots(2, 1) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + ax[0].plot_date(time1 + dt, data1, 'o', color='r') + ax[1].plot(time1, data1, 'o', color='r') + + +@check_figures_equal(extensions=["png"]) +def test_shaped_data(fig_test, fig_ref): + row = np.arange(10).reshape((1, -1)) + col = np.arange(0, 100, 10).reshape((-1, 1)) + + axs = fig_test.subplots(2) + axs[0].plot(row) # Actually plots nothing (columns are single points). + axs[1].plot(col) # Same as plotting 1d. + + axs = fig_ref.subplots(2) + # xlim from the implicit "x=0", ylim from the row datalim. + axs[0].set(xlim=(-.06, .06), ylim=(0, 9)) + axs[1].plot(col.ravel()) + + +def test_structured_data(): + # support for structured data + pts = np.array([(1, 1), (2, 2)], dtype=[("ones", float), ("twos", float)]) + + # this should not read second name as a format and raise ValueError + axs = plt.figure().subplots(2) + axs[0].plot("ones", "twos", data=pts) + axs[1].plot("ones", "twos", "r", data=pts) + + +@image_comparison(['aitoff_proj'], extensions=["png"], + remove_text=True, style='mpl20') +def test_aitoff_proj(): + """ + Test aitoff projection ref.: + https://github.com/matplotlib/matplotlib/pull/14451 + """ + x = np.linspace(-np.pi, np.pi, 20) + y = np.linspace(-np.pi / 2, np.pi / 2, 20) + X, Y = np.meshgrid(x, y) + + fig, ax = plt.subplots(figsize=(8, 4.2), + subplot_kw=dict(projection="aitoff")) + ax.grid() + ax.plot(X.flat, Y.flat, 'o', markersize=4) + + +@image_comparison(['axvspan_epoch.png']) +def test_axvspan_epoch(): + import matplotlib.testing.jpl_units as units + units.register() + + # generate some data + t0 = units.Epoch("ET", dt=datetime.datetime(2009, 1, 21)) + tf = units.Epoch("ET", dt=datetime.datetime(2009, 1, 22)) + dt = units.Duration("ET", units.day.convert("sec")) + + ax = plt.gca() + ax.axvspan(t0, tf, facecolor="blue", alpha=0.25) + ax.set_xlim(t0 - 5.0*dt, tf + 5.0*dt) + + +@image_comparison(['axhspan_epoch.png'], tol=0.02) +def test_axhspan_epoch(): + import matplotlib.testing.jpl_units as units + units.register() + + # generate some data + t0 = units.Epoch("ET", dt=datetime.datetime(2009, 1, 21)) + tf = units.Epoch("ET", dt=datetime.datetime(2009, 1, 22)) + dt = units.Duration("ET", units.day.convert("sec")) + + ax = plt.gca() + ax.axhspan(t0, tf, facecolor="blue", alpha=0.25) + ax.set_ylim(t0 - 5.0*dt, tf + 5.0*dt) + + +@image_comparison(['hexbin_extent.png', 'hexbin_extent.png'], remove_text=True) +def test_hexbin_extent(): + # this test exposes sf bug 2856228 + fig, ax = plt.subplots() + data = (np.arange(2000) / 2000).reshape((2, 1000)) + x, y = data + + ax.hexbin(x, y, extent=[.1, .3, .6, .7]) + + # Reuse testcase from above for a labeled data test + data = {"x": x, "y": y} + + fig, ax = plt.subplots() + ax.hexbin("x", "y", extent=[.1, .3, .6, .7], data=data) + + +def test_hexbin_bad_extents(): + fig, ax = plt.subplots() + data = (np.arange(20) / 20).reshape((2, 10)) + x, y = data + + with pytest.raises(ValueError, match="In extent, xmax must be greater than xmin"): + ax.hexbin(x, y, extent=(1, 0, 0, 1)) + + with pytest.raises(ValueError, match="In extent, ymax must be greater than ymin"): + ax.hexbin(x, y, extent=(0, 1, 1, 0)) + + +def test_hexbin_string_norm(): + fig, ax = plt.subplots() + hex = ax.hexbin(np.random.rand(10), np.random.rand(10), norm="log", vmin=2, vmax=5) + assert isinstance(hex, matplotlib.collections.PolyCollection) + assert isinstance(hex.norm, matplotlib.colors.LogNorm) + assert hex.norm.vmin == 2 + assert hex.norm.vmax == 5 + + +@image_comparison(['hexbin_empty.png'], remove_text=True) +def test_hexbin_empty(): + # From #3886: creating hexbin from empty dataset raises ValueError + fig, ax = plt.subplots() + ax.hexbin([], []) + # From #23922: creating hexbin with log scaling from empty + # dataset raises ValueError + ax.hexbin([], [], bins='log') + # From #27103: np.max errors when handed empty data + ax.hexbin([], [], C=[], reduce_C_function=np.max) + # No string-comparison warning from NumPy. + ax.hexbin([], [], bins=np.arange(10)) + + +def test_hexbin_pickable(): + # From #1973: Test that picking a hexbin collection works + fig, ax = plt.subplots() + data = (np.arange(200) / 200).reshape((2, 100)) + x, y = data + hb = ax.hexbin(x, y, extent=[.1, .3, .6, .7], picker=-1) + mouse_event = SimpleNamespace(x=400, y=300) + assert hb.contains(mouse_event)[0] + + +@image_comparison(['hexbin_log.png'], style='mpl20') +def test_hexbin_log(): + # Issue #1636 (and also test log scaled colorbar) + + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + + np.random.seed(19680801) + n = 100000 + x = np.random.standard_normal(n) + y = 2.0 + 3.0 * x + 4.0 * np.random.standard_normal(n) + y = np.power(2, y * 0.5) + + fig, ax = plt.subplots() + h = ax.hexbin(x, y, yscale='log', bins='log', + marginals=True, reduce_C_function=np.sum) + plt.colorbar(h) + + # Make sure offsets are set + assert h.get_offsets().shape == (11558, 2) + + +def test_hexbin_log_offsets(): + x = np.geomspace(1, 100, 500) + + fig, ax = plt.subplots() + h = ax.hexbin(x, x, xscale='log', yscale='log', gridsize=2) + np.testing.assert_almost_equal( + h.get_offsets(), + np.array( + [[0, 0], + [0, 2], + [1, 0], + [1, 2], + [2, 0], + [2, 2], + [0.5, 1], + [1.5, 1]])) + + +@image_comparison(["hexbin_linear.png"], style="mpl20", remove_text=True) +def test_hexbin_linear(): + # Issue #21165 + np.random.seed(19680801) + n = 100000 + x = np.random.standard_normal(n) + y = 2.0 + 3.0 * x + 4.0 * np.random.standard_normal(n) + + fig, ax = plt.subplots() + ax.hexbin(x, y, gridsize=(10, 5), marginals=True, + reduce_C_function=np.sum) + + +def test_hexbin_log_clim(): + x, y = np.arange(200).reshape((2, 100)) + fig, ax = plt.subplots() + h = ax.hexbin(x, y, bins='log', vmin=2, vmax=100) + assert h.get_clim() == (2, 100) + + +@check_figures_equal(extensions=['png']) +def test_hexbin_mincnt_behavior_upon_C_parameter(fig_test, fig_ref): + # see: gh:12926 + datapoints = [ + # list of (x, y) + (0, 0), + (0, 0), + (6, 0), + (0, 6), + ] + X, Y = zip(*datapoints) + C = [1] * len(X) + extent = [-10., 10, -10., 10] + gridsize = (7, 7) + + ax_test = fig_test.subplots() + ax_ref = fig_ref.subplots() + + # without C parameter + ax_ref.hexbin( + X, Y, + extent=extent, + gridsize=gridsize, + mincnt=1, + ) + ax_ref.set_facecolor("green") # for contrast of background + + # with C parameter + ax_test.hexbin( + X, Y, + C=[1] * len(X), + reduce_C_function=lambda v: sum(v), + mincnt=1, + extent=extent, + gridsize=gridsize, + ) + ax_test.set_facecolor("green") + + +def test_inverted_limits(): + # Test gh:1553 + # Calling invert_xaxis prior to plotting should not disable autoscaling + # while still maintaining the inverted direction + fig, ax = plt.subplots() + ax.invert_xaxis() + ax.plot([-5, -3, 2, 4], [1, 2, -3, 5]) + + assert ax.get_xlim() == (4, -5) + assert ax.get_ylim() == (-3, 5) + plt.close() + + fig, ax = plt.subplots() + ax.invert_yaxis() + ax.plot([-5, -3, 2, 4], [1, 2, -3, 5]) + + assert ax.get_xlim() == (-5, 4) + assert ax.get_ylim() == (5, -3) + + # Test inverting nonlinear axes. + fig, ax = plt.subplots() + ax.set_yscale("log") + ax.set_ylim(10, 1) + assert ax.get_ylim() == (10, 1) + + +@image_comparison(['nonfinite_limits']) +def test_nonfinite_limits(): + x = np.arange(0., np.e, 0.01) + # silence divide by zero warning from log(0) + with np.errstate(divide='ignore'): + y = np.log(x) + x[len(x)//2] = np.nan + fig, ax = plt.subplots() + ax.plot(x, y) + + +@mpl.style.context('default') +@pytest.mark.parametrize('plot_fun', + ['scatter', 'plot', 'fill_between']) +@check_figures_equal(extensions=["png"]) +def test_limits_empty_data(plot_fun, fig_test, fig_ref): + # Check that plotting empty data doesn't change autoscaling of dates + x = np.arange("2010-01-01", "2011-01-01", dtype="datetime64[D]") + + ax_test = fig_test.subplots() + ax_ref = fig_ref.subplots() + + getattr(ax_test, plot_fun)([], []) + + for ax in [ax_test, ax_ref]: + getattr(ax, plot_fun)(x, range(len(x)), color='C0') + + +@image_comparison(['imshow', 'imshow'], remove_text=True, style='mpl20') +def test_imshow(): + # use former defaults to match existing baseline image + matplotlib.rcParams['image.interpolation'] = 'nearest' + # Create a NxN image + N = 100 + (x, y) = np.indices((N, N)) + x -= N//2 + y -= N//2 + r = np.sqrt(x**2+y**2-x*y) + + # Create a contour plot at N/4 and extract both the clip path and transform + fig, ax = plt.subplots() + ax.imshow(r) + + # Reuse testcase from above for a labeled data test + data = {"r": r} + fig, ax = plt.subplots() + ax.imshow("r", data=data) + + +@image_comparison(['imshow_clip'], style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 1.24) +def test_imshow_clip(): + # As originally reported by Gellule Xg + # use former defaults to match existing baseline image + matplotlib.rcParams['image.interpolation'] = 'nearest' + + # Create a NxN image + N = 100 + (x, y) = np.indices((N, N)) + x -= N//2 + y -= N//2 + r = np.sqrt(x**2+y**2-x*y) + + # Create a contour plot at N/4 and extract both the clip path and transform + fig, ax = plt.subplots() + + c = ax.contour(r, [N/4]) + clip_path = mtransforms.TransformedPath(c.get_paths()[0], c.get_transform()) + + # Plot the image clipped by the contour + ax.imshow(r, clip_path=clip_path) + + +def test_imshow_norm_vminvmax(): + """Parameters vmin, vmax should error if norm is given.""" + a = [[1, 2], [3, 4]] + ax = plt.axes() + with pytest.raises(ValueError, + match="Passing a Normalize instance simultaneously " + "with vmin/vmax is not supported."): + ax.imshow(a, norm=mcolors.Normalize(-10, 10), vmin=0, vmax=5) + + +@image_comparison(['polycollection_joinstyle'], remove_text=True) +def test_polycollection_joinstyle(): + # Bug #2890979 reported by Matthew West + fig, ax = plt.subplots() + verts = np.array([[1, 1], [1, 2], [2, 2], [2, 1]]) + c = mpl.collections.PolyCollection([verts], linewidths=40) + ax.add_collection(c) + ax.set_xbound(0, 3) + ax.set_ybound(0, 3) + + +@pytest.mark.parametrize( + 'x, y1, y2', [ + (np.zeros((2, 2)), 3, 3), + (np.arange(0.0, 2, 0.02), np.zeros((2, 2)), 3), + (np.arange(0.0, 2, 0.02), 3, np.zeros((2, 2))) + ], ids=[ + '2d_x_input', + '2d_y1_input', + '2d_y2_input' + ] +) +def test_fill_between_input(x, y1, y2): + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.fill_between(x, y1, y2) + + +@pytest.mark.parametrize( + 'y, x1, x2', [ + (np.zeros((2, 2)), 3, 3), + (np.arange(0.0, 2, 0.02), np.zeros((2, 2)), 3), + (np.arange(0.0, 2, 0.02), 3, np.zeros((2, 2))) + ], ids=[ + '2d_y_input', + '2d_x1_input', + '2d_x2_input' + ] +) +def test_fill_betweenx_input(y, x1, x2): + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.fill_betweenx(y, x1, x2) + + +@image_comparison(['fill_between_interpolate.png'], remove_text=True, + tol=0 if platform.machine() == 'x86_64' else 0.012) +def test_fill_between_interpolate(): + x = np.arange(0.0, 2, 0.02) + y1 = np.sin(2*np.pi*x) + y2 = 1.2*np.sin(4*np.pi*x) + + fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True) + ax1.plot(x, y1, x, y2, color='black') + ax1.fill_between(x, y1, y2, where=y2 >= y1, facecolor='white', hatch='/', + interpolate=True) + ax1.fill_between(x, y1, y2, where=y2 <= y1, facecolor='red', + interpolate=True) + + # Test support for masked arrays. + y2 = np.ma.masked_greater(y2, 1.0) + # Test that plotting works for masked arrays with the first element masked + y2[0] = np.ma.masked + ax2.plot(x, y1, x, y2, color='black') + ax2.fill_between(x, y1, y2, where=y2 >= y1, facecolor='green', + interpolate=True) + ax2.fill_between(x, y1, y2, where=y2 <= y1, facecolor='red', + interpolate=True) + + +@image_comparison(['fill_between_interpolate_decreasing.png'], + style='mpl20', remove_text=True) +def test_fill_between_interpolate_decreasing(): + p = np.array([724.3, 700, 655]) + t = np.array([9.4, 7, 2.2]) + prof = np.array([7.9, 6.6, 3.8]) + + fig, ax = plt.subplots(figsize=(9, 9)) + + ax.plot(t, p, 'tab:red') + ax.plot(prof, p, 'k') + + ax.fill_betweenx(p, t, prof, where=prof < t, + facecolor='blue', interpolate=True, alpha=0.4) + ax.fill_betweenx(p, t, prof, where=prof > t, + facecolor='red', interpolate=True, alpha=0.4) + + ax.set_xlim(0, 30) + ax.set_ylim(800, 600) + + +@image_comparison(['fill_between_interpolate_nan.png'], remove_text=True) +def test_fill_between_interpolate_nan(): + # Tests fix for issue #18986. + x = np.arange(10) + y1 = np.asarray([8, 18, np.nan, 18, 8, 18, 24, 18, 8, 18]) + y2 = np.asarray([18, 11, 8, 11, 18, 26, 32, 30, np.nan, np.nan]) + + fig, ax = plt.subplots() + + ax.plot(x, y1, c='k') + ax.plot(x, y2, c='b') + ax.fill_between(x, y1, y2, where=y2 >= y1, facecolor="green", + interpolate=True, alpha=0.5) + ax.fill_between(x, y1, y2, where=y1 >= y2, facecolor="red", + interpolate=True, alpha=0.5) + + +# test_symlog and test_symlog2 used to have baseline images in all three +# formats, but the png and svg baselines got invalidated by the removal of +# minor tick overstriking. +@image_comparison(['symlog.pdf']) +def test_symlog(): + x = np.array([0, 1, 2, 4, 6, 9, 12, 24]) + y = np.array([1000000, 500000, 100000, 100, 5, 0, 0, 0]) + + fig, ax = plt.subplots() + ax.plot(x, y) + ax.set_yscale('symlog') + ax.set_xscale('linear') + ax.set_ylim(-1, 10000000) + + +@image_comparison(['symlog2.pdf'], remove_text=True) +def test_symlog2(): + # Numbers from -50 to 50, with 0.1 as step + x = np.arange(-50, 50, 0.001) + + fig, axs = plt.subplots(5, 1) + for ax, linthresh in zip(axs, [20., 2., 1., 0.1, 0.01]): + ax.plot(x, x) + ax.set_xscale('symlog', linthresh=linthresh) + ax.grid(True) + axs[-1].set_ylim(-0.1, 0.1) + + +def test_pcolorargs_5205(): + # Smoketest to catch issue found in gh:5205 + x = [-1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5] + y = [-1.5, -1.25, -1.0, -0.75, -0.5, -0.25, 0, + 0.25, 0.5, 0.75, 1.0, 1.25, 1.5] + X, Y = np.meshgrid(x, y) + Z = np.hypot(X, Y) + + plt.pcolor(Z) + plt.pcolor(list(Z)) + plt.pcolor(x, y, Z[:-1, :-1]) + plt.pcolor(X, Y, list(Z[:-1, :-1])) + + +@image_comparison(['pcolormesh'], remove_text=True) +def test_pcolormesh(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + + n = 12 + x = np.linspace(-1.5, 1.5, n) + y = np.linspace(-1.5, 1.5, n*2) + X, Y = np.meshgrid(x, y) + Qx = np.cos(Y) - np.cos(X) + Qz = np.sin(Y) + np.sin(X) + Qx = (Qx + 1.1) + Z = np.hypot(X, Y) / 5 + Z = (Z - Z.min()) / np.ptp(Z) + + # The color array can include masked values: + Zm = ma.masked_where(np.abs(Qz) < 0.5 * np.max(Qz), Z) + + _, (ax1, ax2, ax3) = plt.subplots(1, 3) + ax1.pcolormesh(Qx, Qz, Zm[:-1, :-1], lw=0.5, edgecolors='k') + ax2.pcolormesh(Qx, Qz, Zm[:-1, :-1], lw=2, edgecolors=['b', 'w']) + ax3.pcolormesh(Qx, Qz, Zm, shading="gouraud") + + +@image_comparison(['pcolormesh_small'], extensions=["eps"]) +def test_pcolormesh_small(): + n = 3 + x = np.linspace(-1.5, 1.5, n) + y = np.linspace(-1.5, 1.5, n*2) + X, Y = np.meshgrid(x, y) + Qx = np.cos(Y) - np.cos(X) + Qz = np.sin(Y) + np.sin(X) + Qx = (Qx + 1.1) + Z = np.hypot(X, Y) / 5 + Z = (Z - Z.min()) / np.ptp(Z) + Zm = ma.masked_where(np.abs(Qz) < 0.5 * np.max(Qz), Z) + Zm2 = ma.masked_where(Qz < -0.5 * np.max(Qz), Z) + + fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2) + ax1.pcolormesh(Qx, Qz, Zm[:-1, :-1], lw=0.5, edgecolors='k') + ax2.pcolormesh(Qx, Qz, Zm[:-1, :-1], lw=2, edgecolors=['b', 'w']) + # gouraud with Zm yields a blank plot; there are no unmasked triangles. + ax3.pcolormesh(Qx, Qz, Zm, shading="gouraud") + # Reduce the masking to get a plot. + ax4.pcolormesh(Qx, Qz, Zm2, shading="gouraud") + + for ax in fig.axes: + ax.set_axis_off() + + +@image_comparison(['pcolormesh_alpha'], extensions=["png", "pdf"], + remove_text=True) +def test_pcolormesh_alpha(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + + n = 12 + X, Y = np.meshgrid( + np.linspace(-1.5, 1.5, n), + np.linspace(-1.5, 1.5, n*2) + ) + Qx = X + Qy = Y + np.sin(X) + Z = np.hypot(X, Y) / 5 + Z = (Z - Z.min()) / np.ptp(Z) + vir = mpl.colormaps["viridis"].resampled(16) + # make another colormap with varying alpha + colors = vir(np.arange(16)) + colors[:, 3] = 0.5 + 0.5*np.sin(np.arange(16)) + cmap = mcolors.ListedColormap(colors) + + fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2) + for ax in ax1, ax2, ax3, ax4: + ax.add_patch(mpatches.Rectangle( + (0, -1.5), 1.5, 3, facecolor=[.7, .1, .1, .5], zorder=0 + )) + # ax1, ax2: constant alpha + ax1.pcolormesh(Qx, Qy, Z[:-1, :-1], cmap=vir, alpha=0.4, + shading='flat', zorder=1) + ax2.pcolormesh(Qx, Qy, Z, cmap=vir, alpha=0.4, shading='gouraud', zorder=1) + # ax3, ax4: alpha from colormap + ax3.pcolormesh(Qx, Qy, Z[:-1, :-1], cmap=cmap, shading='flat', zorder=1) + ax4.pcolormesh(Qx, Qy, Z, cmap=cmap, shading='gouraud', zorder=1) + + +@pytest.mark.parametrize("dims,alpha", [(3, 1), (4, 0.5)]) +@check_figures_equal(extensions=["png"]) +def test_pcolormesh_rgba(fig_test, fig_ref, dims, alpha): + ax = fig_test.subplots() + c = np.ones((5, 6, dims), dtype=float) / 2 + ax.pcolormesh(c) + + ax = fig_ref.subplots() + ax.pcolormesh(c[..., 0], cmap="gray", vmin=0, vmax=1, alpha=alpha) + + +@check_figures_equal(extensions=["png"]) +def test_pcolormesh_nearest_noargs(fig_test, fig_ref): + x = np.arange(4) + y = np.arange(7) + X, Y = np.meshgrid(x, y) + C = X + Y + + ax = fig_test.subplots() + ax.pcolormesh(C, shading="nearest") + + ax = fig_ref.subplots() + ax.pcolormesh(x, y, C, shading="nearest") + + +@image_comparison(['pcolormesh_datetime_axis.png'], style='mpl20') +def test_pcolormesh_datetime_axis(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + + fig = plt.figure() + fig.subplots_adjust(hspace=0.4, top=0.98, bottom=.15) + base = datetime.datetime(2013, 1, 1) + x = np.array([base + datetime.timedelta(days=d) for d in range(21)]) + y = np.arange(21) + z1, z2 = np.meshgrid(np.arange(20), np.arange(20)) + z = z1 * z2 + plt.subplot(221) + plt.pcolormesh(x[:-1], y[:-1], z[:-1, :-1]) + plt.subplot(222) + plt.pcolormesh(x, y, z) + x = np.repeat(x[np.newaxis], 21, axis=0) + y = np.repeat(y[:, np.newaxis], 21, axis=1) + plt.subplot(223) + plt.pcolormesh(x[:-1, :-1], y[:-1, :-1], z[:-1, :-1]) + plt.subplot(224) + plt.pcolormesh(x, y, z) + for ax in fig.get_axes(): + for label in ax.get_xticklabels(): + label.set_ha('right') + label.set_rotation(30) + + +@image_comparison(['pcolor_datetime_axis.png'], style='mpl20') +def test_pcolor_datetime_axis(): + fig = plt.figure() + fig.subplots_adjust(hspace=0.4, top=0.98, bottom=.15) + base = datetime.datetime(2013, 1, 1) + x = np.array([base + datetime.timedelta(days=d) for d in range(21)]) + y = np.arange(21) + z1, z2 = np.meshgrid(np.arange(20), np.arange(20)) + z = z1 * z2 + plt.subplot(221) + plt.pcolor(x[:-1], y[:-1], z[:-1, :-1]) + plt.subplot(222) + plt.pcolor(x, y, z) + x = np.repeat(x[np.newaxis], 21, axis=0) + y = np.repeat(y[:, np.newaxis], 21, axis=1) + plt.subplot(223) + plt.pcolor(x[:-1, :-1], y[:-1, :-1], z[:-1, :-1]) + plt.subplot(224) + plt.pcolor(x, y, z) + for ax in fig.get_axes(): + for label in ax.get_xticklabels(): + label.set_ha('right') + label.set_rotation(30) + + +def test_pcolorargs(): + n = 12 + x = np.linspace(-1.5, 1.5, n) + y = np.linspace(-1.5, 1.5, n*2) + X, Y = np.meshgrid(x, y) + Z = np.hypot(X, Y) / 5 + + _, ax = plt.subplots() + with pytest.raises(TypeError): + ax.pcolormesh(y, x, Z) + with pytest.raises(TypeError): + ax.pcolormesh(X, Y, Z.T) + with pytest.raises(TypeError): + ax.pcolormesh(x, y, Z[:-1, :-1], shading="gouraud") + with pytest.raises(TypeError): + ax.pcolormesh(X, Y, Z[:-1, :-1], shading="gouraud") + x[0] = np.nan + with pytest.raises(ValueError): + ax.pcolormesh(x, y, Z[:-1, :-1]) + with np.errstate(invalid='ignore'): + x = np.ma.array(x, mask=(x < 0)) + with pytest.raises(ValueError): + ax.pcolormesh(x, y, Z[:-1, :-1]) + # If the X or Y coords do not possess monotonicity in their respective + # directions, a warning indicating a bad grid will be triggered. + # The case of specifying coordinates by inputting 1D arrays. + x = [359, 0, 1] + y = [-10, 10] + X, Y = np.meshgrid(x, y) + Z = np.zeros(X.shape) + with pytest.warns(UserWarning, + match='are not monotonically increasing or decreasing'): + ax.pcolormesh(X, Y, Z, shading='auto') + # The case of specifying coordinates by inputting 2D arrays. + x = np.linspace(-1, 1, 3) + y = np.linspace(-1, 1, 3) + X, Y = np.meshgrid(x, y) + Z = np.zeros(X.shape) + np.random.seed(19680801) + noise_X = np.random.random(X.shape) + noise_Y = np.random.random(Y.shape) + with pytest.warns(UserWarning, + match='are not monotonically increasing or ' + 'decreasing') as record: + # Small perturbations in coordinates will not disrupt the monotonicity + # of the X-coords and Y-coords in their respective directions. + # Therefore, no warnings will be triggered. + ax.pcolormesh(X+noise_X, Y+noise_Y, Z, shading='auto') + assert len(record) == 0 + # Large perturbations have disrupted the monotonicity of the X-coords + # and Y-coords in their respective directions, thus resulting in two + # bad grid warnings. + ax.pcolormesh(X+10*noise_X, Y+10*noise_Y, Z, shading='auto') + assert len(record) == 2 + + +def test_pcolormesh_underflow_error(): + """ + Test that underflow errors don't crop up in pcolormesh. Probably + a numpy bug (https://github.com/numpy/numpy/issues/25810). + """ + with np.errstate(under="raise"): + x = np.arange(0, 3, 0.1) + y = np.arange(0, 6, 0.1) + z = np.random.randn(len(y), len(x)) + fig, ax = plt.subplots() + ax.pcolormesh(x, y, z) + + +def test_pcolorargs_with_read_only(): + x = np.arange(6).reshape(2, 3) + xmask = np.broadcast_to([False, True, False], x.shape) # read-only array + assert xmask.flags.writeable is False + masked_x = np.ma.array(x, mask=xmask) + plt.pcolormesh(masked_x) + + x = np.linspace(0, 1, 10) + y = np.linspace(0, 1, 10) + X, Y = np.meshgrid(x, y) + Z = np.sin(2 * np.pi * X) * np.cos(2 * np.pi * Y) + mask = np.zeros(10, dtype=bool) + mask[-1] = True + mask = np.broadcast_to(mask, Z.shape) + assert mask.flags.writeable is False + masked_Z = np.ma.array(Z, mask=mask) + plt.pcolormesh(X, Y, masked_Z) + + masked_X = np.ma.array(X, mask=mask) + masked_Y = np.ma.array(Y, mask=mask) + plt.pcolor(masked_X, masked_Y, masked_Z) + + +@check_figures_equal(extensions=["png"]) +def test_pcolornearest(fig_test, fig_ref): + ax = fig_test.subplots() + x = np.arange(0, 10) + y = np.arange(0, 3) + np.random.seed(19680801) + Z = np.random.randn(2, 9) + ax.pcolormesh(x, y, Z, shading='flat') + + ax = fig_ref.subplots() + # specify the centers + x2 = x[:-1] + np.diff(x) / 2 + y2 = y[:-1] + np.diff(y) / 2 + ax.pcolormesh(x2, y2, Z, shading='nearest') + + +@check_figures_equal(extensions=["png"]) +def test_pcolornearestunits(fig_test, fig_ref): + ax = fig_test.subplots() + x = [datetime.datetime.fromtimestamp(x * 3600) for x in range(10)] + y = np.arange(0, 3) + np.random.seed(19680801) + Z = np.random.randn(2, 9) + ax.pcolormesh(x, y, Z, shading='flat') + + ax = fig_ref.subplots() + # specify the centers + x2 = [datetime.datetime.fromtimestamp((x + 0.5) * 3600) for x in range(9)] + y2 = y[:-1] + np.diff(y) / 2 + ax.pcolormesh(x2, y2, Z, shading='nearest') + + +def test_pcolorflaterror(): + fig, ax = plt.subplots() + x = np.arange(0, 9) + y = np.arange(0, 3) + np.random.seed(19680801) + Z = np.random.randn(3, 9) + with pytest.raises(TypeError, match='Dimensions of C'): + ax.pcolormesh(x, y, Z, shading='flat') + + +def test_samesizepcolorflaterror(): + fig, ax = plt.subplots() + x, y = np.meshgrid(np.arange(5), np.arange(3)) + Z = x + y + with pytest.raises(TypeError, match=r".*one smaller than X"): + ax.pcolormesh(x, y, Z, shading='flat') + + +@pytest.mark.parametrize('snap', [False, True]) +@check_figures_equal(extensions=["png"]) +def test_pcolorauto(fig_test, fig_ref, snap): + ax = fig_test.subplots() + x = np.arange(0, 10) + y = np.arange(0, 4) + np.random.seed(19680801) + Z = np.random.randn(3, 9) + # this is the same as flat; note that auto is default + ax.pcolormesh(x, y, Z, snap=snap) + + ax = fig_ref.subplots() + # specify the centers + x2 = x[:-1] + np.diff(x) / 2 + y2 = y[:-1] + np.diff(y) / 2 + # this is same as nearest: + ax.pcolormesh(x2, y2, Z, snap=snap) + + +@image_comparison(['canonical'], + tol=0 if platform.machine() == 'x86_64' else 0.02) +def test_canonical(): + fig, ax = plt.subplots() + ax.plot([1, 2, 3]) + + +@image_comparison(['arc_angles.png'], remove_text=True, style='default') +def test_arc_angles(): + # Ellipse parameters + w = 2 + h = 1 + centre = (0.2, 0.5) + scale = 2 + + fig, axs = plt.subplots(3, 3) + for i, ax in enumerate(axs.flat): + theta2 = i * 360 / 9 + theta1 = theta2 - 45 + + ax.add_patch(mpatches.Ellipse(centre, w, h, alpha=0.3)) + ax.add_patch(mpatches.Arc(centre, w, h, theta1=theta1, theta2=theta2)) + # Straight lines intersecting start and end of arc + ax.plot([scale * np.cos(np.deg2rad(theta1)) + centre[0], + centre[0], + scale * np.cos(np.deg2rad(theta2)) + centre[0]], + [scale * np.sin(np.deg2rad(theta1)) + centre[1], + centre[1], + scale * np.sin(np.deg2rad(theta2)) + centre[1]]) + + ax.set_xlim(-scale, scale) + ax.set_ylim(-scale, scale) + + # This looks the same, but it triggers a different code path when it + # gets large enough. + w *= 10 + h *= 10 + centre = (centre[0] * 10, centre[1] * 10) + scale *= 10 + + +@image_comparison(['arc_ellipse'], remove_text=True) +def test_arc_ellipse(): + xcenter, ycenter = 0.38, 0.52 + width, height = 1e-1, 3e-1 + angle = -30 + + theta = np.deg2rad(np.arange(360)) + x = width / 2. * np.cos(theta) + y = height / 2. * np.sin(theta) + + rtheta = np.deg2rad(angle) + R = np.array([ + [np.cos(rtheta), -np.sin(rtheta)], + [np.sin(rtheta), np.cos(rtheta)]]) + + x, y = np.dot(R, [x, y]) + x += xcenter + y += ycenter + + fig = plt.figure() + ax = fig.add_subplot(211, aspect='auto') + ax.fill(x, y, alpha=0.2, facecolor='yellow', edgecolor='yellow', + linewidth=1, zorder=1) + + e1 = mpatches.Arc((xcenter, ycenter), width, height, + angle=angle, linewidth=2, fill=False, zorder=2) + + ax.add_patch(e1) + + ax = fig.add_subplot(212, aspect='equal') + ax.fill(x, y, alpha=0.2, facecolor='green', edgecolor='green', zorder=1) + e2 = mpatches.Arc((xcenter, ycenter), width, height, + angle=angle, linewidth=2, fill=False, zorder=2) + + ax.add_patch(e2) + + +def test_marker_as_markerstyle(): + fix, ax = plt.subplots() + m = mmarkers.MarkerStyle('o') + ax.plot([1, 2, 3], [3, 2, 1], marker=m) + ax.scatter([1, 2, 3], [4, 3, 2], marker=m) + ax.errorbar([1, 2, 3], [5, 4, 3], marker=m) + + +@image_comparison(['markevery.png'], remove_text=True) +def test_markevery(): + x = np.linspace(0, 10, 100) + y = np.sin(x) * np.sqrt(x/10 + 0.5) + + # check marker only plot + fig, ax = plt.subplots() + ax.plot(x, y, 'o', label='default') + ax.plot(x, y, 'd', markevery=None, label='mark all') + ax.plot(x, y, 's', markevery=10, label='mark every 10') + ax.plot(x, y, '+', markevery=(5, 20), label='mark every 5 starting at 10') + ax.legend() + + +@image_comparison(['markevery_line.png'], remove_text=True, tol=0.005) +def test_markevery_line(): + # TODO: a slight change in rendering between Inkscape versions may explain + # why one had to introduce a small non-zero tolerance for the SVG test + # to pass. One may try to remove this hack once Travis' Inkscape version + # is modern enough. FWIW, no failure with 0.92.3 on my computer (#11358). + x = np.linspace(0, 10, 100) + y = np.sin(x) * np.sqrt(x/10 + 0.5) + + # check line/marker combos + fig, ax = plt.subplots() + ax.plot(x, y, '-o', label='default') + ax.plot(x, y, '-d', markevery=None, label='mark all') + ax.plot(x, y, '-s', markevery=10, label='mark every 10') + ax.plot(x, y, '-+', markevery=(5, 20), label='mark every 5 starting at 10') + ax.legend() + + +@image_comparison(['markevery_linear_scales.png'], remove_text=True, tol=0.001) +def test_markevery_linear_scales(): + cases = [None, + 8, + (30, 8), + [16, 24, 30], [0, -1], + slice(100, 200, 3), + 0.1, 0.3, 1.5, + (0.0, 0.1), (0.45, 0.1)] + + cols = 3 + gs = matplotlib.gridspec.GridSpec(len(cases) // cols + 1, cols) + + delta = 0.11 + x = np.linspace(0, 10 - 2 * delta, 200) + delta + y = np.sin(x) + 1.0 + delta + + for i, case in enumerate(cases): + row = (i // cols) + col = i % cols + plt.subplot(gs[row, col]) + plt.title('markevery=%s' % str(case)) + plt.plot(x, y, 'o', ls='-', ms=4, markevery=case) + + +@image_comparison(['markevery_linear_scales_zoomed.png'], remove_text=True) +def test_markevery_linear_scales_zoomed(): + cases = [None, + 8, + (30, 8), + [16, 24, 30], [0, -1], + slice(100, 200, 3), + 0.1, 0.3, 1.5, + (0.0, 0.1), (0.45, 0.1)] + + cols = 3 + gs = matplotlib.gridspec.GridSpec(len(cases) // cols + 1, cols) + + delta = 0.11 + x = np.linspace(0, 10 - 2 * delta, 200) + delta + y = np.sin(x) + 1.0 + delta + + for i, case in enumerate(cases): + row = (i // cols) + col = i % cols + plt.subplot(gs[row, col]) + plt.title('markevery=%s' % str(case)) + plt.plot(x, y, 'o', ls='-', ms=4, markevery=case) + plt.xlim((6, 6.7)) + plt.ylim((1.1, 1.7)) + + +@image_comparison(['markevery_log_scales.png'], remove_text=True) +def test_markevery_log_scales(): + cases = [None, + 8, + (30, 8), + [16, 24, 30], [0, -1], + slice(100, 200, 3), + 0.1, 0.3, 1.5, + (0.0, 0.1), (0.45, 0.1)] + + cols = 3 + gs = matplotlib.gridspec.GridSpec(len(cases) // cols + 1, cols) + + delta = 0.11 + x = np.linspace(0, 10 - 2 * delta, 200) + delta + y = np.sin(x) + 1.0 + delta + + for i, case in enumerate(cases): + row = (i // cols) + col = i % cols + plt.subplot(gs[row, col]) + plt.title('markevery=%s' % str(case)) + plt.xscale('log') + plt.yscale('log') + plt.plot(x, y, 'o', ls='-', ms=4, markevery=case) + + +@image_comparison(['markevery_polar.png'], style='default', remove_text=True) +def test_markevery_polar(): + cases = [None, + 8, + (30, 8), + [16, 24, 30], [0, -1], + slice(100, 200, 3), + 0.1, 0.3, 1.5, + (0.0, 0.1), (0.45, 0.1)] + + cols = 3 + gs = matplotlib.gridspec.GridSpec(len(cases) // cols + 1, cols) + + r = np.linspace(0, 3.0, 200) + theta = 2 * np.pi * r + + for i, case in enumerate(cases): + row = (i // cols) + col = i % cols + plt.subplot(gs[row, col], polar=True) + plt.title('markevery=%s' % str(case)) + plt.plot(theta, r, 'o', ls='-', ms=4, markevery=case) + + +@image_comparison(['markevery_linear_scales_nans.png'], remove_text=True) +def test_markevery_linear_scales_nans(): + cases = [None, + 8, + (30, 8), + [16, 24, 30], [0, -1], + slice(100, 200, 3), + 0.1, 0.3, 1.5, + (0.0, 0.1), (0.45, 0.1)] + + cols = 3 + gs = matplotlib.gridspec.GridSpec(len(cases) // cols + 1, cols) + + delta = 0.11 + x = np.linspace(0, 10 - 2 * delta, 200) + delta + y = np.sin(x) + 1.0 + delta + y[:10] = y[-20:] = y[50:70] = np.nan + + for i, case in enumerate(cases): + row = (i // cols) + col = i % cols + plt.subplot(gs[row, col]) + plt.title('markevery=%s' % str(case)) + plt.plot(x, y, 'o', ls='-', ms=4, markevery=case) + + +@image_comparison(['marker_edges'], remove_text=True) +def test_marker_edges(): + x = np.linspace(0, 1, 10) + fig, ax = plt.subplots() + ax.plot(x, np.sin(x), 'y.', ms=30.0, mew=0, mec='r') + ax.plot(x+0.1, np.sin(x), 'y.', ms=30.0, mew=1, mec='r') + ax.plot(x+0.2, np.sin(x), 'y.', ms=30.0, mew=2, mec='b') + + +@image_comparison(['bar_tick_label_single.png', 'bar_tick_label_single.png']) +def test_bar_tick_label_single(): + # From 2516: plot bar with array of string labels for x axis + ax = plt.gca() + ax.bar(0, 1, align='edge', tick_label='0') + + # Reuse testcase from above for a labeled data test + data = {"a": 0, "b": 1} + fig, ax = plt.subplots() + ax = plt.gca() + ax.bar("a", "b", align='edge', tick_label='0', data=data) + + +def test_nan_bar_values(): + fig, ax = plt.subplots() + ax.bar([0, 1], [np.nan, 4]) + + +def test_bar_ticklabel_fail(): + fig, ax = plt.subplots() + ax.bar([], []) + + +@image_comparison(['bar_tick_label_multiple.png']) +def test_bar_tick_label_multiple(): + # From 2516: plot bar with array of string labels for x axis + ax = plt.gca() + ax.bar([1, 2.5], [1, 2], width=[0.2, 0.5], tick_label=['a', 'b'], + align='center') + + +@image_comparison(['bar_tick_label_multiple_old_label_alignment.png']) +def test_bar_tick_label_multiple_old_alignment(): + # Test that the alignment for class is backward compatible + matplotlib.rcParams["ytick.alignment"] = "center" + ax = plt.gca() + ax.bar([1, 2.5], [1, 2], width=[0.2, 0.5], tick_label=['a', 'b'], + align='center') + + +@check_figures_equal(extensions=["png"]) +def test_bar_decimal_center(fig_test, fig_ref): + ax = fig_test.subplots() + x0 = [1.5, 8.4, 5.3, 4.2] + y0 = [1.1, 2.2, 3.3, 4.4] + x = [Decimal(x) for x in x0] + y = [Decimal(y) for y in y0] + # Test image - vertical, align-center bar chart with Decimal() input + ax.bar(x, y, align='center') + # Reference image + ax = fig_ref.subplots() + ax.bar(x0, y0, align='center') + + +@check_figures_equal(extensions=["png"]) +def test_barh_decimal_center(fig_test, fig_ref): + ax = fig_test.subplots() + x0 = [1.5, 8.4, 5.3, 4.2] + y0 = [1.1, 2.2, 3.3, 4.4] + x = [Decimal(x) for x in x0] + y = [Decimal(y) for y in y0] + # Test image - horizontal, align-center bar chart with Decimal() input + ax.barh(x, y, height=[0.5, 0.5, 1, 1], align='center') + # Reference image + ax = fig_ref.subplots() + ax.barh(x0, y0, height=[0.5, 0.5, 1, 1], align='center') + + +@check_figures_equal(extensions=["png"]) +def test_bar_decimal_width(fig_test, fig_ref): + x = [1.5, 8.4, 5.3, 4.2] + y = [1.1, 2.2, 3.3, 4.4] + w0 = [0.7, 1.45, 1, 2] + w = [Decimal(i) for i in w0] + # Test image - vertical bar chart with Decimal() width + ax = fig_test.subplots() + ax.bar(x, y, width=w, align='center') + # Reference image + ax = fig_ref.subplots() + ax.bar(x, y, width=w0, align='center') + + +@check_figures_equal(extensions=["png"]) +def test_barh_decimal_height(fig_test, fig_ref): + x = [1.5, 8.4, 5.3, 4.2] + y = [1.1, 2.2, 3.3, 4.4] + h0 = [0.7, 1.45, 1, 2] + h = [Decimal(i) for i in h0] + # Test image - horizontal bar chart with Decimal() height + ax = fig_test.subplots() + ax.barh(x, y, height=h, align='center') + # Reference image + ax = fig_ref.subplots() + ax.barh(x, y, height=h0, align='center') + + +def test_bar_color_none_alpha(): + ax = plt.gca() + rects = ax.bar([1, 2], [2, 4], alpha=0.3, color='none', edgecolor='r') + for rect in rects: + assert rect.get_facecolor() == (0, 0, 0, 0) + assert rect.get_edgecolor() == (1, 0, 0, 0.3) + + +def test_bar_edgecolor_none_alpha(): + ax = plt.gca() + rects = ax.bar([1, 2], [2, 4], alpha=0.3, color='r', edgecolor='none') + for rect in rects: + assert rect.get_facecolor() == (1, 0, 0, 0.3) + assert rect.get_edgecolor() == (0, 0, 0, 0) + + +@image_comparison(['barh_tick_label.png']) +def test_barh_tick_label(): + # From 2516: plot barh with array of string labels for y axis + ax = plt.gca() + ax.barh([1, 2.5], [1, 2], height=[0.2, 0.5], tick_label=['a', 'b'], + align='center') + + +def test_bar_timedelta(): + """Smoketest that bar can handle width and height in delta units.""" + fig, ax = plt.subplots() + ax.bar(datetime.datetime(2018, 1, 1), 1., + width=datetime.timedelta(hours=3)) + ax.bar(datetime.datetime(2018, 1, 1), 1., + xerr=datetime.timedelta(hours=2), + width=datetime.timedelta(hours=3)) + fig, ax = plt.subplots() + ax.barh(datetime.datetime(2018, 1, 1), 1, + height=datetime.timedelta(hours=3)) + ax.barh(datetime.datetime(2018, 1, 1), 1, + height=datetime.timedelta(hours=3), + yerr=datetime.timedelta(hours=2)) + fig, ax = plt.subplots() + ax.barh([datetime.datetime(2018, 1, 1), datetime.datetime(2018, 1, 1)], + np.array([1, 1.5]), + height=datetime.timedelta(hours=3)) + ax.barh([datetime.datetime(2018, 1, 1), datetime.datetime(2018, 1, 1)], + np.array([1, 1.5]), + height=[datetime.timedelta(hours=t) for t in [1, 2]]) + ax.broken_barh([(datetime.datetime(2018, 1, 1), + datetime.timedelta(hours=1))], + (10, 20)) + + +def test_bar_datetime_start(): + """test that tickers are correct for datetimes""" + start = np.array([np.datetime64('2012-01-01'), np.datetime64('2012-02-01'), + np.datetime64('2012-01-15')]) + stop = np.array([np.datetime64('2012-02-07'), np.datetime64('2012-02-13'), + np.datetime64('2012-02-12')]) + + fig, ax = plt.subplots() + ax.bar([0, 1, 3], height=stop-start, bottom=start) + assert isinstance(ax.yaxis.get_major_formatter(), mdates.AutoDateFormatter) + + fig, ax = plt.subplots() + ax.barh([0, 1, 3], width=stop-start, left=start) + assert isinstance(ax.xaxis.get_major_formatter(), mdates.AutoDateFormatter) + + +def test_boxplot_dates_pandas(pd): + # smoke test for boxplot and dates in pandas + data = np.random.rand(5, 2) + years = pd.date_range('1/1/2000', + periods=2, freq=pd.DateOffset(years=1)).year + plt.figure() + plt.boxplot(data, positions=years) + + +def test_boxplot_capwidths(): + data = np.random.rand(5, 3) + fig, axs = plt.subplots(9) + + axs[0].boxplot(data, capwidths=[0.3, 0.2, 0.1], widths=[0.1, 0.2, 0.3]) + axs[1].boxplot(data, capwidths=[0.3, 0.2, 0.1], widths=0.2) + axs[2].boxplot(data, capwidths=[0.3, 0.2, 0.1]) + + axs[3].boxplot(data, capwidths=0.5, widths=[0.1, 0.2, 0.3]) + axs[4].boxplot(data, capwidths=0.5, widths=0.2) + axs[5].boxplot(data, capwidths=0.5) + + axs[6].boxplot(data, widths=[0.1, 0.2, 0.3]) + axs[7].boxplot(data, widths=0.2) + axs[8].boxplot(data) + + +def test_pcolor_regression(pd): + from pandas.plotting import ( + register_matplotlib_converters, + deregister_matplotlib_converters, + ) + + fig = plt.figure() + ax = fig.add_subplot(111) + + times = [datetime.datetime(2021, 1, 1)] + while len(times) < 7: + times.append(times[-1] + datetime.timedelta(seconds=120)) + + y_vals = np.arange(5) + + time_axis, y_axis = np.meshgrid(times, y_vals) + shape = (len(y_vals) - 1, len(times) - 1) + z_data = np.arange(shape[0] * shape[1]) + + z_data.shape = shape + try: + register_matplotlib_converters() + + im = ax.pcolormesh(time_axis, y_axis, z_data) + # make sure this does not raise! + fig.canvas.draw() + finally: + deregister_matplotlib_converters() + + +def test_bar_pandas(pd): + # Smoke test for pandas + df = pd.DataFrame( + {'year': [2018, 2018, 2018], + 'month': [1, 1, 1], + 'day': [1, 2, 3], + 'value': [1, 2, 3]}) + df['date'] = pd.to_datetime(df[['year', 'month', 'day']]) + + monthly = df[['date', 'value']].groupby(['date']).sum() + dates = monthly.index + forecast = monthly['value'] + baseline = monthly['value'] + + fig, ax = plt.subplots() + ax.bar(dates, forecast, width=10, align='center') + ax.plot(dates, baseline, color='orange', lw=4) + + +def test_bar_pandas_indexed(pd): + # Smoke test for indexed pandas + df = pd.DataFrame({"x": [1., 2., 3.], "width": [.2, .4, .6]}, + index=[1, 2, 3]) + fig, ax = plt.subplots() + ax.bar(df.x, 1., width=df.width) + + +@mpl.style.context('default') +@check_figures_equal(extensions=['png']) +def test_bar_hatches(fig_test, fig_ref): + ax_test = fig_test.subplots() + ax_ref = fig_ref.subplots() + + x = [1, 2] + y = [2, 3] + hatches = ['x', 'o'] + for i in range(2): + ax_ref.bar(x[i], y[i], color='C0', hatch=hatches[i]) + + ax_test.bar(x, y, hatch=hatches) + + +@pytest.mark.parametrize( + ("x", "width", "label", "expected_labels", "container_label"), + [ + ("x", 1, "x", ["_nolegend_"], "x"), + (["a", "b", "c"], [10, 20, 15], ["A", "B", "C"], + ["A", "B", "C"], "_nolegend_"), + (["a", "b", "c"], [10, 20, 15], ["R", "Y", "_nolegend_"], + ["R", "Y", "_nolegend_"], "_nolegend_"), + (["a", "b", "c"], [10, 20, 15], "bars", + ["_nolegend_", "_nolegend_", "_nolegend_"], "bars"), + ] +) +def test_bar_labels(x, width, label, expected_labels, container_label): + _, ax = plt.subplots() + bar_container = ax.bar(x, width, label=label) + bar_labels = [bar.get_label() for bar in bar_container] + assert expected_labels == bar_labels + assert bar_container.get_label() == container_label + + +def test_bar_labels_length(): + _, ax = plt.subplots() + with pytest.raises(ValueError): + ax.bar(["x", "y"], [1, 2], label=["X", "Y", "Z"]) + _, ax = plt.subplots() + with pytest.raises(ValueError): + ax.bar(["x", "y"], [1, 2], label=["X"]) + + +def test_pandas_minimal_plot(pd): + # smoke test that series and index objects do not warn + for x in [pd.Series([1, 2], dtype="float64"), + pd.Series([1, 2], dtype="Float64")]: + plt.plot(x, x) + plt.plot(x.index, x) + plt.plot(x) + plt.plot(x.index) + df = pd.DataFrame({'col': [1, 2, 3]}) + plt.plot(df) + plt.plot(df, df) + + +@image_comparison(['hist_log.png'], remove_text=True) +def test_hist_log(): + data0 = np.linspace(0, 1, 200)**3 + data = np.concatenate([1 - data0, 1 + data0]) + fig, ax = plt.subplots() + ax.hist(data, fill=False, log=True) + + +@check_figures_equal(extensions=["png"]) +def test_hist_log_2(fig_test, fig_ref): + axs_test = fig_test.subplots(2, 3) + axs_ref = fig_ref.subplots(2, 3) + for i, histtype in enumerate(["bar", "step", "stepfilled"]): + # Set log scale, then call hist(). + axs_test[0, i].set_yscale("log") + axs_test[0, i].hist(1, 1, histtype=histtype) + # Call hist(), then set log scale. + axs_test[1, i].hist(1, 1, histtype=histtype) + axs_test[1, i].set_yscale("log") + # Use hist(..., log=True). + for ax in axs_ref[:, i]: + ax.hist(1, 1, log=True, histtype=histtype) + + +def test_hist_log_barstacked(): + fig, axs = plt.subplots(2) + axs[0].hist([[0], [0, 1]], 2, histtype="barstacked") + axs[0].set_yscale("log") + axs[1].hist([0, 0, 1], 2, histtype="barstacked") + axs[1].set_yscale("log") + fig.canvas.draw() + assert axs[0].get_ylim() == axs[1].get_ylim() + + +@image_comparison(['hist_bar_empty.png'], remove_text=True) +def test_hist_bar_empty(): + # From #3886: creating hist from empty dataset raises ValueError + ax = plt.gca() + ax.hist([], histtype='bar') + + +def test_hist_float16(): + np.random.seed(19680801) + values = np.clip( + np.random.normal(0.5, 0.3, size=1000), 0, 1).astype(np.float16) + h = plt.hist(values, bins=3, alpha=0.5) + bc = h[2] + # Check that there are no overlapping rectangles + for r in range(1, len(bc)): + rleft = bc[r-1].get_corners() + rright = bc[r].get_corners() + # right hand position of left rectangle <= + # left hand position of right rectangle + assert rleft[1][0] <= rright[0][0] + + +@image_comparison(['hist_step_empty.png'], remove_text=True) +def test_hist_step_empty(): + # From #3886: creating hist from empty dataset raises ValueError + ax = plt.gca() + ax.hist([], histtype='step') + + +@image_comparison(['hist_step_filled.png'], remove_text=True) +def test_hist_step_filled(): + np.random.seed(0) + x = np.random.randn(1000, 3) + n_bins = 10 + + kwargs = [{'fill': True}, {'fill': False}, {'fill': None}, {}]*2 + types = ['step']*4+['stepfilled']*4 + fig, axs = plt.subplots(nrows=2, ncols=4) + + for kg, _type, ax in zip(kwargs, types, axs.flat): + ax.hist(x, n_bins, histtype=_type, stacked=True, **kg) + ax.set_title(f'{kg}/{_type}') + ax.set_ylim(bottom=-50) + + patches = axs[0, 0].patches + assert all(p.get_facecolor() == p.get_edgecolor() for p in patches) + + +@image_comparison(['hist_density.png']) +def test_hist_density(): + np.random.seed(19680801) + data = np.random.standard_normal(2000) + fig, ax = plt.subplots() + ax.hist(data, density=True) + + +def test_hist_unequal_bins_density(): + # Test correct behavior of normalized histogram with unequal bins + # https://github.com/matplotlib/matplotlib/issues/9557 + rng = np.random.RandomState(57483) + t = rng.randn(100) + bins = [-3, -1, -0.5, 0, 1, 5] + mpl_heights, _, _ = plt.hist(t, bins=bins, density=True) + np_heights, _ = np.histogram(t, bins=bins, density=True) + assert_allclose(mpl_heights, np_heights) + + +def test_hist_datetime_datasets(): + data = [[datetime.datetime(2017, 1, 1), datetime.datetime(2017, 1, 1)], + [datetime.datetime(2017, 1, 1), datetime.datetime(2017, 1, 2)]] + fig, ax = plt.subplots() + ax.hist(data, stacked=True) + ax.hist(data, stacked=False) + + +@pytest.mark.parametrize("bins_preprocess", + [mpl.dates.date2num, + lambda bins: bins, + lambda bins: np.asarray(bins, 'datetime64')], + ids=['date2num', 'datetime.datetime', + 'np.datetime64']) +def test_hist_datetime_datasets_bins(bins_preprocess): + data = [[datetime.datetime(2019, 1, 5), datetime.datetime(2019, 1, 11), + datetime.datetime(2019, 2, 1), datetime.datetime(2019, 3, 1)], + [datetime.datetime(2019, 1, 11), datetime.datetime(2019, 2, 5), + datetime.datetime(2019, 2, 18), datetime.datetime(2019, 3, 1)]] + + date_edges = [datetime.datetime(2019, 1, 1), datetime.datetime(2019, 2, 1), + datetime.datetime(2019, 3, 1)] + + fig, ax = plt.subplots() + _, bins, _ = ax.hist(data, bins=bins_preprocess(date_edges), stacked=True) + np.testing.assert_allclose(bins, mpl.dates.date2num(date_edges)) + + _, bins, _ = ax.hist(data, bins=bins_preprocess(date_edges), stacked=False) + np.testing.assert_allclose(bins, mpl.dates.date2num(date_edges)) + + +@pytest.mark.parametrize('data, expected_number_of_hists', + [([], 1), + ([[]], 1), + ([[], []], 2)]) +def test_hist_with_empty_input(data, expected_number_of_hists): + hists, _, _ = plt.hist(data) + hists = np.asarray(hists) + + if hists.ndim == 1: + assert 1 == expected_number_of_hists + else: + assert hists.shape[0] == expected_number_of_hists + + +@pytest.mark.parametrize("histtype, zorder", + [("bar", mpl.patches.Patch.zorder), + ("step", mpl.lines.Line2D.zorder), + ("stepfilled", mpl.patches.Patch.zorder)]) +def test_hist_zorder(histtype, zorder): + ax = plt.figure().add_subplot() + ax.hist([1, 2], histtype=histtype) + assert ax.patches + for patch in ax.patches: + assert patch.get_zorder() == zorder + + +def test_stairs_no_baseline_fill_warns(): + fig, ax = plt.subplots() + with pytest.warns(UserWarning, match="baseline=None and fill=True"): + ax.stairs( + [4, 5, 1, 0, 2], + [1, 2, 3, 4, 5, 6], + facecolor="blue", + baseline=None, + fill=True + ) + + +@check_figures_equal(extensions=['png']) +def test_stairs(fig_test, fig_ref): + import matplotlib.lines as mlines + y = np.array([6, 14, 32, 37, 48, 32, 21, 4]) # hist + x = np.array([1., 2., 3., 4., 5., 6., 7., 8., 9.]) # bins + + test_axes = fig_test.subplots(3, 2).flatten() + test_axes[0].stairs(y, x, baseline=None) + test_axes[1].stairs(y, x, baseline=None, orientation='horizontal') + test_axes[2].stairs(y, x) + test_axes[3].stairs(y, x, orientation='horizontal') + test_axes[4].stairs(y, x) + test_axes[4].semilogy() + test_axes[5].stairs(y, x, orientation='horizontal') + test_axes[5].semilogx() + + # defaults of `PathPatch` to be used for all following Line2D + style = {'solid_joinstyle': 'miter', 'solid_capstyle': 'butt'} + + ref_axes = fig_ref.subplots(3, 2).flatten() + ref_axes[0].plot(x, np.append(y, y[-1]), drawstyle='steps-post', **style) + ref_axes[1].plot(np.append(y[0], y), x, drawstyle='steps-post', **style) + + ref_axes[2].plot(x, np.append(y, y[-1]), drawstyle='steps-post', **style) + ref_axes[2].add_line(mlines.Line2D([x[0], x[0]], [0, y[0]], **style)) + ref_axes[2].add_line(mlines.Line2D([x[-1], x[-1]], [0, y[-1]], **style)) + ref_axes[2].set_ylim(0, None) + + ref_axes[3].plot(np.append(y[0], y), x, drawstyle='steps-post', **style) + ref_axes[3].add_line(mlines.Line2D([0, y[0]], [x[0], x[0]], **style)) + ref_axes[3].add_line(mlines.Line2D([0, y[-1]], [x[-1], x[-1]], **style)) + ref_axes[3].set_xlim(0, None) + + ref_axes[4].plot(x, np.append(y, y[-1]), drawstyle='steps-post', **style) + ref_axes[4].add_line(mlines.Line2D([x[0], x[0]], [0, y[0]], **style)) + ref_axes[4].add_line(mlines.Line2D([x[-1], x[-1]], [0, y[-1]], **style)) + ref_axes[4].semilogy() + + ref_axes[5].plot(np.append(y[0], y), x, drawstyle='steps-post', **style) + ref_axes[5].add_line(mlines.Line2D([0, y[0]], [x[0], x[0]], **style)) + ref_axes[5].add_line(mlines.Line2D([0, y[-1]], [x[-1], x[-1]], **style)) + ref_axes[5].semilogx() + + +@check_figures_equal(extensions=['png']) +def test_stairs_fill(fig_test, fig_ref): + h, bins = [1, 2, 3, 4, 2], [0, 1, 2, 3, 4, 5] + bs = -2 + # Test + test_axes = fig_test.subplots(2, 2).flatten() + test_axes[0].stairs(h, bins, fill=True) + test_axes[1].stairs(h, bins, orientation='horizontal', fill=True) + test_axes[2].stairs(h, bins, baseline=bs, fill=True) + test_axes[3].stairs(h, bins, baseline=bs, orientation='horizontal', + fill=True) + + # # Ref + ref_axes = fig_ref.subplots(2, 2).flatten() + ref_axes[0].fill_between(bins, np.append(h, h[-1]), step='post', lw=0) + ref_axes[0].set_ylim(0, None) + ref_axes[1].fill_betweenx(bins, np.append(h, h[-1]), step='post', lw=0) + ref_axes[1].set_xlim(0, None) + ref_axes[2].fill_between(bins, np.append(h, h[-1]), + np.ones(len(h)+1)*bs, step='post', lw=0) + ref_axes[2].set_ylim(bs, None) + ref_axes[3].fill_betweenx(bins, np.append(h, h[-1]), + np.ones(len(h)+1)*bs, step='post', lw=0) + ref_axes[3].set_xlim(bs, None) + + +@check_figures_equal(extensions=['png']) +def test_stairs_update(fig_test, fig_ref): + # fixed ylim because stairs() does autoscale, but updating data does not + ylim = -3, 4 + # Test + test_ax = fig_test.add_subplot() + h = test_ax.stairs([1, 2, 3]) + test_ax.set_ylim(ylim) + h.set_data([3, 2, 1]) + h.set_data(edges=np.arange(4)+2) + h.set_data([1, 2, 1], np.arange(4)/2) + h.set_data([1, 2, 3]) + h.set_data(None, np.arange(4)) + assert np.allclose(h.get_data()[0], np.arange(1, 4)) + assert np.allclose(h.get_data()[1], np.arange(4)) + h.set_data(baseline=-2) + assert h.get_data().baseline == -2 + + # Ref + ref_ax = fig_ref.add_subplot() + h = ref_ax.stairs([1, 2, 3], baseline=-2) + ref_ax.set_ylim(ylim) + + +@check_figures_equal(extensions=['png']) +def test_stairs_baseline_None(fig_test, fig_ref): + x = np.array([0, 2, 3, 5, 10]) + y = np.array([1.148, 1.231, 1.248, 1.25]) + + test_axes = fig_test.add_subplot() + test_axes.stairs(y, x, baseline=None) + + style = {'solid_joinstyle': 'miter', 'solid_capstyle': 'butt'} + + ref_axes = fig_ref.add_subplot() + ref_axes.plot(x, np.append(y, y[-1]), drawstyle='steps-post', **style) + + +def test_stairs_empty(): + ax = plt.figure().add_subplot() + ax.stairs([], [42]) + assert ax.get_xlim() == (39, 45) + assert ax.get_ylim() == (-0.06, 0.06) + + +def test_stairs_invalid_nan(): + with pytest.raises(ValueError, match='Nan values in "edges"'): + plt.stairs([1, 2], [0, np.nan, 1]) + + +def test_stairs_invalid_mismatch(): + with pytest.raises(ValueError, match='Size mismatch'): + plt.stairs([1, 2], [0, 1]) + + +def test_stairs_invalid_update(): + h = plt.stairs([1, 2], [0, 1, 2]) + with pytest.raises(ValueError, match='Nan values in "edges"'): + h.set_data(edges=[1, np.nan, 2]) + + +def test_stairs_invalid_update2(): + h = plt.stairs([1, 2], [0, 1, 2]) + with pytest.raises(ValueError, match='Size mismatch'): + h.set_data(edges=np.arange(5)) + + +@image_comparison(['test_stairs_options.png'], remove_text=True) +def test_stairs_options(): + x, y = np.array([1, 2, 3, 4, 5]), np.array([1, 2, 3, 4]).astype(float) + yn = y.copy() + yn[1] = np.nan + + fig, ax = plt.subplots() + ax.stairs(y*3, x, color='green', fill=True, label="A") + ax.stairs(y, x*3-3, color='red', fill=True, + orientation='horizontal', label="B") + ax.stairs(yn, x, color='orange', ls='--', lw=2, label="C") + ax.stairs(yn/3, x*3-2, ls='--', lw=2, baseline=0.5, + orientation='horizontal', label="D") + ax.stairs(y[::-1]*3+13, x-1, color='red', ls='--', lw=2, baseline=None, + label="E") + ax.stairs(y[::-1]*3+14, x, baseline=26, + color='purple', ls='--', lw=2, label="F") + ax.stairs(yn[::-1]*3+15, x+1, baseline=np.linspace(27, 25, len(y)), + color='blue', ls='--', label="G", fill=True) + ax.stairs(y[:-1][::-1]*2+11, x[:-1]+0.5, color='black', ls='--', lw=2, + baseline=12, hatch='//', label="H") + ax.legend(loc=0) + + +@image_comparison(['test_stairs_datetime.png']) +def test_stairs_datetime(): + f, ax = plt.subplots(constrained_layout=True) + ax.stairs(np.arange(36), + np.arange(np.datetime64('2001-12-27'), + np.datetime64('2002-02-02'))) + plt.xticks(rotation=30) + + +@check_figures_equal(extensions=['png']) +def test_stairs_edge_handling(fig_test, fig_ref): + # Test + test_ax = fig_test.add_subplot() + test_ax.stairs([1, 2, 3], color='red', fill=True) + + # Ref + ref_ax = fig_ref.add_subplot() + st = ref_ax.stairs([1, 2, 3], fill=True) + st.set_color('red') + + +def contour_dat(): + x = np.linspace(-3, 5, 150) + y = np.linspace(-3, 5, 120) + z = np.cos(x) + np.sin(y[:, np.newaxis]) + return x, y, z + + +@image_comparison(['contour_hatching'], remove_text=True, style='mpl20') +def test_contour_hatching(): + x, y, z = contour_dat() + fig, ax = plt.subplots() + ax.contourf(x, y, z, 7, hatches=['/', '\\', '//', '-'], + cmap=mpl.colormaps['gray'], + extend='both', alpha=0.5) + + +@image_comparison(['contour_colorbar'], style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.54) +def test_contour_colorbar(): + x, y, z = contour_dat() + + fig, ax = plt.subplots() + cs = ax.contourf(x, y, z, levels=np.arange(-1.8, 1.801, 0.2), + cmap=mpl.colormaps['RdBu'], + vmin=-0.6, + vmax=0.6, + extend='both') + cs1 = ax.contour(x, y, z, levels=np.arange(-2.2, -0.599, 0.2), + colors=['y'], + linestyles='solid', + linewidths=2) + cs2 = ax.contour(x, y, z, levels=np.arange(0.6, 2.2, 0.2), + colors=['c'], + linewidths=2) + cbar = fig.colorbar(cs, ax=ax) + cbar.add_lines(cs1) + cbar.add_lines(cs2, erase=False) + + +@image_comparison(['hist2d.png', 'hist2d.png'], remove_text=True, style='mpl20') +def test_hist2d(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + + np.random.seed(0) + # make it not symmetric in case we switch x and y axis + x = np.random.randn(100)*2+5 + y = np.random.randn(100)-2 + fig, ax = plt.subplots() + ax.hist2d(x, y, bins=10, rasterized=True) + + # Reuse testcase from above for a labeled data test + data = {"x": x, "y": y} + fig, ax = plt.subplots() + ax.hist2d("x", "y", bins=10, data=data, rasterized=True) + + +@image_comparison(['hist2d_transpose.png'], remove_text=True, style='mpl20') +def test_hist2d_transpose(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + + np.random.seed(0) + # make sure the output from np.histogram is transposed before + # passing to pcolorfast + x = np.array([5]*100) + y = np.random.randn(100)-2 + fig, ax = plt.subplots() + ax.hist2d(x, y, bins=10, rasterized=True) + + +def test_hist2d_density(): + x, y = np.random.random((2, 100)) + ax = plt.figure().subplots() + for obj in [ax, plt]: + obj.hist2d(x, y, density=True) + + +class TestScatter: + @image_comparison(['scatter'], style='mpl20', remove_text=True) + def test_scatter_plot(self): + data = {"x": np.array([3, 4, 2, 6]), "y": np.array([2, 5, 2, 3]), + "c": ['r', 'y', 'b', 'lime'], "s": [24, 15, 19, 29], + "c2": ['0.5', '0.6', '0.7', '0.8']} + + fig, ax = plt.subplots() + ax.scatter(data["x"] - 1., data["y"] - 1., c=data["c"], s=data["s"]) + ax.scatter(data["x"] + 1., data["y"] + 1., c=data["c2"], s=data["s"]) + ax.scatter("x", "y", c="c", s="s", data=data) + + @image_comparison(['scatter_marker.png'], remove_text=True) + def test_scatter_marker(self): + fig, (ax0, ax1, ax2) = plt.subplots(ncols=3) + ax0.scatter([3, 4, 2, 6], [2, 5, 2, 3], + c=[(1, 0, 0), 'y', 'b', 'lime'], + s=[60, 50, 40, 30], + edgecolors=['k', 'r', 'g', 'b'], + marker='s') + ax1.scatter([3, 4, 2, 6], [2, 5, 2, 3], + c=[(1, 0, 0), 'y', 'b', 'lime'], + s=[60, 50, 40, 30], + edgecolors=['k', 'r', 'g', 'b'], + marker=mmarkers.MarkerStyle('o', fillstyle='top')) + # unit area ellipse + rx, ry = 3, 1 + area = rx * ry * np.pi + theta = np.linspace(0, 2 * np.pi, 21) + verts = np.column_stack([np.cos(theta) * rx / area, + np.sin(theta) * ry / area]) + ax2.scatter([3, 4, 2, 6], [2, 5, 2, 3], + c=[(1, 0, 0), 'y', 'b', 'lime'], + s=[60, 50, 40, 30], + edgecolors=['k', 'r', 'g', 'b'], + marker=verts) + + @image_comparison(['scatter_2D'], remove_text=True, extensions=['png']) + def test_scatter_2D(self): + x = np.arange(3) + y = np.arange(2) + x, y = np.meshgrid(x, y) + z = x + y + fig, ax = plt.subplots() + ax.scatter(x, y, c=z, s=200, edgecolors='face') + + @check_figures_equal(extensions=["png"]) + def test_scatter_decimal(self, fig_test, fig_ref): + x0 = np.array([1.5, 8.4, 5.3, 4.2]) + y0 = np.array([1.1, 2.2, 3.3, 4.4]) + x = np.array([Decimal(i) for i in x0]) + y = np.array([Decimal(i) for i in y0]) + c = ['r', 'y', 'b', 'lime'] + s = [24, 15, 19, 29] + # Test image - scatter plot with Decimal() input + ax = fig_test.subplots() + ax.scatter(x, y, c=c, s=s) + # Reference image + ax = fig_ref.subplots() + ax.scatter(x0, y0, c=c, s=s) + + def test_scatter_color(self): + # Try to catch cases where 'c' kwarg should have been used. + with pytest.raises(ValueError): + plt.scatter([1, 2], [1, 2], color=[0.1, 0.2]) + with pytest.raises(ValueError): + plt.scatter([1, 2, 3], [1, 2, 3], color=[1, 2, 3]) + + @pytest.mark.parametrize('kwargs', + [ + {'cmap': 'gray'}, + {'norm': mcolors.Normalize()}, + {'vmin': 0}, + {'vmax': 0} + ]) + def test_scatter_color_warning(self, kwargs): + warn_match = "No data for colormapping provided " + # Warn for cases where 'cmap', 'norm', 'vmin', 'vmax' + # kwargs are being overridden + with pytest.warns(Warning, match=warn_match): + plt.scatter([], [], **kwargs) + with pytest.warns(Warning, match=warn_match): + plt.scatter([1, 2], [3, 4], c=[], **kwargs) + # Do not warn for cases where 'c' matches 'x' and 'y' + plt.scatter([], [], c=[], **kwargs) + plt.scatter([1, 2], [3, 4], c=[4, 5], **kwargs) + + def test_scatter_unfilled(self): + coll = plt.scatter([0, 1, 2], [1, 3, 2], c=['0.1', '0.3', '0.5'], + marker=mmarkers.MarkerStyle('o', fillstyle='none'), + linewidths=[1.1, 1.2, 1.3]) + assert coll.get_facecolors().shape == (0, 4) # no facecolors + assert_array_equal(coll.get_edgecolors(), [[0.1, 0.1, 0.1, 1], + [0.3, 0.3, 0.3, 1], + [0.5, 0.5, 0.5, 1]]) + assert_array_equal(coll.get_linewidths(), [1.1, 1.2, 1.3]) + + @mpl.style.context('default') + def test_scatter_unfillable(self): + coll = plt.scatter([0, 1, 2], [1, 3, 2], c=['0.1', '0.3', '0.5'], + marker='x', + linewidths=[1.1, 1.2, 1.3]) + assert_array_equal(coll.get_facecolors(), coll.get_edgecolors()) + assert_array_equal(coll.get_edgecolors(), [[0.1, 0.1, 0.1, 1], + [0.3, 0.3, 0.3, 1], + [0.5, 0.5, 0.5, 1]]) + assert_array_equal(coll.get_linewidths(), [1.1, 1.2, 1.3]) + + def test_scatter_size_arg_size(self): + x = np.arange(4) + with pytest.raises(ValueError, match='same size as x and y'): + plt.scatter(x, x, x[1:]) + with pytest.raises(ValueError, match='same size as x and y'): + plt.scatter(x[1:], x[1:], x) + with pytest.raises(ValueError, match='float array-like'): + plt.scatter(x, x, 'foo') + + def test_scatter_edgecolor_RGB(self): + # GitHub issue 19066 + coll = plt.scatter([1, 2, 3], [1, np.nan, np.nan], + edgecolor=(1, 0, 0)) + assert mcolors.same_color(coll.get_edgecolor(), (1, 0, 0)) + coll = plt.scatter([1, 2, 3, 4], [1, np.nan, np.nan, 1], + edgecolor=(1, 0, 0, 1)) + assert mcolors.same_color(coll.get_edgecolor(), (1, 0, 0, 1)) + + @check_figures_equal(extensions=["png"]) + def test_scatter_invalid_color(self, fig_test, fig_ref): + ax = fig_test.subplots() + cmap = mpl.colormaps["viridis"].resampled(16) + cmap.set_bad("k", 1) + # Set a nonuniform size to prevent the last call to `scatter` (plotting + # the invalid points separately in fig_ref) from using the marker + # stamping fast path, which would result in slightly offset markers. + ax.scatter(range(4), range(4), + c=[1, np.nan, 2, np.nan], s=[1, 2, 3, 4], + cmap=cmap, plotnonfinite=True) + ax = fig_ref.subplots() + cmap = mpl.colormaps["viridis"].resampled(16) + ax.scatter([0, 2], [0, 2], c=[1, 2], s=[1, 3], cmap=cmap) + ax.scatter([1, 3], [1, 3], s=[2, 4], color="k") + + @check_figures_equal(extensions=["png"]) + def test_scatter_no_invalid_color(self, fig_test, fig_ref): + # With plotnonfinite=False we plot only 2 points. + ax = fig_test.subplots() + cmap = mpl.colormaps["viridis"].resampled(16) + cmap.set_bad("k", 1) + ax.scatter(range(4), range(4), + c=[1, np.nan, 2, np.nan], s=[1, 2, 3, 4], + cmap=cmap, plotnonfinite=False) + ax = fig_ref.subplots() + ax.scatter([0, 2], [0, 2], c=[1, 2], s=[1, 3], cmap=cmap) + + def test_scatter_norm_vminvmax(self): + """Parameters vmin, vmax should error if norm is given.""" + x = [1, 2, 3] + ax = plt.axes() + with pytest.raises(ValueError, + match="Passing a Normalize instance simultaneously " + "with vmin/vmax is not supported."): + ax.scatter(x, x, c=x, norm=mcolors.Normalize(-10, 10), + vmin=0, vmax=5) + + @check_figures_equal(extensions=["png"]) + def test_scatter_single_point(self, fig_test, fig_ref): + ax = fig_test.subplots() + ax.scatter(1, 1, c=1) + ax = fig_ref.subplots() + ax.scatter([1], [1], c=[1]) + + @check_figures_equal(extensions=["png"]) + def test_scatter_different_shapes(self, fig_test, fig_ref): + x = np.arange(10) + ax = fig_test.subplots() + ax.scatter(x, x.reshape(2, 5), c=x.reshape(5, 2)) + ax = fig_ref.subplots() + ax.scatter(x.reshape(5, 2), x, c=x.reshape(2, 5)) + + # Parameters for *test_scatter_c*. NB: assuming that the + # scatter plot will have 4 elements. The tuple scheme is: + # (*c* parameter case, exception regexp key or None if no exception) + params_test_scatter_c = [ + # single string: + ('0.5', None), + # Single letter-sequences + (["rgby"], "conversion"), + # Special cases + ("red", None), + ("none", None), + (None, None), + (["r", "g", "b", "none"], None), + # Non-valid color spec (FWIW, 'jaune' means yellow in French) + ("jaune", "conversion"), + (["jaune"], "conversion"), # wrong type before wrong size + (["jaune"]*4, "conversion"), + # Value-mapping like + ([0.5]*3, None), # should emit a warning for user's eyes though + ([0.5]*4, None), # NB: no warning as matching size allows mapping + ([0.5]*5, "shape"), + # list of strings: + (['0.5', '0.4', '0.6', '0.7'], None), + (['0.5', 'red', '0.6', 'C5'], None), + (['0.5', 0.5, '0.6', 'C5'], "conversion"), + # RGB values + ([[1, 0, 0]], None), + ([[1, 0, 0]]*3, "shape"), + ([[1, 0, 0]]*4, None), + ([[1, 0, 0]]*5, "shape"), + # RGBA values + ([[1, 0, 0, 0.5]], None), + ([[1, 0, 0, 0.5]]*3, "shape"), + ([[1, 0, 0, 0.5]]*4, None), + ([[1, 0, 0, 0.5]]*5, "shape"), + # Mix of valid color specs + ([[1, 0, 0, 0.5]]*3 + [[1, 0, 0]], None), + ([[1, 0, 0, 0.5], "red", "0.0"], "shape"), + ([[1, 0, 0, 0.5], "red", "0.0", "C5"], None), + ([[1, 0, 0, 0.5], "red", "0.0", "C5", [0, 1, 0]], "shape"), + # Mix of valid and non valid color specs + ([[1, 0, 0, 0.5], "red", "jaune"], "conversion"), + ([[1, 0, 0, 0.5], "red", "0.0", "jaune"], "conversion"), + ([[1, 0, 0, 0.5], "red", "0.0", "C5", "jaune"], "conversion"), + ] + + @pytest.mark.parametrize('c_case, re_key', params_test_scatter_c) + def test_scatter_c(self, c_case, re_key): + def get_next_color(): # pragma: no cover + return 'blue' # currently unused + + xsize = 4 + # Additional checking of *c* (introduced in #11383). + REGEXP = { + "shape": "^'c' argument has [0-9]+ elements", # shape mismatch + "conversion": "^'c' argument must be a color", # bad vals + } + + assert_context = ( + pytest.raises(ValueError, match=REGEXP[re_key]) + if re_key is not None + else pytest.warns(match="argument looks like a single numeric RGB") + if isinstance(c_case, list) and len(c_case) == 3 + else contextlib.nullcontext() + ) + with assert_context: + mpl.axes.Axes._parse_scatter_color_args( + c=c_case, edgecolors="black", kwargs={}, xsize=xsize, + get_next_color_func=get_next_color) + + @mpl.style.context('default') + @check_figures_equal(extensions=["png"]) + def test_scatter_single_color_c(self, fig_test, fig_ref): + rgb = [[1, 0.5, 0.05]] + rgba = [[1, 0.5, 0.05, .5]] + + # set via color kwarg + ax_ref = fig_ref.subplots() + ax_ref.scatter(np.ones(3), range(3), color=rgb) + ax_ref.scatter(np.ones(4)*2, range(4), color=rgba) + + # set via broadcasting via c + ax_test = fig_test.subplots() + ax_test.scatter(np.ones(3), range(3), c=rgb) + ax_test.scatter(np.ones(4)*2, range(4), c=rgba) + + def test_scatter_linewidths(self): + x = np.arange(5) + + fig, ax = plt.subplots() + for i in range(3): + pc = ax.scatter(x, np.full(5, i), c=f'C{i}', marker='x', s=100, + linewidths=i + 1) + assert pc.get_linewidths() == i + 1 + + pc = ax.scatter(x, np.full(5, 3), c='C3', marker='x', s=100, + linewidths=[*range(1, 5), None]) + assert_array_equal(pc.get_linewidths(), + [*range(1, 5), mpl.rcParams['lines.linewidth']]) + + def test_scatter_singular_plural_arguments(self): + + with pytest.raises(TypeError, + match="Got both 'linewidth' and 'linewidths',\ + which are aliases of one another"): + plt.scatter([1, 2, 3], [1, 2, 3], linewidths=[0.5, 0.4, 0.3], linewidth=0.2) + + with pytest.raises(TypeError, + match="Got both 'edgecolor' and 'edgecolors',\ + which are aliases of one another"): + plt.scatter([1, 2, 3], [1, 2, 3], + edgecolors=["#ffffff", "#000000", "#f0f0f0"], + edgecolor="#ffffff") + + with pytest.raises(TypeError, + match="Got both 'facecolors' and 'facecolor',\ + which are aliases of one another"): + plt.scatter([1, 2, 3], [1, 2, 3], + facecolors=["#ffffff", "#000000", "#f0f0f0"], + facecolor="#ffffff") + + +def _params(c=None, xsize=2, *, edgecolors=None, **kwargs): + return (c, edgecolors, kwargs, xsize) +_result = namedtuple('_result', 'c, colors') + + +@pytest.mark.parametrize( + 'params, expected_result', + [(_params(), + _result(c='b', colors=np.array([[0, 0, 1, 1]]))), + (_params(c='r'), + _result(c='r', colors=np.array([[1, 0, 0, 1]]))), + (_params(c='r', colors='b'), + _result(c='r', colors=np.array([[1, 0, 0, 1]]))), + # color + (_params(color='b'), + _result(c='b', colors=np.array([[0, 0, 1, 1]]))), + (_params(color=['b', 'g']), + _result(c=['b', 'g'], colors=np.array([[0, 0, 1, 1], [0, .5, 0, 1]]))), + ]) +def test_parse_scatter_color_args(params, expected_result): + def get_next_color(): # pragma: no cover + return 'blue' # currently unused + + c, colors, _edgecolors = mpl.axes.Axes._parse_scatter_color_args( + *params, get_next_color_func=get_next_color) + assert c == expected_result.c + assert_allclose(colors, expected_result.colors) + +del _params +del _result + + +@pytest.mark.parametrize( + 'kwargs, expected_edgecolors', + [(dict(), None), + (dict(c='b'), None), + (dict(edgecolors='r'), 'r'), + (dict(edgecolors=['r', 'g']), ['r', 'g']), + (dict(edgecolor='r'), 'r'), + (dict(edgecolors='face'), 'face'), + (dict(edgecolors='none'), 'none'), + (dict(edgecolor='r', edgecolors='g'), 'r'), + (dict(c='b', edgecolor='r', edgecolors='g'), 'r'), + (dict(color='r'), 'r'), + (dict(color='r', edgecolor='g'), 'g'), + ]) +def test_parse_scatter_color_args_edgecolors(kwargs, expected_edgecolors): + def get_next_color(): # pragma: no cover + return 'blue' # currently unused + + c = kwargs.pop('c', None) + edgecolors = kwargs.pop('edgecolors', None) + _, _, result_edgecolors = \ + mpl.axes.Axes._parse_scatter_color_args( + c, edgecolors, kwargs, xsize=2, get_next_color_func=get_next_color) + assert result_edgecolors == expected_edgecolors + + +def test_parse_scatter_color_args_error(): + def get_next_color(): # pragma: no cover + return 'blue' # currently unused + + with pytest.raises(ValueError, + match="RGBA values should be within 0-1 range"): + c = np.array([[0.1, 0.2, 0.7], [0.2, 0.4, 1.4]]) # value > 1 + mpl.axes.Axes._parse_scatter_color_args( + c, None, kwargs={}, xsize=2, get_next_color_func=get_next_color) + + +# Warning message tested in the next two tests. +WARN_MSG = ( + "You passed both c and facecolor/facecolors for the markers. " + "c has precedence over facecolor/facecolors. This behavior may " + "change in the future." +) +# Test cases shared between direct and integration tests +COLOR_TEST_CASES = [ + ('red', 'blue'), + (['red', 'blue'], ['green', 'yellow']), + ([[1, 0, 0], [0, 1, 0]], [[0, 0, 1], [1, 1, 0]]) +] + + +@pytest.mark.parametrize('c, facecolor', COLOR_TEST_CASES) +def test_parse_c_facecolor_warning_direct(c, facecolor): + """Test the internal _parse_scatter_color_args method directly.""" + def get_next_color(): # pragma: no cover + return 'blue' # currently unused + + # Test with facecolors (plural) + with pytest.warns(UserWarning, match=WARN_MSG): + mpl.axes.Axes._parse_scatter_color_args( + c=c, edgecolors=None, kwargs={'facecolors': facecolor}, + xsize=2, get_next_color_func=get_next_color) + + # Test with facecolor (singular) + with pytest.warns(UserWarning, match=WARN_MSG): + mpl.axes.Axes._parse_scatter_color_args( + c=c, edgecolors=None, kwargs={'facecolor': facecolor}, + xsize=2, get_next_color_func=get_next_color) + + +@pytest.mark.parametrize('c, facecolor', COLOR_TEST_CASES) +def test_scatter_c_facecolor_warning_integration(c, facecolor): + """Test the warning through the actual scatter plot creation.""" + fig, ax = plt.subplots() + x = [0, 1] if isinstance(c, (list, tuple)) else [0] + y = x + + # Test with facecolors (plural) + with pytest.warns(UserWarning, match=WARN_MSG): + ax.scatter(x, y, c=c, facecolors=facecolor) + + # Test with facecolor (singular) + with pytest.warns(UserWarning, match=WARN_MSG): + ax.scatter(x, y, c=c, facecolor=facecolor) + + +def test_as_mpl_axes_api(): + # tests the _as_mpl_axes api + class Polar: + def __init__(self): + self.theta_offset = 0 + + def _as_mpl_axes(self): + # implement the matplotlib axes interface + return PolarAxes, {'theta_offset': self.theta_offset} + + prj = Polar() + prj2 = Polar() + prj2.theta_offset = np.pi + + # testing axes creation with plt.axes + ax = plt.axes((0, 0, 1, 1), projection=prj) + assert type(ax) is PolarAxes + plt.close() + + # testing axes creation with subplot + ax = plt.subplot(121, projection=prj) + assert type(ax) is PolarAxes + plt.close() + + +def test_pyplot_axes(): + # test focusing of Axes in other Figure + fig1, ax1 = plt.subplots() + fig2, ax2 = plt.subplots() + plt.sca(ax1) + assert ax1 is plt.gca() + assert fig1 is plt.gcf() + plt.close(fig1) + plt.close(fig2) + + +def test_log_scales(): + fig, ax = plt.subplots() + ax.plot(np.log(np.linspace(0.1, 100))) + ax.set_yscale('log', base=5.5) + ax.invert_yaxis() + ax.set_xscale('log', base=9.0) + xticks, yticks = ( + [(t.get_loc(), t.label1.get_text()) for t in axis._update_ticks()] + for axis in [ax.xaxis, ax.yaxis] + ) + assert xticks == [ + (1.0, '$\\mathdefault{9^{0}}$'), + (9.0, '$\\mathdefault{9^{1}}$'), + (81.0, '$\\mathdefault{9^{2}}$'), + (2.0, ''), + (3.0, ''), + (4.0, ''), + (5.0, ''), + (6.0, ''), + (7.0, ''), + (8.0, ''), + (18.0, ''), + (27.0, ''), + (36.0, ''), + (45.0, ''), + (54.0, ''), + (63.0, ''), + (72.0, ''), + ] + assert yticks == [ + (0.18181818181818182, '$\\mathdefault{5.5^{-1}}$'), + (1.0, '$\\mathdefault{5.5^{0}}$'), + (5.5, '$\\mathdefault{5.5^{1}}$'), + (0.36363636363636365, ''), + (0.5454545454545454, ''), + (0.7272727272727273, ''), + (0.9090909090909092, ''), + (2.0, ''), + (3.0, ''), + (4.0, ''), + (5.0, ''), + ] + + +def test_log_scales_no_data(): + _, ax = plt.subplots() + ax.set(xscale="log", yscale="log") + ax.xaxis.set_major_locator(mticker.MultipleLocator(1)) + assert ax.get_xlim() == ax.get_ylim() == (1, 10) + + +def test_log_scales_invalid(): + fig, ax = plt.subplots() + ax.set_xscale('log') + with pytest.warns(UserWarning, match='Attempt to set non-positive'): + ax.set_xlim(-1, 10) + ax.set_yscale('log') + with pytest.warns(UserWarning, match='Attempt to set non-positive'): + ax.set_ylim(-1, 10) + + +@image_comparison(['stackplot_test_image.png', 'stackplot_test_image.png'], + tol=0 if platform.machine() == 'x86_64' else 0.031) +def test_stackplot(): + fig = plt.figure() + x = np.linspace(0, 10, 10) + y1 = 1.0 * x + y2 = 2.0 * x + 1 + y3 = 3.0 * x + 2 + ax = fig.add_subplot(1, 1, 1) + ax.stackplot(x, y1, y2, y3) + ax.set_xlim((0, 10)) + ax.set_ylim((0, 70)) + + # Reuse testcase from above for a test with labeled data and with colours + # from the Axes property cycle. + data = {"x": x, "y1": y1, "y2": y2, "y3": y3} + fig, ax = plt.subplots() + ax.stackplot("x", "y1", "y2", "y3", data=data, colors=["C0", "C1", "C2"]) + ax.set_xlim((0, 10)) + ax.set_ylim((0, 70)) + + +@image_comparison(['stackplot_test_baseline.png'], remove_text=True) +def test_stackplot_baseline(): + np.random.seed(0) + + def layers(n, m): + a = np.zeros((m, n)) + for i in range(n): + for j in range(5): + x = 1 / (.1 + np.random.random()) + y = 2 * np.random.random() - .5 + z = 10 / (.1 + np.random.random()) + a[:, i] += x * np.exp(-((np.arange(m) / m - y) * z) ** 2) + return a + + d = layers(3, 100) + d[50, :] = 0 # test for fixed weighted wiggle (issue #6313) + + fig, axs = plt.subplots(2, 2) + + axs[0, 0].stackplot(range(100), d.T, baseline='zero') + axs[0, 1].stackplot(range(100), d.T, baseline='sym') + axs[1, 0].stackplot(range(100), d.T, baseline='wiggle') + axs[1, 1].stackplot(range(100), d.T, baseline='weighted_wiggle') + + +@check_figures_equal(extensions=['png']) +def test_stackplot_hatching(fig_ref, fig_test): + x = np.linspace(0, 10, 10) + y1 = 1.0 * x + y2 = 2.0 * x + 1 + y3 = 3.0 * x + 2 + # stackplot with different hatching styles (issue #27146) + ax_test = fig_test.subplots() + ax_test.stackplot(x, y1, y2, y3, hatch=["x", "//", "\\\\"], colors=["white"]) + ax_test.set_xlim((0, 10)) + ax_test.set_ylim((0, 70)) + # compare with result from hatching each layer individually + stack_baseline = np.zeros(len(x)) + ax_ref = fig_ref.subplots() + ax_ref.fill_between(x, stack_baseline, y1, hatch="x", facecolor="white") + ax_ref.fill_between(x, y1, y1+y2, hatch="//", facecolor="white") + ax_ref.fill_between(x, y1+y2, y1+y2+y3, hatch="\\\\", facecolor="white") + ax_ref.set_xlim((0, 10)) + ax_ref.set_ylim((0, 70)) + + +def _bxp_test_helper( + stats_kwargs={}, transform_stats=lambda s: s, bxp_kwargs={}): + np.random.seed(937) + logstats = mpl.cbook.boxplot_stats( + np.random.lognormal(mean=1.25, sigma=1., size=(37, 4)), **stats_kwargs) + fig, ax = plt.subplots() + if bxp_kwargs.get('orientation', 'vertical') == 'vertical': + ax.set_yscale('log') + else: + ax.set_xscale('log') + # Work around baseline images generate back when bxp did not respect the + # boxplot.boxprops.linewidth rcParam when patch_artist is False. + if not bxp_kwargs.get('patch_artist', False): + mpl.rcParams['boxplot.boxprops.linewidth'] = \ + mpl.rcParams['lines.linewidth'] + ax.bxp(transform_stats(logstats), **bxp_kwargs) + + +@image_comparison(['bxp_baseline.png'], + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_baseline(): + _bxp_test_helper() + + +@image_comparison(['bxp_rangewhis.png'], + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_rangewhis(): + _bxp_test_helper(stats_kwargs=dict(whis=[0, 100])) + + +@image_comparison(['bxp_percentilewhis.png'], + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_percentilewhis(): + _bxp_test_helper(stats_kwargs=dict(whis=[5, 95])) + + +@image_comparison(['bxp_with_xlabels.png'], + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_with_xlabels(): + def transform(stats): + for s, label in zip(stats, list('ABCD')): + s['label'] = label + return stats + + _bxp_test_helper(transform_stats=transform) + + +@image_comparison(['bxp_horizontal.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default', + tol=0.1) +def test_bxp_horizontal(): + _bxp_test_helper(bxp_kwargs=dict(orientation='horizontal')) + + +@image_comparison(['bxp_with_ylabels.png'], + savefig_kwarg={'dpi': 40}, + style='default', + tol=0.1) +def test_bxp_with_ylabels(): + def transform(stats): + for s, label in zip(stats, list('ABCD')): + s['label'] = label + return stats + + _bxp_test_helper(transform_stats=transform, + bxp_kwargs=dict(orientation='horizontal')) + + +@image_comparison(['bxp_patchartist.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_patchartist(): + _bxp_test_helper(bxp_kwargs=dict(patch_artist=True)) + + +@image_comparison(['bxp_custompatchartist.png'], + remove_text=True, + savefig_kwarg={'dpi': 100}, + style='default') +def test_bxp_custompatchartist(): + _bxp_test_helper(bxp_kwargs=dict( + patch_artist=True, + boxprops=dict(facecolor='yellow', edgecolor='green', ls=':'))) + + +@image_comparison(['bxp_customoutlier.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_customoutlier(): + _bxp_test_helper(bxp_kwargs=dict( + flierprops=dict(linestyle='none', marker='d', mfc='g'))) + + +@image_comparison(['bxp_withmean_custompoint.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_showcustommean(): + _bxp_test_helper(bxp_kwargs=dict( + showmeans=True, + meanprops=dict(linestyle='none', marker='d', mfc='green'), + )) + + +@image_comparison(['bxp_custombox.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_custombox(): + _bxp_test_helper(bxp_kwargs=dict( + boxprops=dict(linestyle='--', color='b', lw=3))) + + +@image_comparison(['bxp_custommedian.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_custommedian(): + _bxp_test_helper(bxp_kwargs=dict( + medianprops=dict(linestyle='--', color='b', lw=3))) + + +@image_comparison(['bxp_customcap.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_customcap(): + _bxp_test_helper(bxp_kwargs=dict( + capprops=dict(linestyle='--', color='g', lw=3))) + + +@image_comparison(['bxp_customwhisker.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_customwhisker(): + _bxp_test_helper(bxp_kwargs=dict( + whiskerprops=dict(linestyle='-', color='m', lw=3))) + + +@check_figures_equal(extensions=['png']) +def test_boxplot_median_bound_by_box(fig_test, fig_ref): + data = np.arange(3) + medianprops_test = {"linewidth": 12} + medianprops_ref = {**medianprops_test, "solid_capstyle": "butt"} + fig_test.subplots().boxplot(data, medianprops=medianprops_test) + fig_ref.subplots().boxplot(data, medianprops=medianprops_ref) + + +@image_comparison(['bxp_withnotch.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_shownotches(): + _bxp_test_helper(bxp_kwargs=dict(shownotches=True)) + + +@image_comparison(['bxp_nocaps.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_nocaps(): + _bxp_test_helper(bxp_kwargs=dict(showcaps=False)) + + +@image_comparison(['bxp_nobox.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_nobox(): + _bxp_test_helper(bxp_kwargs=dict(showbox=False)) + + +@image_comparison(['bxp_no_flier_stats.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_no_flier_stats(): + def transform(stats): + for s in stats: + s.pop('fliers', None) + return stats + + _bxp_test_helper(transform_stats=transform, + bxp_kwargs=dict(showfliers=False)) + + +@image_comparison(['bxp_withmean_point.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_showmean(): + _bxp_test_helper(bxp_kwargs=dict(showmeans=True, meanline=False)) + + +@image_comparison(['bxp_withmean_line.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_showmeanasline(): + _bxp_test_helper(bxp_kwargs=dict(showmeans=True, meanline=True)) + + +@image_comparison(['bxp_scalarwidth.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_scalarwidth(): + _bxp_test_helper(bxp_kwargs=dict(widths=.25)) + + +@image_comparison(['bxp_customwidths.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_customwidths(): + _bxp_test_helper(bxp_kwargs=dict(widths=[0.10, 0.25, 0.65, 0.85])) + + +@image_comparison(['bxp_custompositions.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_custompositions(): + _bxp_test_helper(bxp_kwargs=dict(positions=[1, 5, 6, 7])) + + +def test_bxp_bad_widths(): + with pytest.raises(ValueError): + _bxp_test_helper(bxp_kwargs=dict(widths=[1])) + + +def test_bxp_bad_positions(): + with pytest.raises(ValueError): + _bxp_test_helper(bxp_kwargs=dict(positions=[2, 3])) + + +@image_comparison(['bxp_custom_capwidths.png'], + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_custom_capwidths(): + _bxp_test_helper(bxp_kwargs=dict(capwidths=[0.0, 0.1, 0.5, 1.0])) + + +@image_comparison(['bxp_custom_capwidth.png'], + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_custom_capwidth(): + _bxp_test_helper(bxp_kwargs=dict(capwidths=0.6)) + + +def test_bxp_bad_capwidths(): + with pytest.raises(ValueError): + _bxp_test_helper(bxp_kwargs=dict(capwidths=[1])) + + +@image_comparison(['boxplot.png', 'boxplot.png'], tol=1.28, style='default') +def test_boxplot(): + # Randomness used for bootstrapping. + np.random.seed(937) + + x = np.linspace(-7, 7, 140) + x = np.hstack([-25, x, 25]) + fig, ax = plt.subplots() + + ax.boxplot([x, x], bootstrap=10000, notch=1) + ax.set_ylim((-30, 30)) + + # Reuse testcase from above for a labeled data test + data = {"x": [x, x]} + fig, ax = plt.subplots() + ax.boxplot("x", bootstrap=10000, notch=1, data=data) + ax.set_ylim((-30, 30)) + + +@check_figures_equal(extensions=["png"]) +def test_boxplot_masked(fig_test, fig_ref): + # Check that masked values are ignored when plotting a boxplot + x_orig = np.linspace(-1, 1, 200) + + ax = fig_test.subplots() + x = x_orig[x_orig >= 0] + ax.boxplot(x) + + x = np.ma.masked_less(x_orig, 0) + ax = fig_ref.subplots() + ax.boxplot(x) + + +@image_comparison(['boxplot_custom_capwidths.png'], + savefig_kwarg={'dpi': 40}, style='default') +def test_boxplot_custom_capwidths(): + + x = np.linspace(-7, 7, 140) + x = np.hstack([-25, x, 25]) + fig, ax = plt.subplots() + + ax.boxplot([x, x], notch=1, capwidths=[0.01, 0.2]) + + +@image_comparison(['boxplot_sym2.png'], remove_text=True, style='default') +def test_boxplot_sym2(): + # Randomness used for bootstrapping. + np.random.seed(937) + + x = np.linspace(-7, 7, 140) + x = np.hstack([-25, x, 25]) + fig, [ax1, ax2] = plt.subplots(1, 2) + + ax1.boxplot([x, x], bootstrap=10000, sym='^') + ax1.set_ylim((-30, 30)) + + ax2.boxplot([x, x], bootstrap=10000, sym='g') + ax2.set_ylim((-30, 30)) + + +@image_comparison(['boxplot_sym.png'], + remove_text=True, + savefig_kwarg={'dpi': 40}, + style='default') +def test_boxplot_sym(): + x = np.linspace(-7, 7, 140) + x = np.hstack([-25, x, 25]) + fig, ax = plt.subplots() + + ax.boxplot([x, x], sym='gs') + ax.set_ylim((-30, 30)) + + +@image_comparison(['boxplot_autorange_false_whiskers.png', + 'boxplot_autorange_true_whiskers.png'], + style='default') +def test_boxplot_autorange_whiskers(): + # Randomness used for bootstrapping. + np.random.seed(937) + + x = np.ones(140) + x = np.hstack([0, x, 2]) + + fig1, ax1 = plt.subplots() + ax1.boxplot([x, x], bootstrap=10000, notch=1) + ax1.set_ylim((-5, 5)) + + fig2, ax2 = plt.subplots() + ax2.boxplot([x, x], bootstrap=10000, notch=1, autorange=True) + ax2.set_ylim((-5, 5)) + + +def _rc_test_bxp_helper(ax, rc_dict): + x = np.linspace(-7, 7, 140) + x = np.hstack([-25, x, 25]) + with matplotlib.rc_context(rc_dict): + ax.boxplot([x, x]) + return ax + + +@image_comparison(['boxplot_rc_parameters.png'], + savefig_kwarg={'dpi': 100}, remove_text=True, + tol=1, style='default') +def test_boxplot_rc_parameters(): + # Randomness used for bootstrapping. + np.random.seed(937) + + fig, ax = plt.subplots(3) + + rc_axis0 = { + 'boxplot.notch': True, + 'boxplot.whiskers': [5, 95], + 'boxplot.bootstrap': 10000, + + 'boxplot.flierprops.color': 'b', + 'boxplot.flierprops.marker': 'o', + 'boxplot.flierprops.markerfacecolor': 'g', + 'boxplot.flierprops.markeredgecolor': 'b', + 'boxplot.flierprops.markersize': 5, + 'boxplot.flierprops.linestyle': '--', + 'boxplot.flierprops.linewidth': 2.0, + + 'boxplot.boxprops.color': 'r', + 'boxplot.boxprops.linewidth': 2.0, + 'boxplot.boxprops.linestyle': '--', + + 'boxplot.capprops.color': 'c', + 'boxplot.capprops.linewidth': 2.0, + 'boxplot.capprops.linestyle': '--', + + 'boxplot.medianprops.color': 'k', + 'boxplot.medianprops.linewidth': 2.0, + 'boxplot.medianprops.linestyle': '--', + } + + rc_axis1 = { + 'boxplot.whiskers': [0, 100], + 'boxplot.patchartist': True, + } + + rc_axis2 = { + 'boxplot.whiskers': 2.0, + 'boxplot.showcaps': False, + 'boxplot.showbox': False, + 'boxplot.showfliers': False, + 'boxplot.showmeans': True, + 'boxplot.meanline': True, + + 'boxplot.meanprops.color': 'c', + 'boxplot.meanprops.linewidth': 2.0, + 'boxplot.meanprops.linestyle': '--', + + 'boxplot.whiskerprops.color': 'r', + 'boxplot.whiskerprops.linewidth': 2.0, + 'boxplot.whiskerprops.linestyle': '-.', + } + dict_list = [rc_axis0, rc_axis1, rc_axis2] + for axis, rc_axis in zip(ax, dict_list): + _rc_test_bxp_helper(axis, rc_axis) + + assert (matplotlib.patches.PathPatch in + [type(t) for t in ax[1].get_children()]) + + +@image_comparison(['boxplot_with_CIarray.png'], + remove_text=True, savefig_kwarg={'dpi': 40}, style='default') +def test_boxplot_with_CIarray(): + # Randomness used for bootstrapping. + np.random.seed(937) + + x = np.linspace(-7, 7, 140) + x = np.hstack([-25, x, 25]) + fig, ax = plt.subplots() + CIs = np.array([[-1.5, 3.], [-1., 3.5]]) + + # show a boxplot with Matplotlib medians and confidence intervals, and + # another with manual values + ax.boxplot([x, x], bootstrap=10000, usermedians=[None, 1.0], + conf_intervals=CIs, notch=1) + ax.set_ylim((-30, 30)) + + +@image_comparison(['boxplot_no_inverted_whisker.png'], + remove_text=True, savefig_kwarg={'dpi': 40}, style='default') +def test_boxplot_no_weird_whisker(): + x = np.array([3, 9000, 150, 88, 350, 200000, 1400, 960], + dtype=np.float64) + ax1 = plt.axes() + ax1.boxplot(x) + ax1.set_yscale('log') + ax1.yaxis.grid(False, which='minor') + ax1.xaxis.grid(False) + + +def test_boxplot_bad_medians(): + x = np.linspace(-7, 7, 140) + x = np.hstack([-25, x, 25]) + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.boxplot(x, usermedians=[1, 2]) + with pytest.raises(ValueError): + ax.boxplot([x, x], usermedians=[[1, 2], [1, 2]]) + + +def test_boxplot_bad_ci(): + x = np.linspace(-7, 7, 140) + x = np.hstack([-25, x, 25]) + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.boxplot([x, x], conf_intervals=[[1, 2]]) + with pytest.raises(ValueError): + ax.boxplot([x, x], conf_intervals=[[1, 2], [1]]) + + +def test_boxplot_zorder(): + x = np.arange(10) + fix, ax = plt.subplots() + assert ax.boxplot(x)['boxes'][0].get_zorder() == 2 + assert ax.boxplot(x, zorder=10)['boxes'][0].get_zorder() == 10 + + +def test_boxplot_marker_behavior(): + plt.rcParams['lines.marker'] = 's' + plt.rcParams['boxplot.flierprops.marker'] = 'o' + plt.rcParams['boxplot.meanprops.marker'] = '^' + fig, ax = plt.subplots() + test_data = np.arange(100) + test_data[-1] = 150 # a flier point + bxp_handle = ax.boxplot(test_data, showmeans=True) + for bxp_lines in ['whiskers', 'caps', 'boxes', 'medians']: + for each_line in bxp_handle[bxp_lines]: + # Ensure that the rcParams['lines.marker'] is overridden by '' + assert each_line.get_marker() == '' + + # Ensure that markers for fliers and means aren't overridden with '' + assert bxp_handle['fliers'][0].get_marker() == 'o' + assert bxp_handle['means'][0].get_marker() == '^' + + +@image_comparison(['boxplot_mod_artists_after_plotting.png'], + remove_text=True, savefig_kwarg={'dpi': 40}, style='default') +def test_boxplot_mod_artist_after_plotting(): + x = [0.15, 0.11, 0.06, 0.06, 0.12, 0.56, -0.56] + fig, ax = plt.subplots() + bp = ax.boxplot(x, sym="o") + for key in bp: + for obj in bp[key]: + obj.set_color('green') + + +@image_comparison(['violinplot_vert_baseline.png', + 'violinplot_vert_baseline.png']) +def test_vert_violinplot_baseline(): + # First 9 digits of frac(sqrt(2)) + np.random.seed(414213562) + data = [np.random.normal(size=100) for _ in range(4)] + ax = plt.axes() + ax.violinplot(data, positions=range(4), showmeans=False, showextrema=False, + showmedians=False) + + # Reuse testcase from above for a labeled data test + data = {"d": data} + fig, ax = plt.subplots() + ax.violinplot("d", positions=range(4), showmeans=False, showextrema=False, + showmedians=False, data=data) + + +@image_comparison(['violinplot_vert_showmeans.png']) +def test_vert_violinplot_showmeans(): + ax = plt.axes() + # First 9 digits of frac(sqrt(3)) + np.random.seed(732050807) + data = [np.random.normal(size=100) for _ in range(4)] + ax.violinplot(data, positions=range(4), showmeans=True, showextrema=False, + showmedians=False) + + +@image_comparison(['violinplot_vert_showextrema.png']) +def test_vert_violinplot_showextrema(): + ax = plt.axes() + # First 9 digits of frac(sqrt(5)) + np.random.seed(236067977) + data = [np.random.normal(size=100) for _ in range(4)] + ax.violinplot(data, positions=range(4), showmeans=False, showextrema=True, + showmedians=False) + + +@image_comparison(['violinplot_vert_showmedians.png']) +def test_vert_violinplot_showmedians(): + ax = plt.axes() + # First 9 digits of frac(sqrt(7)) + np.random.seed(645751311) + data = [np.random.normal(size=100) for _ in range(4)] + ax.violinplot(data, positions=range(4), showmeans=False, showextrema=False, + showmedians=True) + + +@image_comparison(['violinplot_vert_showall.png']) +def test_vert_violinplot_showall(): + ax = plt.axes() + # First 9 digits of frac(sqrt(11)) + np.random.seed(316624790) + data = [np.random.normal(size=100) for _ in range(4)] + ax.violinplot(data, positions=range(4), showmeans=True, showextrema=True, + showmedians=True, + quantiles=[[0.1, 0.9], [0.2, 0.8], [0.3, 0.7], [0.4, 0.6]]) + + +@image_comparison(['violinplot_vert_custompoints_10.png']) +def test_vert_violinplot_custompoints_10(): + ax = plt.axes() + # First 9 digits of frac(sqrt(13)) + np.random.seed(605551275) + data = [np.random.normal(size=100) for _ in range(4)] + ax.violinplot(data, positions=range(4), showmeans=False, showextrema=False, + showmedians=False, points=10) + + +@image_comparison(['violinplot_vert_custompoints_200.png']) +def test_vert_violinplot_custompoints_200(): + ax = plt.axes() + # First 9 digits of frac(sqrt(17)) + np.random.seed(123105625) + data = [np.random.normal(size=100) for _ in range(4)] + ax.violinplot(data, positions=range(4), showmeans=False, showextrema=False, + showmedians=False, points=200) + + +@image_comparison(['violinplot_horiz_baseline.png']) +def test_horiz_violinplot_baseline(): + ax = plt.axes() + # First 9 digits of frac(sqrt(19)) + np.random.seed(358898943) + data = [np.random.normal(size=100) for _ in range(4)] + ax.violinplot(data, positions=range(4), orientation='horizontal', showmeans=False, + showextrema=False, showmedians=False) + + +@image_comparison(['violinplot_horiz_showmedians.png']) +def test_horiz_violinplot_showmedians(): + ax = plt.axes() + # First 9 digits of frac(sqrt(23)) + np.random.seed(795831523) + data = [np.random.normal(size=100) for _ in range(4)] + ax.violinplot(data, positions=range(4), orientation='horizontal', showmeans=False, + showextrema=False, showmedians=True) + + +@image_comparison(['violinplot_horiz_showmeans.png']) +def test_horiz_violinplot_showmeans(): + ax = plt.axes() + # First 9 digits of frac(sqrt(29)) + np.random.seed(385164807) + data = [np.random.normal(size=100) for _ in range(4)] + ax.violinplot(data, positions=range(4), orientation='horizontal', showmeans=True, + showextrema=False, showmedians=False) + + +@image_comparison(['violinplot_horiz_showextrema.png']) +def test_horiz_violinplot_showextrema(): + ax = plt.axes() + # First 9 digits of frac(sqrt(31)) + np.random.seed(567764362) + data = [np.random.normal(size=100) for _ in range(4)] + ax.violinplot(data, positions=range(4), orientation='horizontal', showmeans=False, + showextrema=True, showmedians=False) + + +@image_comparison(['violinplot_horiz_showall.png']) +def test_horiz_violinplot_showall(): + ax = plt.axes() + # First 9 digits of frac(sqrt(37)) + np.random.seed(82762530) + data = [np.random.normal(size=100) for _ in range(4)] + ax.violinplot(data, positions=range(4), orientation='horizontal', showmeans=True, + showextrema=True, showmedians=True, + quantiles=[[0.1, 0.9], [0.2, 0.8], [0.3, 0.7], [0.4, 0.6]]) + + +@image_comparison(['violinplot_horiz_custompoints_10.png']) +def test_horiz_violinplot_custompoints_10(): + ax = plt.axes() + # First 9 digits of frac(sqrt(41)) + np.random.seed(403124237) + data = [np.random.normal(size=100) for _ in range(4)] + ax.violinplot(data, positions=range(4), orientation='horizontal', showmeans=False, + showextrema=False, showmedians=False, points=10) + + +@image_comparison(['violinplot_horiz_custompoints_200.png']) +def test_horiz_violinplot_custompoints_200(): + ax = plt.axes() + # First 9 digits of frac(sqrt(43)) + np.random.seed(557438524) + data = [np.random.normal(size=100) for _ in range(4)] + ax.violinplot(data, positions=range(4), orientation='horizontal', showmeans=False, + showextrema=False, showmedians=False, points=200) + + +@image_comparison(['violinplot_sides.png'], remove_text=True, style='mpl20') +def test_violinplot_sides(): + ax = plt.axes() + np.random.seed(19680801) + data = [np.random.normal(size=100)] + # Check horizontal violinplot + for pos, side in zip([0, -0.5, 0.5], ['both', 'low', 'high']): + ax.violinplot(data, positions=[pos], orientation='horizontal', showmeans=False, + showextrema=True, showmedians=True, side=side) + # Check vertical violinplot + for pos, side in zip([4, 3.5, 4.5], ['both', 'low', 'high']): + ax.violinplot(data, positions=[pos], orientation='vertical', showmeans=False, + showextrema=True, showmedians=True, side=side) + + +def test_violinplot_bad_positions(): + ax = plt.axes() + # First 9 digits of frac(sqrt(47)) + np.random.seed(855654600) + data = [np.random.normal(size=100) for _ in range(4)] + with pytest.raises(ValueError): + ax.violinplot(data, positions=range(5)) + + +def test_violinplot_bad_widths(): + ax = plt.axes() + # First 9 digits of frac(sqrt(53)) + np.random.seed(280109889) + data = [np.random.normal(size=100) for _ in range(4)] + with pytest.raises(ValueError): + ax.violinplot(data, positions=range(4), widths=[1, 2, 3]) + + +def test_violinplot_bad_quantiles(): + ax = plt.axes() + # First 9 digits of frac(sqrt(73)) + np.random.seed(544003745) + data = [np.random.normal(size=100)] + + # Different size quantile list and plots + with pytest.raises(ValueError): + ax.violinplot(data, quantiles=[[0.1, 0.2], [0.5, 0.7]]) + + +def test_violinplot_outofrange_quantiles(): + ax = plt.axes() + # First 9 digits of frac(sqrt(79)) + np.random.seed(888194417) + data = [np.random.normal(size=100)] + + # Quantile value above 100 + with pytest.raises(ValueError): + ax.violinplot(data, quantiles=[[0.1, 0.2, 0.3, 1.05]]) + + # Quantile value below 0 + with pytest.raises(ValueError): + ax.violinplot(data, quantiles=[[-0.05, 0.2, 0.3, 0.75]]) + + +@check_figures_equal(extensions=["png"]) +def test_violinplot_single_list_quantiles(fig_test, fig_ref): + # Ensures quantile list for 1D can be passed in as single list + # First 9 digits of frac(sqrt(83)) + np.random.seed(110433579) + data = [np.random.normal(size=100)] + + # Test image + ax = fig_test.subplots() + ax.violinplot(data, quantiles=[0.1, 0.3, 0.9]) + + # Reference image + ax = fig_ref.subplots() + ax.violinplot(data, quantiles=[[0.1, 0.3, 0.9]]) + + +@check_figures_equal(extensions=["png"]) +def test_violinplot_pandas_series(fig_test, fig_ref, pd): + np.random.seed(110433579) + s1 = pd.Series(np.random.normal(size=7), index=[9, 8, 7, 6, 5, 4, 3]) + s2 = pd.Series(np.random.normal(size=9), index=list('ABCDEFGHI')) + s3 = pd.Series(np.random.normal(size=11)) + fig_test.subplots().violinplot([s1, s2, s3]) + fig_ref.subplots().violinplot([s1.values, s2.values, s3.values]) + + +def test_manage_xticks(): + _, ax = plt.subplots() + ax.set_xlim(0, 4) + old_xlim = ax.get_xlim() + np.random.seed(0) + y1 = np.random.normal(10, 3, 20) + y2 = np.random.normal(3, 1, 20) + ax.boxplot([y1, y2], positions=[1, 2], manage_ticks=False) + new_xlim = ax.get_xlim() + assert_array_equal(old_xlim, new_xlim) + + +def test_boxplot_not_single(): + fig, ax = plt.subplots() + ax.boxplot(np.random.rand(100), positions=[3]) + ax.boxplot(np.random.rand(100), positions=[5]) + fig.canvas.draw() + assert ax.get_xlim() == (2.5, 5.5) + assert list(ax.get_xticks()) == [3, 5] + assert [t.get_text() for t in ax.get_xticklabels()] == ["3", "5"] + + +def test_tick_space_size_0(): + # allow font size to be zero, which affects ticks when there is + # no other text in the figure. + plt.plot([0, 1], [0, 1]) + matplotlib.rcParams.update({'font.size': 0}) + b = io.BytesIO() + plt.savefig(b, dpi=80, format='raw') + + +@image_comparison(['errorbar_basic.png', 'errorbar_mixed.png', 'errorbar_basic.png']) +def test_errorbar(): + # longdouble due to floating point rounding issues with certain + # computer chipsets + x = np.arange(0.1, 4, 0.5, dtype=np.longdouble) + y = np.exp(-x) + + yerr = 0.1 + 0.2*np.sqrt(x) + xerr = 0.1 + yerr + + # First illustrate basic pyplot interface, using defaults where possible. + fig = plt.figure() + ax = fig.gca() + ax.errorbar(x, y, xerr=0.2, yerr=0.4) + ax.set_title("Simplest errorbars, 0.2 in x, 0.4 in y") + + # Now switch to a more OO interface to exercise more features. + fig, axs = plt.subplots(nrows=2, ncols=2, sharex=True) + ax = axs[0, 0] + ax.errorbar(x, y, yerr=yerr, fmt='o') + ax.set_title('Vert. symmetric') + + # With 4 subplots, reduce the number of axis ticks to avoid crowding. + ax.locator_params(nbins=4) + + ax = axs[0, 1] + ax.errorbar(x, y, xerr=xerr, fmt='o', alpha=0.4) + ax.set_title('Hor. symmetric w/ alpha') + + ax = axs[1, 0] + ax.errorbar(x, y, yerr=[yerr, 2*yerr], xerr=[xerr, 2*xerr], fmt='--o') + ax.set_title('H, V asymmetric') + + ax = axs[1, 1] + ax.set_yscale('log') + # Here we have to be careful to keep all y values positive: + ylower = np.maximum(1e-2, y - yerr) + yerr_lower = y - ylower + + ax.errorbar(x, y, yerr=[yerr_lower, 2*yerr], xerr=xerr, + fmt='o', ecolor='g', capthick=2) + ax.set_title('Mixed sym., log y') + # Force limits due to floating point slop potentially expanding the range + ax.set_ylim(1e-2, 1e1) + + fig.suptitle('Variable errorbars') + + # Reuse the first testcase from above for a labeled data test + data = {"x": x, "y": y} + fig = plt.figure() + ax = fig.gca() + ax.errorbar("x", "y", xerr=0.2, yerr=0.4, data=data) + ax.set_title("Simplest errorbars, 0.2 in x, 0.4 in y") + + +@image_comparison(['mixed_errorbar_polar_caps.png'], remove_text=True) +def test_mixed_errorbar_polar_caps(): + """ + Mix several polar errorbar use cases in a single test figure. + + It is advisable to position individual points off the grid. If there are + problems with reproducibility of this test, consider removing grid. + """ + fig = plt.figure() + ax = plt.subplot(111, projection='polar') + + # symmetric errorbars + th_sym = [1, 2, 3] + r_sym = [0.9]*3 + ax.errorbar(th_sym, r_sym, xerr=0.35, yerr=0.2, fmt="o") + + # long errorbars + th_long = [np.pi/2 + .1, np.pi + .1] + r_long = [1.8, 2.2] + ax.errorbar(th_long, r_long, xerr=0.8 * np.pi, yerr=0.15, fmt="o") + + # asymmetric errorbars + th_asym = [4*np.pi/3 + .1, 5*np.pi/3 + .1, 2*np.pi-0.1] + r_asym = [1.1]*3 + xerr = [[.3, .3, .2], [.2, .3, .3]] + yerr = [[.35, .5, .5], [.5, .35, .5]] + ax.errorbar(th_asym, r_asym, xerr=xerr, yerr=yerr, fmt="o") + + # overlapping errorbar + th_over = [2.1] + r_over = [3.1] + ax.errorbar(th_over, r_over, xerr=10, yerr=.2, fmt="o") + + +def test_errorbar_colorcycle(): + + f, ax = plt.subplots() + x = np.arange(10) + y = 2*x + + e1, _, _ = ax.errorbar(x, y, c=None) + e2, _, _ = ax.errorbar(x, 2*y, c=None) + ln1, = ax.plot(x, 4*y) + + assert mcolors.to_rgba(e1.get_color()) == mcolors.to_rgba('C0') + assert mcolors.to_rgba(e2.get_color()) == mcolors.to_rgba('C1') + assert mcolors.to_rgba(ln1.get_color()) == mcolors.to_rgba('C2') + + +@check_figures_equal(extensions=['png']) +def test_errorbar_cycle_ecolor(fig_test, fig_ref): + x = np.arange(0.1, 4, 0.5) + y = [np.exp(-x+n) for n in range(4)] + + axt = fig_test.subplots() + axr = fig_ref.subplots() + + for yi, color in zip(y, ['C0', 'C1', 'C2', 'C3']): + axt.errorbar(x, yi, yerr=(yi * 0.25), linestyle='-', + marker='o', ecolor='black') + axr.errorbar(x, yi, yerr=(yi * 0.25), linestyle='-', + marker='o', color=color, ecolor='black') + + +def test_errorbar_shape(): + fig = plt.figure() + ax = fig.gca() + + x = np.arange(0.1, 4, 0.5) + y = np.exp(-x) + yerr1 = 0.1 + 0.2*np.sqrt(x) + yerr = np.vstack((yerr1, 2*yerr1)).T + xerr = 0.1 + yerr + + with pytest.raises(ValueError): + ax.errorbar(x, y, yerr=yerr, fmt='o') + with pytest.raises(ValueError): + ax.errorbar(x, y, xerr=xerr, fmt='o') + with pytest.raises(ValueError): + ax.errorbar(x, y, yerr=yerr, xerr=xerr, fmt='o') + + +@image_comparison(['errorbar_limits.png']) +def test_errorbar_limits(): + x = np.arange(0.5, 5.5, 0.5) + y = np.exp(-x) + xerr = 0.1 + yerr = 0.2 + ls = 'dotted' + + fig, ax = plt.subplots() + + # standard error bars + ax.errorbar(x, y, xerr=xerr, yerr=yerr, ls=ls, color='blue') + + # including upper limits + uplims = np.zeros_like(x) + uplims[[1, 5, 9]] = True + ax.errorbar(x, y+0.5, xerr=xerr, yerr=yerr, uplims=uplims, ls=ls, + color='green') + + # including lower limits + lolims = np.zeros_like(x) + lolims[[2, 4, 8]] = True + ax.errorbar(x, y+1.0, xerr=xerr, yerr=yerr, lolims=lolims, ls=ls, + color='red') + + # including upper and lower limits + ax.errorbar(x, y+1.5, marker='o', ms=8, xerr=xerr, yerr=yerr, + lolims=lolims, uplims=uplims, ls=ls, color='magenta') + + # including xlower and xupper limits + xerr = 0.2 + yerr = np.full_like(x, 0.2) + yerr[[3, 6]] = 0.3 + xlolims = lolims + xuplims = uplims + lolims = np.zeros_like(x) + uplims = np.zeros_like(x) + lolims[[6]] = True + uplims[[3]] = True + ax.errorbar(x, y+2.1, marker='o', ms=8, xerr=xerr, yerr=yerr, + xlolims=xlolims, xuplims=xuplims, uplims=uplims, + lolims=lolims, ls='none', mec='blue', capsize=0, + color='cyan') + ax.set_xlim((0, 5.5)) + ax.set_title('Errorbar upper and lower limits') + + +def test_errorbar_nonefmt(): + # Check that passing 'none' as a format still plots errorbars + x = np.arange(5) + y = np.arange(5) + + plotline, _, barlines = plt.errorbar(x, y, xerr=1, yerr=1, fmt='none') + assert plotline is None + for errbar in barlines: + assert np.all(errbar.get_color() == mcolors.to_rgba('C0')) + + +def test_errorbar_remove(): + x = np.arange(5) + y = np.arange(5) + + fig, ax = plt.subplots() + ec = ax.errorbar(x, y, xerr=1, yerr=1) + + assert len(ax.containers) == 1 + assert len(ax.lines) == 5 + assert len(ax.collections) == 2 + + ec.remove() + + assert not ax.containers + assert not ax.lines + assert not ax.collections + + +def test_errorbar_line_specific_kwargs(): + # Check that passing line-specific keyword arguments will not result in + # errors. + x = np.arange(5) + y = np.arange(5) + + plotline, _, _ = plt.errorbar(x, y, xerr=1, yerr=1, ls='None', + marker='s', fillstyle='full', + drawstyle='steps-mid', + dash_capstyle='round', + dash_joinstyle='miter', + solid_capstyle='butt', + solid_joinstyle='bevel') + assert plotline.get_fillstyle() == 'full' + assert plotline.get_drawstyle() == 'steps-mid' + + +@check_figures_equal(extensions=['png']) +def test_errorbar_with_prop_cycle(fig_test, fig_ref): + ax = fig_ref.subplots() + ax.errorbar(x=[2, 4, 10], y=[0, 1, 2], yerr=0.5, + ls='--', marker='s', mfc='k') + ax.errorbar(x=[2, 4, 10], y=[2, 3, 4], yerr=0.5, color='tab:green', + ls=':', marker='s', mfc='y') + ax.errorbar(x=[2, 4, 10], y=[4, 5, 6], yerr=0.5, fmt='tab:blue', + ls='-.', marker='o', mfc='c') + ax.set_xlim(1, 11) + + _cycle = cycler(ls=['--', ':', '-.'], marker=['s', 's', 'o'], + mfc=['k', 'y', 'c'], color=['b', 'g', 'r']) + plt.rc("axes", prop_cycle=_cycle) + ax = fig_test.subplots() + ax.errorbar(x=[2, 4, 10], y=[0, 1, 2], yerr=0.5) + ax.errorbar(x=[2, 4, 10], y=[2, 3, 4], yerr=0.5, color='tab:green') + ax.errorbar(x=[2, 4, 10], y=[4, 5, 6], yerr=0.5, fmt='tab:blue') + ax.set_xlim(1, 11) + + +def test_errorbar_every_invalid(): + x = np.linspace(0, 1, 15) + y = x * (1-x) + yerr = y/6 + + ax = plt.figure().subplots() + + with pytest.raises(ValueError, match='not a tuple of two integers'): + ax.errorbar(x, y, yerr, errorevery=(1, 2, 3)) + with pytest.raises(ValueError, match='not a tuple of two integers'): + ax.errorbar(x, y, yerr, errorevery=(1.3, 3)) + with pytest.raises(ValueError, match='not a valid NumPy fancy index'): + ax.errorbar(x, y, yerr, errorevery=[False, True]) + with pytest.raises(ValueError, match='not a recognized value'): + ax.errorbar(x, y, yerr, errorevery='foobar') + + +def test_xerr_yerr_not_negative(): + ax = plt.figure().subplots() + + with pytest.raises(ValueError, + match="'xerr' must not contain negative values"): + ax.errorbar(x=[0], y=[0], xerr=[[-0.5], [1]], yerr=[[-0.5], [1]]) + with pytest.raises(ValueError, + match="'xerr' must not contain negative values"): + ax.errorbar(x=[0], y=[0], xerr=[[-0.5], [1]]) + with pytest.raises(ValueError, + match="'yerr' must not contain negative values"): + ax.errorbar(x=[0], y=[0], yerr=[[-0.5], [1]]) + with pytest.raises(ValueError, + match="'yerr' must not contain negative values"): + x = np.arange(5) + y = [datetime.datetime(2021, 9, i * 2 + 1) for i in x] + ax.errorbar(x=x, + y=y, + yerr=datetime.timedelta(days=-10)) + + +def test_xerr_yerr_not_none(): + ax = plt.figure().subplots() + + with pytest.raises(ValueError, + match="'xerr' must not contain None"): + ax.errorbar(x=[0], y=[0], xerr=[[None], [1]], yerr=[[None], [1]]) + with pytest.raises(ValueError, + match="'xerr' must not contain None"): + ax.errorbar(x=[0], y=[0], xerr=[[None], [1]]) + with pytest.raises(ValueError, + match="'yerr' must not contain None"): + ax.errorbar(x=[0], y=[0], yerr=[[None], [1]]) + + +@check_figures_equal(extensions=['png']) +def test_errorbar_every(fig_test, fig_ref): + x = np.linspace(0, 1, 15) + y = x * (1-x) + yerr = y/6 + + ax_ref = fig_ref.subplots() + ax_test = fig_test.subplots() + + for color, shift in zip('rgbk', [0, 0, 2, 7]): + y += .02 + + # Check errorevery using an explicit offset and step. + ax_test.errorbar(x, y, yerr, errorevery=(shift, 4), + capsize=4, c=color) + + # Using manual errorbars + # n.b. errorbar draws the main plot at z=2.1 by default + ax_ref.plot(x, y, c=color, zorder=2.1) + ax_ref.errorbar(x[shift::4], y[shift::4], yerr[shift::4], + capsize=4, c=color, fmt='none') + + # Check that markevery is propagated to line, without affecting errorbars. + ax_test.errorbar(x, y + 0.1, yerr, markevery=(1, 4), capsize=4, fmt='o') + ax_ref.plot(x[1::4], y[1::4] + 0.1, 'o', zorder=2.1) + ax_ref.errorbar(x, y + 0.1, yerr, capsize=4, fmt='none') + + # Check that passing a slice to markevery/errorevery works. + ax_test.errorbar(x, y + 0.2, yerr, errorevery=slice(2, None, 3), + markevery=slice(2, None, 3), + capsize=4, c='C0', fmt='o') + ax_ref.plot(x[2::3], y[2::3] + 0.2, 'o', c='C0', zorder=2.1) + ax_ref.errorbar(x[2::3], y[2::3] + 0.2, yerr[2::3], + capsize=4, c='C0', fmt='none') + + # Check that passing an iterable to markevery/errorevery works. + ax_test.errorbar(x, y + 0.2, yerr, errorevery=[False, True, False] * 5, + markevery=[False, True, False] * 5, + capsize=4, c='C1', fmt='o') + ax_ref.plot(x[1::3], y[1::3] + 0.2, 'o', c='C1', zorder=2.1) + ax_ref.errorbar(x[1::3], y[1::3] + 0.2, yerr[1::3], + capsize=4, c='C1', fmt='none') + + +@pytest.mark.parametrize('elinewidth', [[1, 2, 3], + np.array([1, 2, 3]), + 1]) +def test_errorbar_linewidth_type(elinewidth): + plt.errorbar([1, 2, 3], [1, 2, 3], yerr=[1, 2, 3], elinewidth=elinewidth) + + +@check_figures_equal(extensions=["png"]) +def test_errorbar_nan(fig_test, fig_ref): + ax = fig_test.add_subplot() + xs = range(5) + ys = np.array([1, 2, np.nan, np.nan, 3]) + es = np.array([4, 5, np.nan, np.nan, 6]) + ax.errorbar(xs, ys, yerr=es) + ax = fig_ref.add_subplot() + ax.errorbar([0, 1], [1, 2], yerr=[4, 5]) + ax.errorbar([4], [3], yerr=[6], fmt="C0") + + +@check_figures_equal() +def test_errorbar_masked_negative(fig_test, fig_ref): + ax = fig_test.add_subplot() + xs = range(5) + mask = np.array([False, False, True, True, False]) + ys = np.ma.array([1, 2, 2, 2, 3], mask=mask) + es = np.ma.array([4, 5, -1, -10, 6], mask=mask) + ax.errorbar(xs, ys, yerr=es) + ax = fig_ref.add_subplot() + ax.errorbar([0, 1], [1, 2], yerr=[4, 5]) + ax.errorbar([4], [3], yerr=[6], fmt="C0") + + +@image_comparison(['hist_stacked_stepfilled.png', 'hist_stacked_stepfilled.png']) +def test_hist_stacked_stepfilled(): + # make some data + d1 = np.linspace(1, 3, 20) + d2 = np.linspace(0, 10, 50) + fig, ax = plt.subplots() + ax.hist((d1, d2), histtype="stepfilled", stacked=True) + + # Reuse testcase from above for a labeled data test + data = {"x": (d1, d2)} + fig, ax = plt.subplots() + ax.hist("x", histtype="stepfilled", stacked=True, data=data) + + +@image_comparison(['hist_offset.png']) +def test_hist_offset(): + # make some data + d1 = np.linspace(0, 10, 50) + d2 = np.linspace(1, 3, 20) + fig, ax = plt.subplots() + ax.hist(d1, bottom=5) + ax.hist(d2, bottom=15) + + +@image_comparison(['hist_step.png'], remove_text=True) +def test_hist_step(): + # make some data + d1 = np.linspace(1, 3, 20) + fig, ax = plt.subplots() + ax.hist(d1, histtype="step") + ax.set_ylim(0, 10) + ax.set_xlim(-1, 5) + + +@image_comparison(['hist_step_horiz.png']) +def test_hist_step_horiz(): + # make some data + d1 = np.linspace(0, 10, 50) + d2 = np.linspace(1, 3, 20) + fig, ax = plt.subplots() + ax.hist((d1, d2), histtype="step", orientation="horizontal") + + +@image_comparison(['hist_stacked_weights.png']) +def test_hist_stacked_weighted(): + # make some data + d1 = np.linspace(0, 10, 50) + d2 = np.linspace(1, 3, 20) + w1 = np.linspace(0.01, 3.5, 50) + w2 = np.linspace(0.05, 2., 20) + fig, ax = plt.subplots() + ax.hist((d1, d2), weights=(w1, w2), histtype="stepfilled", stacked=True) + + +@image_comparison(['stem.png'], style='mpl20', remove_text=True) +def test_stem(): + x = np.linspace(0.1, 2 * np.pi, 100) + + fig, ax = plt.subplots() + # Label is a single space to force a legend to be drawn, but to avoid any + # text being drawn + ax.stem(x, np.cos(x), + linefmt='C2-.', markerfmt='k+', basefmt='C1-.', label=' ') + ax.legend() + + +def test_stem_args(): + """Test that stem() correctly identifies x and y values.""" + def _assert_equal(stem_container, expected): + x, y = map(list, stem_container.markerline.get_data()) + assert x == expected[0] + assert y == expected[1] + + fig, ax = plt.subplots() + + x = [1, 3, 5] + y = [9, 8, 7] + + # Test the call signatures + _assert_equal(ax.stem(y), expected=([0, 1, 2], y)) + _assert_equal(ax.stem(x, y), expected=(x, y)) + _assert_equal(ax.stem(x, y, linefmt='r--'), expected=(x, y)) + _assert_equal(ax.stem(x, y, 'r--'), expected=(x, y)) + _assert_equal(ax.stem(x, y, linefmt='r--', basefmt='b--'), expected=(x, y)) + _assert_equal(ax.stem(y, linefmt='r--'), expected=([0, 1, 2], y)) + _assert_equal(ax.stem(y, 'r--'), expected=([0, 1, 2], y)) + + +def test_stem_markerfmt(): + """Test that stem(..., markerfmt=...) produces the intended markers.""" + def _assert_equal(stem_container, linecolor=None, markercolor=None, + marker=None): + """ + Check that the given StemContainer has the properties listed as + keyword-arguments. + """ + if linecolor is not None: + assert mcolors.same_color( + stem_container.stemlines.get_color(), + linecolor) + if markercolor is not None: + assert mcolors.same_color( + stem_container.markerline.get_color(), + markercolor) + if marker is not None: + assert stem_container.markerline.get_marker() == marker + assert stem_container.markerline.get_linestyle() == 'None' + + fig, ax = plt.subplots() + + x = [1, 3, 5] + y = [9, 8, 7] + + # no linefmt + _assert_equal(ax.stem(x, y), markercolor='C0', marker='o') + _assert_equal(ax.stem(x, y, markerfmt='x'), markercolor='C0', marker='x') + _assert_equal(ax.stem(x, y, markerfmt='rx'), markercolor='r', marker='x') + + # positional linefmt + _assert_equal( + ax.stem(x, y, 'r'), # marker color follows linefmt if not given + linecolor='r', markercolor='r', marker='o') + _assert_equal( + ax.stem(x, y, 'rx'), # the marker is currently not taken from linefmt + linecolor='r', markercolor='r', marker='o') + _assert_equal( + ax.stem(x, y, 'r', markerfmt='x'), # only marker type specified + linecolor='r', markercolor='r', marker='x') + _assert_equal( + ax.stem(x, y, 'r', markerfmt='g'), # only marker color specified + linecolor='r', markercolor='g', marker='o') + _assert_equal( + ax.stem(x, y, 'r', markerfmt='gx'), # marker type and color specified + linecolor='r', markercolor='g', marker='x') + _assert_equal( + ax.stem(x, y, 'r', markerfmt=' '), # markerfmt=' ' for no marker + linecolor='r', markercolor='r', marker='None') + _assert_equal( + ax.stem(x, y, 'r', markerfmt=''), # markerfmt='' for no marker + linecolor='r', markercolor='r', marker='None') + + # with linefmt kwarg + _assert_equal( + ax.stem(x, y, linefmt='r'), + linecolor='r', markercolor='r', marker='o') + _assert_equal( + ax.stem(x, y, linefmt='r', markerfmt='x'), + linecolor='r', markercolor='r', marker='x') + _assert_equal( + ax.stem(x, y, linefmt='r', markerfmt='gx'), + linecolor='r', markercolor='g', marker='x') + + +def test_stem_dates(): + fig, ax = plt.subplots(1, 1) + xs = [dateutil.parser.parse("2013-9-28 11:00:00"), + dateutil.parser.parse("2013-9-28 12:00:00")] + ys = [100, 200] + ax.stem(xs, ys) + + +@image_comparison(['stem_orientation.png'], style='mpl20', remove_text=True) +def test_stem_orientation(): + x = np.linspace(0.1, 2*np.pi, 50) + + fig, ax = plt.subplots() + ax.stem(x, np.cos(x), + linefmt='C2-.', markerfmt='kx', basefmt='C1-.', + orientation='horizontal') + + +@image_comparison(['hist_stacked_stepfilled_alpha.png']) +def test_hist_stacked_stepfilled_alpha(): + # make some data + d1 = np.linspace(1, 3, 20) + d2 = np.linspace(0, 10, 50) + fig, ax = plt.subplots() + ax.hist((d1, d2), histtype="stepfilled", stacked=True, alpha=0.5) + + +@image_comparison(['hist_stacked_step.png']) +def test_hist_stacked_step(): + # make some data + d1 = np.linspace(1, 3, 20) + d2 = np.linspace(0, 10, 50) + fig, ax = plt.subplots() + ax.hist((d1, d2), histtype="step", stacked=True) + + +@image_comparison(['hist_stacked_normed.png']) +def test_hist_stacked_density(): + # make some data + d1 = np.linspace(1, 3, 20) + d2 = np.linspace(0, 10, 50) + fig, ax = plt.subplots() + ax.hist((d1, d2), stacked=True, density=True) + + +@image_comparison(['hist_step_bottom.png'], remove_text=True) +def test_hist_step_bottom(): + # make some data + d1 = np.linspace(1, 3, 20) + fig, ax = plt.subplots() + ax.hist(d1, bottom=np.arange(10), histtype="stepfilled") + + +def test_hist_step_geometry(): + bins = [0, 1, 2, 3] + data = [0, 0, 1, 1, 1, 2] + top = [[0, 0], [0, 2], [1, 2], [1, 3], [2, 3], [2, 1], [3, 1], [3, 0]] + bottom = [[2, 0], [2, 0], [1, 0], [1, 0], [0, 0]] + + for histtype, xy in [('step', top), ('stepfilled', top + bottom)]: + _, _, (polygon, ) = plt.hist(data, bins=bins, histtype=histtype) + assert_array_equal(polygon.get_xy(), xy) + + +def test_hist_step_bottom_geometry(): + bins = [0, 1, 2, 3] + data = [0, 0, 1, 1, 1, 2] + top = [[0, 1], [0, 3], [1, 3], [1, 5], [2, 5], [2, 2.5], [3, 2.5], [3, 1.5]] + bottom = [[2, 1.5], [2, 2], [1, 2], [1, 1], [0, 1]] + + for histtype, xy in [('step', top), ('stepfilled', top + bottom)]: + _, _, (polygon, ) = plt.hist(data, bins=bins, bottom=[1, 2, 1.5], + histtype=histtype) + assert_array_equal(polygon.get_xy(), xy) + + +def test_hist_stacked_step_geometry(): + bins = [0, 1, 2, 3] + data_1 = [0, 0, 1, 1, 1, 2] + data_2 = [0, 1, 2] + tops = [ + [[0, 0], [0, 2], [1, 2], [1, 3], [2, 3], [2, 1], [3, 1], [3, 0]], + [[0, 2], [0, 3], [1, 3], [1, 4], [2, 4], [2, 2], [3, 2], [3, 1]], + ] + bottoms = [ + [[2, 0], [2, 0], [1, 0], [1, 0], [0, 0]], + [[2, 1], [2, 3], [1, 3], [1, 2], [0, 2]], + ] + combined = [t + b for t, b in zip(tops, bottoms)] + + for histtype, xy in [('step', tops), ('stepfilled', combined)]: + _, _, patches = plt.hist([data_1, data_2], bins=bins, stacked=True, + histtype=histtype) + assert len(patches) == 2 + polygon, = patches[0] + assert_array_equal(polygon.get_xy(), xy[0]) + polygon, = patches[1] + assert_array_equal(polygon.get_xy(), xy[1]) + + +def test_hist_stacked_step_bottom_geometry(): + bins = [0, 1, 2, 3] + data_1 = [0, 0, 1, 1, 1, 2] + data_2 = [0, 1, 2] + tops = [ + [[0, 1], [0, 3], [1, 3], [1, 5], [2, 5], [2, 2.5], [3, 2.5], [3, 1.5]], + [[0, 3], [0, 4], [1, 4], [1, 6], [2, 6], [2, 3.5], [3, 3.5], [3, 2.5]], + ] + bottoms = [ + [[2, 1.5], [2, 2], [1, 2], [1, 1], [0, 1]], + [[2, 2.5], [2, 5], [1, 5], [1, 3], [0, 3]], + ] + combined = [t + b for t, b in zip(tops, bottoms)] + + for histtype, xy in [('step', tops), ('stepfilled', combined)]: + _, _, patches = plt.hist([data_1, data_2], bins=bins, stacked=True, + bottom=[1, 2, 1.5], histtype=histtype) + assert len(patches) == 2 + polygon, = patches[0] + assert_array_equal(polygon.get_xy(), xy[0]) + polygon, = patches[1] + assert_array_equal(polygon.get_xy(), xy[1]) + + +@image_comparison(['hist_stacked_bar.png']) +def test_hist_stacked_bar(): + # make some data + d = [[100, 100, 100, 100, 200, 320, 450, 80, 20, 600, 310, 800], + [20, 23, 50, 11, 100, 420], [120, 120, 120, 140, 140, 150, 180], + [60, 60, 60, 60, 300, 300, 5, 5, 5, 5, 10, 300], + [555, 555, 555, 30, 30, 30, 30, 30, 100, 100, 100, 100, 30, 30], + [30, 30, 30, 30, 400, 400, 400, 400, 400, 400, 400, 400]] + colors = [(0.5759849696758961, 1.0, 0.0), (0.0, 1.0, 0.350624650815206), + (0.0, 1.0, 0.6549834156005998), (0.0, 0.6569064625276622, 1.0), + (0.28302699607823545, 0.0, 1.0), (0.6849123462299822, 0.0, 1.0)] + labels = ['green', 'orange', ' yellow', 'magenta', 'black'] + fig, ax = plt.subplots() + ax.hist(d, bins=10, histtype='barstacked', align='mid', color=colors, + label=labels) + ax.legend(loc='upper right', bbox_to_anchor=(1.0, 1.0), ncols=1) + + +@pytest.mark.parametrize('kwargs', ({'facecolor': ["b", "g", "r"]}, + {'edgecolor': ["b", "g", "r"]}, + {'hatch': ["/", "\\", "."]}, + {'linestyle': ["-", "--", ":"]}, + {'linewidth': [1, 1.5, 2]}, + {'color': ["b", "g", "r"]})) +@check_figures_equal(extensions=["png"]) +def test_hist_vectorized_params(fig_test, fig_ref, kwargs): + np.random.seed(19680801) + xs = [np.random.randn(n) for n in [20, 50, 100]] + + (axt1, axt2) = fig_test.subplots(2) + (axr1, axr2) = fig_ref.subplots(2) + + for histtype, axt, axr in [("stepfilled", axt1, axr1), ("step", axt2, axr2)]: + _, bins, _ = axt.hist(xs, bins=10, histtype=histtype, **kwargs) + + kw, values = next(iter(kwargs.items())) + for i, (x, value) in enumerate(zip(xs, values)): + axr.hist(x, bins=bins, histtype=histtype, **{kw: value}, + zorder=(len(xs)-i)/2) + + +def test_hist_sequence_type_styles(): + facecolor = ('r', 0.5) + edgecolor = [0.5, 0.5, 0.5] + linestyle = (0, (1, 1)) + + arr = np.random.uniform(size=50) + _, _, bars = plt.hist(arr, facecolor=facecolor, edgecolor=edgecolor, + linestyle=linestyle) + assert mcolors.same_color(bars[0].get_facecolor(), facecolor) + assert mcolors.same_color(bars[0].get_edgecolor(), edgecolor) + assert bars[0].get_linestyle() == linestyle + + +def test_hist_color_none(): + arr = np.random.uniform(size=50) + # No edgecolor is the default but check that it can be explicitly passed. + _, _, bars = plt.hist(arr, facecolor='none', edgecolor='none') + assert bars[0].get_facecolor(), (0, 0, 0, 0) + assert bars[0].get_edgecolor(), (0, 0, 0, 0) + + +@pytest.mark.parametrize('kwargs, patch_face, patch_edge', + # 'C0'(blue) stands for the first color of the + # default color cycle as well as the patch.facecolor rcParam + # When the expected edgecolor is 'k'(black), + # it corresponds to the patch.edgecolor rcParam + [({'histtype': 'stepfilled', 'color': 'r', + 'facecolor': 'y', 'edgecolor': 'g'}, 'y', 'g'), + ({'histtype': 'step', 'color': 'r', + 'facecolor': 'y', 'edgecolor': 'g'}, ('y', 0), 'g'), + ({'histtype': 'stepfilled', 'color': 'r', + 'edgecolor': 'g'}, 'r', 'g'), + ({'histtype': 'step', 'color': 'r', + 'edgecolor': 'g'}, ('r', 0), 'g'), + ({'histtype': 'stepfilled', 'color': 'r', + 'facecolor': 'y'}, 'y', 'k'), + ({'histtype': 'step', 'color': 'r', + 'facecolor': 'y'}, ('y', 0), 'r'), + ({'histtype': 'stepfilled', + 'facecolor': 'y', 'edgecolor': 'g'}, 'y', 'g'), + ({'histtype': 'step', 'facecolor': 'y', + 'edgecolor': 'g'}, ('y', 0), 'g'), + ({'histtype': 'stepfilled', 'color': 'r'}, 'r', 'k'), + ({'histtype': 'step', 'color': 'r'}, ('r', 0), 'r'), + ({'histtype': 'stepfilled', 'facecolor': 'y'}, 'y', 'k'), + ({'histtype': 'step', 'facecolor': 'y'}, ('y', 0), 'C0'), + ({'histtype': 'stepfilled', 'edgecolor': 'g'}, 'C0', 'g'), + ({'histtype': 'step', 'edgecolor': 'g'}, ('C0', 0), 'g'), + ({'histtype': 'stepfilled'}, 'C0', 'k'), + ({'histtype': 'step'}, ('C0', 0), 'C0')]) +def test_hist_color_semantics(kwargs, patch_face, patch_edge): + _, _, patches = plt.figure().subplots().hist([1, 2, 3], **kwargs) + assert all(mcolors.same_color([p.get_facecolor(), p.get_edgecolor()], + [patch_face, patch_edge]) for p in patches) + + +def test_hist_barstacked_bottom_unchanged(): + b = np.array([10, 20]) + plt.hist([[0, 1], [0, 1]], 2, histtype="barstacked", bottom=b) + assert b.tolist() == [10, 20] + + +def test_hist_emptydata(): + fig, ax = plt.subplots() + ax.hist([[], range(10), range(10)], histtype="step") + + +def test_hist_unused_labels(): + # When a list with one dataset and N elements is provided and N labels, ensure + # that the first label is used for the dataset and all other labels are ignored + fig, ax = plt.subplots() + ax.hist([[1, 2, 3]], label=["values", "unused", "also unused"]) + _, labels = ax.get_legend_handles_labels() + assert labels == ["values"] + + +def test_hist_labels(): + # test singleton labels OK + fig, ax = plt.subplots() + _, _, bars = ax.hist([0, 1], label=0) + assert bars[0].get_label() == '0' + _, _, bars = ax.hist([0, 1], label=[0]) + assert bars[0].get_label() == '0' + _, _, bars = ax.hist([0, 1], label=None) + assert bars[0].get_label() == '_nolegend_' + _, _, bars = ax.hist([0, 1], label='0') + assert bars[0].get_label() == '0' + _, _, bars = ax.hist([0, 1], label='00') + assert bars[0].get_label() == '00' + + +@image_comparison(['transparent_markers'], remove_text=True) +def test_transparent_markers(): + np.random.seed(0) + data = np.random.random(50) + + fig, ax = plt.subplots() + ax.plot(data, 'D', mfc='none', markersize=100) + + +@image_comparison(['rgba_markers'], remove_text=True) +def test_rgba_markers(): + fig, axs = plt.subplots(ncols=2) + rcolors = [(1, 0, 0, 1), (1, 0, 0, 0.5)] + bcolors = [(0, 0, 1, 1), (0, 0, 1, 0.5)] + alphas = [None, 0.2] + kw = dict(ms=100, mew=20) + for i, alpha in enumerate(alphas): + for j, rcolor in enumerate(rcolors): + for k, bcolor in enumerate(bcolors): + axs[i].plot(j+1, k+1, 'o', mfc=bcolor, mec=rcolor, + alpha=alpha, **kw) + axs[i].plot(j+1, k+3, 'x', mec=rcolor, alpha=alpha, **kw) + for ax in axs: + ax.axis([-1, 4, 0, 5]) + + +@image_comparison(['mollweide_grid.png'], remove_text=True) +def test_mollweide_grid(): + # test that both horizontal and vertical gridlines appear on the Mollweide + # projection + fig = plt.figure() + ax = fig.add_subplot(projection='mollweide') + ax.grid() + + +def test_mollweide_forward_inverse_closure(): + # test that the round-trip Mollweide forward->inverse transformation is an + # approximate identity + fig = plt.figure() + ax = fig.add_subplot(projection='mollweide') + + # set up 1-degree grid in longitude, latitude + lon = np.linspace(-np.pi, np.pi, 360) + # The poles are degenerate and thus sensitive to floating point precision errors + lat = np.linspace(-np.pi / 2.0, np.pi / 2.0, 180)[1:-1] + lon, lat = np.meshgrid(lon, lat) + ll = np.vstack((lon.flatten(), lat.flatten())).T + + # perform forward transform + xy = ax.transProjection.transform(ll) + + # perform inverse transform + ll2 = ax.transProjection.inverted().transform(xy) + + # compare + np.testing.assert_array_almost_equal(ll, ll2, 3) + + +def test_mollweide_inverse_forward_closure(): + # test that the round-trip Mollweide inverse->forward transformation is an + # approximate identity + fig = plt.figure() + ax = fig.add_subplot(projection='mollweide') + + # set up grid in x, y + x = np.linspace(0, 1, 500) + x, y = np.meshgrid(x, x) + xy = np.vstack((x.flatten(), y.flatten())).T + + # perform inverse transform + ll = ax.transProjection.inverted().transform(xy) + + # perform forward transform + xy2 = ax.transProjection.transform(ll) + + # compare + np.testing.assert_array_almost_equal(xy, xy2, 3) + + +@image_comparison(['test_alpha'], remove_text=True) +def test_alpha(): + np.random.seed(0) + data = np.random.random(50) + + fig, ax = plt.subplots() + + # alpha=.5 markers, solid line + ax.plot(data, '-D', color=[1, 0, 0], mfc=[1, 0, 0, .5], + markersize=20, lw=10) + + # everything solid by kwarg + ax.plot(data + 2, '-D', color=[1, 0, 0, .5], mfc=[1, 0, 0, .5], + markersize=20, lw=10, + alpha=1) + + # everything alpha=.5 by kwarg + ax.plot(data + 4, '-D', color=[1, 0, 0], mfc=[1, 0, 0], + markersize=20, lw=10, + alpha=.5) + + # everything alpha=.5 by colors + ax.plot(data + 6, '-D', color=[1, 0, 0, .5], mfc=[1, 0, 0, .5], + markersize=20, lw=10) + + # alpha=.5 line, solid markers + ax.plot(data + 8, '-D', color=[1, 0, 0, .5], mfc=[1, 0, 0], + markersize=20, lw=10) + + +@image_comparison(['eventplot.png', 'eventplot.png'], remove_text=True) +def test_eventplot(): + np.random.seed(0) + + data1 = np.random.random([32, 20]).tolist() + data2 = np.random.random([6, 20]).tolist() + data = data1 + data2 + num_datasets = len(data) + + colors1 = [[0, 1, .7]] * len(data1) + colors2 = [[1, 0, 0], + [0, 1, 0], + [0, 0, 1], + [1, .75, 0], + [1, 0, 1], + [0, 1, 1]] + colors = colors1 + colors2 + + lineoffsets1 = 12 + np.arange(0, len(data1)) * .33 + lineoffsets2 = [-15, -3, 1, 1.5, 6, 10] + lineoffsets = lineoffsets1.tolist() + lineoffsets2 + + linelengths1 = [.33] * len(data1) + linelengths2 = [5, 2, 1, 1, 3, 1.5] + linelengths = linelengths1 + linelengths2 + + fig = plt.figure() + axobj = fig.add_subplot() + colls = axobj.eventplot(data, colors=colors, lineoffsets=lineoffsets, + linelengths=linelengths) + + num_collections = len(colls) + assert num_collections == num_datasets + + # Reuse testcase from above for a labeled data test + data = {"pos": data, "c": colors, "lo": lineoffsets, "ll": linelengths} + fig = plt.figure() + axobj = fig.add_subplot() + colls = axobj.eventplot("pos", colors="c", lineoffsets="lo", + linelengths="ll", data=data) + num_collections = len(colls) + assert num_collections == num_datasets + + +@image_comparison(['test_eventplot_defaults.png'], remove_text=True) +def test_eventplot_defaults(): + """ + test that eventplot produces the correct output given the default params + (see bug #3728) + """ + np.random.seed(0) + + data1 = np.random.random([32, 20]).tolist() + data2 = np.random.random([6, 20]).tolist() + data = data1 + data2 + + fig = plt.figure() + axobj = fig.add_subplot() + axobj.eventplot(data) + + +@pytest.mark.parametrize(('colors'), [ + ('0.5',), # string color with multiple characters: not OK before #8193 fix + ('tab:orange', 'tab:pink', 'tab:cyan', 'bLacK'), # case-insensitive + ('red', (0, 1, 0), None, (1, 0, 1, 0.5)), # a tricky case mixing types +]) +def test_eventplot_colors(colors): + """Test the *colors* parameter of eventplot. Inspired by issue #8193.""" + data = [[0], [1], [2], [3]] # 4 successive events of different nature + + # Build the list of the expected colors + expected = [c if c is not None else 'C0' for c in colors] + # Convert the list into an array of RGBA values + # NB: ['rgbk'] is not a valid argument for to_rgba_array, while 'rgbk' is. + if len(expected) == 1: + expected = expected[0] + expected = np.broadcast_to(mcolors.to_rgba_array(expected), (len(data), 4)) + + fig, ax = plt.subplots() + if len(colors) == 1: # tuple with a single string (like '0.5' or 'rgbk') + colors = colors[0] + collections = ax.eventplot(data, colors=colors) + + for coll, color in zip(collections, expected): + assert_allclose(coll.get_color(), color) + + +def test_eventplot_alpha(): + fig, ax = plt.subplots() + + # one alpha for all + collections = ax.eventplot([[0, 2, 4], [1, 3, 5, 7]], alpha=0.7) + assert collections[0].get_alpha() == 0.7 + assert collections[1].get_alpha() == 0.7 + + # one alpha per collection + collections = ax.eventplot([[0, 2, 4], [1, 3, 5, 7]], alpha=[0.5, 0.7]) + assert collections[0].get_alpha() == 0.5 + assert collections[1].get_alpha() == 0.7 + + with pytest.raises(ValueError, match="alpha and positions are unequal"): + ax.eventplot([[0, 2, 4], [1, 3, 5, 7]], alpha=[0.5, 0.7, 0.9]) + + with pytest.raises(ValueError, match="alpha and positions are unequal"): + ax.eventplot([0, 2, 4], alpha=[0.5, 0.7]) + + +@image_comparison(['test_eventplot_problem_kwargs.png'], remove_text=True) +def test_eventplot_problem_kwargs(recwarn): + """ + test that 'singular' versions of LineCollection props raise an + MatplotlibDeprecationWarning rather than overriding the 'plural' versions + (e.g., to prevent 'color' from overriding 'colors', see issue #4297) + """ + np.random.seed(0) + + data1 = np.random.random([20]).tolist() + data2 = np.random.random([10]).tolist() + data = [data1, data2] + + fig = plt.figure() + axobj = fig.add_subplot() + + axobj.eventplot(data, + colors=['r', 'b'], + color=['c', 'm'], + linewidths=[2, 1], + linewidth=[1, 2], + linestyles=['solid', 'dashed'], + linestyle=['dashdot', 'dotted']) + + assert len(recwarn) == 3 + assert all(issubclass(wi.category, mpl.MatplotlibDeprecationWarning) + for wi in recwarn) + + +def test_empty_eventplot(): + fig, ax = plt.subplots(1, 1) + ax.eventplot([[]], colors=[(0.0, 0.0, 0.0, 0.0)]) + plt.draw() + + +@pytest.mark.parametrize('data', [[[]], [[], [0, 1]], [[0, 1], []]]) +@pytest.mark.parametrize('orientation', [None, 'vertical', 'horizontal']) +def test_eventplot_orientation(data, orientation): + """Introduced when fixing issue #6412.""" + opts = {} if orientation is None else {'orientation': orientation} + fig, ax = plt.subplots(1, 1) + ax.eventplot(data, **opts) + plt.draw() + + +@check_figures_equal(extensions=['png']) +def test_eventplot_units_list(fig_test, fig_ref): + # test that list of lists converted properly: + ts_1 = [datetime.datetime(2021, 1, 1), datetime.datetime(2021, 1, 2), + datetime.datetime(2021, 1, 3)] + ts_2 = [datetime.datetime(2021, 1, 15), datetime.datetime(2021, 1, 16)] + + ax = fig_ref.subplots() + ax.eventplot(ts_1, lineoffsets=0) + ax.eventplot(ts_2, lineoffsets=1) + + ax = fig_test.subplots() + ax.eventplot([ts_1, ts_2]) + + +@image_comparison(['marker_styles.png'], remove_text=True) +def test_marker_styles(): + fig, ax = plt.subplots() + # Since generation of the test image, None was removed but 'none' was + # added. By moving 'none' to the front (=former sorted place of None) + # we can avoid regenerating the test image. This can be removed if the + # test image has to be regenerated for other reasons. + markers = sorted(matplotlib.markers.MarkerStyle.markers, + key=lambda x: str(type(x))+str(x)) + markers.remove('none') + markers = ['none', *markers] + for y, marker in enumerate(markers): + ax.plot((y % 2)*5 + np.arange(10)*10, np.ones(10)*10*y, linestyle='', + marker=marker, markersize=10+y/5, label=marker) + + +@image_comparison(['rc_markerfill.png'], + tol=0 if platform.machine() == 'x86_64' else 0.037) +def test_markers_fillstyle_rcparams(): + fig, ax = plt.subplots() + x = np.arange(7) + for idx, (style, marker) in enumerate( + [('top', 's'), ('bottom', 'o'), ('none', '^')]): + matplotlib.rcParams['markers.fillstyle'] = style + ax.plot(x+idx, marker=marker) + + +@image_comparison(['vertex_markers.png'], remove_text=True) +def test_vertex_markers(): + data = list(range(10)) + marker_as_tuple = ((-1, -1), (1, -1), (1, 1), (-1, 1)) + marker_as_list = [(-1, -1), (1, -1), (1, 1), (-1, 1)] + fig, ax = plt.subplots() + ax.plot(data, linestyle='', marker=marker_as_tuple, mfc='k') + ax.plot(data[::-1], linestyle='', marker=marker_as_list, mfc='b') + ax.set_xlim([-1, 10]) + ax.set_ylim([-1, 10]) + + +@image_comparison(['vline_hline_zorder.png', 'errorbar_zorder.png'], + tol=0 if platform.machine() == 'x86_64' else 0.026) +def test_eb_line_zorder(): + x = list(range(10)) + + # First illustrate basic pyplot interface, using defaults where possible. + fig = plt.figure() + ax = fig.gca() + ax.plot(x, lw=10, zorder=5) + ax.axhline(1, color='red', lw=10, zorder=1) + ax.axhline(5, color='green', lw=10, zorder=10) + ax.axvline(7, color='m', lw=10, zorder=7) + ax.axvline(2, color='k', lw=10, zorder=3) + + ax.set_title("axvline and axhline zorder test") + + # Now switch to a more OO interface to exercise more features. + fig = plt.figure() + ax = fig.gca() + x = list(range(10)) + y = np.zeros(10) + yerr = list(range(10)) + ax.errorbar(x, y, yerr=yerr, zorder=5, lw=5, color='r') + for j in range(10): + ax.axhline(j, lw=5, color='k', zorder=j) + ax.axhline(-j, lw=5, color='k', zorder=j) + + ax.set_title("errorbar zorder test") + + +@check_figures_equal(extensions=['png']) +def test_axline_loglog(fig_test, fig_ref): + ax = fig_test.subplots() + ax.set(xlim=(0.1, 10), ylim=(1e-3, 1)) + ax.loglog([.3, .6], [.3, .6], ".-") + ax.axline((1, 1e-3), (10, 1e-2), c="k") + + ax = fig_ref.subplots() + ax.set(xlim=(0.1, 10), ylim=(1e-3, 1)) + ax.loglog([.3, .6], [.3, .6], ".-") + ax.loglog([1, 10], [1e-3, 1e-2], c="k") + + +@check_figures_equal(extensions=['png']) +def test_axline(fig_test, fig_ref): + ax = fig_test.subplots() + ax.set(xlim=(-1, 1), ylim=(-1, 1)) + ax.axline((0, 0), (1, 1)) + ax.axline((0, 0), (1, 0), color='C1') + ax.axline((0, 0.5), (1, 0.5), color='C2') + # slopes + ax.axline((-0.7, -0.5), slope=0, color='C3') + ax.axline((1, -0.5), slope=-0.5, color='C4') + ax.axline((-0.5, 1), slope=float('inf'), color='C5') + + ax = fig_ref.subplots() + ax.set(xlim=(-1, 1), ylim=(-1, 1)) + ax.plot([-1, 1], [-1, 1]) + ax.axhline(0, color='C1') + ax.axhline(0.5, color='C2') + # slopes + ax.axhline(-0.5, color='C3') + ax.plot([-1, 1], [0.5, -0.5], color='C4') + ax.axvline(-0.5, color='C5') + + +@check_figures_equal(extensions=['png']) +def test_axline_transaxes(fig_test, fig_ref): + ax = fig_test.subplots() + ax.set(xlim=(-1, 1), ylim=(-1, 1)) + ax.axline((0, 0), slope=1, transform=ax.transAxes) + ax.axline((1, 0.5), slope=1, color='C1', transform=ax.transAxes) + ax.axline((0.5, 0.5), slope=0, color='C2', transform=ax.transAxes) + ax.axline((0.5, 0), (0.5, 1), color='C3', transform=ax.transAxes) + + ax = fig_ref.subplots() + ax.set(xlim=(-1, 1), ylim=(-1, 1)) + ax.plot([-1, 1], [-1, 1]) + ax.plot([0, 1], [-1, 0], color='C1') + ax.plot([-1, 1], [0, 0], color='C2') + ax.plot([0, 0], [-1, 1], color='C3') + + +@check_figures_equal(extensions=['png']) +def test_axline_transaxes_panzoom(fig_test, fig_ref): + # test that it is robust against pan/zoom and + # figure resize after plotting + ax = fig_test.subplots() + ax.set(xlim=(-1, 1), ylim=(-1, 1)) + ax.axline((0, 0), slope=1, transform=ax.transAxes) + ax.axline((0.5, 0.5), slope=2, color='C1', transform=ax.transAxes) + ax.axline((0.5, 0.5), slope=0, color='C2', transform=ax.transAxes) + ax.set(xlim=(0, 5), ylim=(0, 10)) + fig_test.set_size_inches(3, 3) + + ax = fig_ref.subplots() + ax.set(xlim=(0, 5), ylim=(0, 10)) + fig_ref.set_size_inches(3, 3) + ax.plot([0, 5], [0, 5]) + ax.plot([0, 5], [0, 10], color='C1') + ax.plot([0, 5], [5, 5], color='C2') + + +def test_axline_args(): + """Exactly one of *xy2* and *slope* must be specified.""" + fig, ax = plt.subplots() + with pytest.raises(TypeError): + ax.axline((0, 0)) # missing second parameter + with pytest.raises(TypeError): + ax.axline((0, 0), (1, 1), slope=1) # redundant parameters + ax.set_xscale('log') + with pytest.raises(TypeError): + ax.axline((0, 0), slope=1) + ax.set_xscale('linear') + ax.set_yscale('log') + with pytest.raises(TypeError): + ax.axline((0, 0), slope=1) + ax.set_yscale('linear') + with pytest.raises(ValueError): + ax.axline((0, 0), (0, 0)) # two identical points are not allowed + plt.draw() + + +@image_comparison(['vlines_basic', 'vlines_with_nan', 'vlines_masked'], + extensions=['png']) +def test_vlines(): + # normal + x1 = [2, 3, 4, 5, 7] + y1 = [2, -6, 3, 8, 2] + fig1, ax1 = plt.subplots() + ax1.vlines(x1, 0, y1, colors='g', linewidth=5) + + # GH #7406 + x2 = [2, 3, 4, 5, 6, 7] + y2 = [2, -6, 3, 8, np.nan, 2] + fig2, (ax2, ax3, ax4) = plt.subplots(nrows=3, figsize=(4, 8)) + ax2.vlines(x2, 0, y2, colors='g', linewidth=5) + + x3 = [2, 3, 4, 5, 6, 7] + y3 = [np.nan, 2, -6, 3, 8, 2] + ax3.vlines(x3, 0, y3, colors='r', linewidth=3, linestyle='--') + + x4 = [2, 3, 4, 5, 6, 7] + y4 = [np.nan, 2, -6, 3, 8, np.nan] + ax4.vlines(x4, 0, y4, colors='k', linewidth=2) + + # tweak the x-axis so we can see the lines better + for ax in [ax1, ax2, ax3, ax4]: + ax.set_xlim(0, 10) + + # check that the y-lims are all automatically the same + assert ax1.get_ylim() == ax2.get_ylim() + assert ax1.get_ylim() == ax3.get_ylim() + assert ax1.get_ylim() == ax4.get_ylim() + + fig3, ax5 = plt.subplots() + x5 = np.ma.masked_equal([2, 4, 6, 8, 10, 12], 8) + ymin5 = np.ma.masked_equal([0, 1, -1, 0, 2, 1], 2) + ymax5 = np.ma.masked_equal([13, 14, 15, 16, 17, 18], 18) + ax5.vlines(x5, ymin5, ymax5, colors='k', linewidth=2) + ax5.set_xlim(0, 15) + + +def test_vlines_default(): + fig, ax = plt.subplots() + with mpl.rc_context({'lines.color': 'red'}): + lines = ax.vlines(0.5, 0, 1) + assert mpl.colors.same_color(lines.get_color(), 'red') + + +@image_comparison(['hlines_basic', 'hlines_with_nan', 'hlines_masked'], + extensions=['png']) +def test_hlines(): + # normal + y1 = [2, 3, 4, 5, 7] + x1 = [2, -6, 3, 8, 2] + fig1, ax1 = plt.subplots() + ax1.hlines(y1, 0, x1, colors='g', linewidth=5) + + # GH #7406 + y2 = [2, 3, 4, 5, 6, 7] + x2 = [2, -6, 3, 8, np.nan, 2] + fig2, (ax2, ax3, ax4) = plt.subplots(nrows=3, figsize=(4, 8)) + ax2.hlines(y2, 0, x2, colors='g', linewidth=5) + + y3 = [2, 3, 4, 5, 6, 7] + x3 = [np.nan, 2, -6, 3, 8, 2] + ax3.hlines(y3, 0, x3, colors='r', linewidth=3, linestyle='--') + + y4 = [2, 3, 4, 5, 6, 7] + x4 = [np.nan, 2, -6, 3, 8, np.nan] + ax4.hlines(y4, 0, x4, colors='k', linewidth=2) + + # tweak the y-axis so we can see the lines better + for ax in [ax1, ax2, ax3, ax4]: + ax.set_ylim(0, 10) + + # check that the x-lims are all automatically the same + assert ax1.get_xlim() == ax2.get_xlim() + assert ax1.get_xlim() == ax3.get_xlim() + assert ax1.get_xlim() == ax4.get_xlim() + + fig3, ax5 = plt.subplots() + y5 = np.ma.masked_equal([2, 4, 6, 8, 10, 12], 8) + xmin5 = np.ma.masked_equal([0, 1, -1, 0, 2, 1], 2) + xmax5 = np.ma.masked_equal([13, 14, 15, 16, 17, 18], 18) + ax5.hlines(y5, xmin5, xmax5, colors='k', linewidth=2) + ax5.set_ylim(0, 15) + + +def test_hlines_default(): + fig, ax = plt.subplots() + with mpl.rc_context({'lines.color': 'red'}): + lines = ax.hlines(0.5, 0, 1) + assert mpl.colors.same_color(lines.get_color(), 'red') + + +@pytest.mark.parametrize('data', [[1, 2, 3, np.nan, 5], + np.ma.masked_equal([1, 2, 3, 4, 5], 4)]) +@check_figures_equal(extensions=["png"]) +def test_lines_with_colors(fig_test, fig_ref, data): + test_colors = ['red', 'green', 'blue', 'purple', 'orange'] + fig_test.add_subplot(2, 1, 1).vlines(data, 0, 1, + colors=test_colors, linewidth=5) + fig_test.add_subplot(2, 1, 2).hlines(data, 0, 1, + colors=test_colors, linewidth=5) + + expect_xy = [1, 2, 3, 5] + expect_color = ['red', 'green', 'blue', 'orange'] + fig_ref.add_subplot(2, 1, 1).vlines(expect_xy, 0, 1, + colors=expect_color, linewidth=5) + fig_ref.add_subplot(2, 1, 2).hlines(expect_xy, 0, 1, + colors=expect_color, linewidth=5) + + +@image_comparison(['vlines_hlines_blended_transform'], + extensions=['png'], style='mpl20') +def test_vlines_hlines_blended_transform(): + t = np.arange(5.0, 10.0, 0.1) + s = np.exp(-t) + np.sin(2 * np.pi * t) + 10 + fig, (hax, vax) = plt.subplots(2, 1, figsize=(6, 6)) + hax.plot(t, s, '^') + hax.hlines([10, 9], xmin=0, xmax=0.5, + transform=hax.get_yaxis_transform(), colors='r') + vax.plot(t, s, '^') + vax.vlines([6, 7], ymin=0, ymax=0.15, transform=vax.get_xaxis_transform(), + colors='r') + + +@image_comparison(['step_linestyle', 'step_linestyle'], remove_text=True, + tol=0.2) +def test_step_linestyle(): + # Tolerance caused by reordering of floating-point operations + # Remove when regenerating the images + x = y = np.arange(10) + + # First illustrate basic pyplot interface, using defaults where possible. + fig, ax_lst = plt.subplots(2, 2) + ax_lst = ax_lst.flatten() + + ln_styles = ['-', '--', '-.', ':'] + + for ax, ls in zip(ax_lst, ln_styles): + ax.step(x, y, lw=5, linestyle=ls, where='pre') + ax.step(x, y + 1, lw=5, linestyle=ls, where='mid') + ax.step(x, y + 2, lw=5, linestyle=ls, where='post') + ax.set_xlim([-1, 5]) + ax.set_ylim([-1, 7]) + + # Reuse testcase from above for a labeled data test + data = {"X": x, "Y0": y, "Y1": y+1, "Y2": y+2} + fig, ax_lst = plt.subplots(2, 2) + ax_lst = ax_lst.flatten() + ln_styles = ['-', '--', '-.', ':'] + for ax, ls in zip(ax_lst, ln_styles): + ax.step("X", "Y0", lw=5, linestyle=ls, where='pre', data=data) + ax.step("X", "Y1", lw=5, linestyle=ls, where='mid', data=data) + ax.step("X", "Y2", lw=5, linestyle=ls, where='post', data=data) + ax.set_xlim([-1, 5]) + ax.set_ylim([-1, 7]) + + +@image_comparison(['mixed_collection'], remove_text=True) +def test_mixed_collection(): + # First illustrate basic pyplot interface, using defaults where possible. + fig, ax = plt.subplots() + + c = mpatches.Circle((8, 8), radius=4, facecolor='none', edgecolor='green') + + # PDF can optimize this one + p1 = mpl.collections.PatchCollection([c], match_original=True) + p1.set_offsets([[0, 0], [24, 24]]) + p1.set_linewidths([1, 5]) + + # PDF can't optimize this one, because the alpha of the edge changes + p2 = mpl.collections.PatchCollection([c], match_original=True) + p2.set_offsets([[48, 0], [-32, -16]]) + p2.set_linewidths([1, 5]) + p2.set_edgecolors([[0, 0, 0.1, 1.0], [0, 0, 0.1, 0.5]]) + + ax.patch.set_color('0.5') + ax.add_collection(p1) + ax.add_collection(p2) + + ax.set_xlim(0, 16) + ax.set_ylim(0, 16) + + +def test_subplot_key_hash(): + ax = plt.subplot(np.int32(5), np.int64(1), 1) + ax.twinx() + assert ax.get_subplotspec().get_geometry() == (5, 1, 0, 0) + + +@image_comparison( + ["specgram_freqs.png", "specgram_freqs_linear.png", + "specgram_noise.png", "specgram_noise_linear.png"], + remove_text=True, tol=0.07, style="default") +def test_specgram(): + """Test axes.specgram in default (psd) mode.""" + + # use former defaults to match existing baseline image + matplotlib.rcParams['image.interpolation'] = 'nearest' + + n = 1000 + Fs = 10. + + fstims = [[Fs/4, Fs/5, Fs/11], [Fs/4.7, Fs/5.6, Fs/11.9]] + NFFT_freqs = int(10 * Fs / np.min(fstims)) + x = np.arange(0, n, 1/Fs) + y_freqs = np.concatenate( + np.sin(2 * np.pi * np.multiply.outer(fstims, x)).sum(axis=1)) + + NFFT_noise = int(10 * Fs / 11) + np.random.seed(0) + y_noise = np.concatenate([np.random.standard_normal(n), np.random.rand(n)]) + + all_sides = ["default", "onesided", "twosided"] + for y, NFFT in [(y_freqs, NFFT_freqs), (y_noise, NFFT_noise)]: + noverlap = NFFT // 2 + pad_to = int(2 ** np.ceil(np.log2(NFFT))) + for ax, sides in zip(plt.figure().subplots(3), all_sides): + ax.specgram(y, NFFT=NFFT, Fs=Fs, noverlap=noverlap, + pad_to=pad_to, sides=sides) + for ax, sides in zip(plt.figure().subplots(3), all_sides): + ax.specgram(y, NFFT=NFFT, Fs=Fs, noverlap=noverlap, + pad_to=pad_to, sides=sides, + scale="linear", norm=matplotlib.colors.LogNorm()) + + +@image_comparison( + ["specgram_magnitude_freqs.png", "specgram_magnitude_freqs_linear.png", + "specgram_magnitude_noise.png", "specgram_magnitude_noise_linear.png"], + remove_text=True, tol=0.07, style="default") +def test_specgram_magnitude(): + """Test axes.specgram in magnitude mode.""" + + # use former defaults to match existing baseline image + matplotlib.rcParams['image.interpolation'] = 'nearest' + + n = 1000 + Fs = 10. + + fstims = [[Fs/4, Fs/5, Fs/11], [Fs/4.7, Fs/5.6, Fs/11.9]] + NFFT_freqs = int(100 * Fs / np.min(fstims)) + x = np.arange(0, n, 1/Fs) + y = np.sin(2 * np.pi * np.multiply.outer(fstims, x)).sum(axis=1) + y[:, -1] = 1 + y_freqs = np.hstack(y) + + NFFT_noise = int(10 * Fs / 11) + np.random.seed(0) + y_noise = np.concatenate([np.random.standard_normal(n), np.random.rand(n)]) + + all_sides = ["default", "onesided", "twosided"] + for y, NFFT in [(y_freqs, NFFT_freqs), (y_noise, NFFT_noise)]: + noverlap = NFFT // 2 + pad_to = int(2 ** np.ceil(np.log2(NFFT))) + for ax, sides in zip(plt.figure().subplots(3), all_sides): + ax.specgram(y, NFFT=NFFT, Fs=Fs, noverlap=noverlap, + pad_to=pad_to, sides=sides, mode="magnitude") + for ax, sides in zip(plt.figure().subplots(3), all_sides): + ax.specgram(y, NFFT=NFFT, Fs=Fs, noverlap=noverlap, + pad_to=pad_to, sides=sides, mode="magnitude", + scale="linear", norm=matplotlib.colors.LogNorm()) + + +@image_comparison( + ["specgram_angle_freqs.png", "specgram_phase_freqs.png", + "specgram_angle_noise.png", "specgram_phase_noise.png"], + remove_text=True, tol=0.07, style="default") +def test_specgram_angle(): + """Test axes.specgram in angle and phase modes.""" + + # use former defaults to match existing baseline image + matplotlib.rcParams['image.interpolation'] = 'nearest' + + n = 1000 + Fs = 10. + + fstims = [[Fs/4, Fs/5, Fs/11], [Fs/4.7, Fs/5.6, Fs/11.9]] + NFFT_freqs = int(10 * Fs / np.min(fstims)) + x = np.arange(0, n, 1/Fs) + y = np.sin(2 * np.pi * np.multiply.outer(fstims, x)).sum(axis=1) + y[:, -1] = 1 + y_freqs = np.hstack(y) + + NFFT_noise = int(10 * Fs / 11) + np.random.seed(0) + y_noise = np.concatenate([np.random.standard_normal(n), np.random.rand(n)]) + + all_sides = ["default", "onesided", "twosided"] + for y, NFFT in [(y_freqs, NFFT_freqs), (y_noise, NFFT_noise)]: + noverlap = NFFT // 2 + pad_to = int(2 ** np.ceil(np.log2(NFFT))) + for mode in ["angle", "phase"]: + for ax, sides in zip(plt.figure().subplots(3), all_sides): + ax.specgram(y, NFFT=NFFT, Fs=Fs, noverlap=noverlap, + pad_to=pad_to, sides=sides, mode=mode) + with pytest.raises(ValueError): + ax.specgram(y, NFFT=NFFT, Fs=Fs, noverlap=noverlap, + pad_to=pad_to, sides=sides, mode=mode, + scale="dB") + + +def test_specgram_fs_none(): + """Test axes.specgram when Fs is None, should not throw error.""" + spec, freqs, t, im = plt.specgram(np.ones(300), Fs=None, scale='linear') + xmin, xmax, freq0, freq1 = im.get_extent() + assert xmin == 32 and xmax == 96 + + +@check_figures_equal(extensions=["png"]) +def test_specgram_origin_rcparam(fig_test, fig_ref): + """Test specgram ignores image.origin rcParam and uses origin 'upper'.""" + t = np.arange(500) + signal = np.sin(t) + + plt.rcParams["image.origin"] = 'upper' + + # Reference: First graph using default origin in imshow (upper), + fig_ref.subplots().specgram(signal) + + # Try to overwrite the setting trying to flip the specgram + plt.rcParams["image.origin"] = 'lower' + + # Test: origin='lower' should be ignored + fig_test.subplots().specgram(signal) + + +def test_specgram_origin_kwarg(): + """Ensure passing origin as a kwarg raises a TypeError.""" + t = np.arange(500) + signal = np.sin(t) + + with pytest.raises(TypeError): + plt.specgram(signal, origin='lower') + + +@image_comparison( + ["psd_freqs.png", "csd_freqs.png", "psd_noise.png", "csd_noise.png"], + remove_text=True, tol=0.002) +def test_psd_csd(): + n = 10000 + Fs = 100. + + fstims = [[Fs/4, Fs/5, Fs/11], [Fs/4.7, Fs/5.6, Fs/11.9]] + NFFT_freqs = int(1000 * Fs / np.min(fstims)) + x = np.arange(0, n, 1/Fs) + ys_freqs = np.sin(2 * np.pi * np.multiply.outer(fstims, x)).sum(axis=1) + + NFFT_noise = int(1000 * Fs / 11) + np.random.seed(0) + ys_noise = [np.random.standard_normal(n), np.random.rand(n)] + + all_kwargs = [{"sides": "default"}, + {"sides": "onesided", "return_line": False}, + {"sides": "twosided", "return_line": True}] + for ys, NFFT in [(ys_freqs, NFFT_freqs), (ys_noise, NFFT_noise)]: + noverlap = NFFT // 2 + pad_to = int(2 ** np.ceil(np.log2(NFFT))) + for ax, kwargs in zip(plt.figure().subplots(3), all_kwargs): + ret = ax.psd(np.concatenate(ys), NFFT=NFFT, Fs=Fs, + noverlap=noverlap, pad_to=pad_to, **kwargs) + assert len(ret) == 2 + kwargs.get("return_line", False) + ax.set(xlabel="", ylabel="") + for ax, kwargs in zip(plt.figure().subplots(3), all_kwargs): + ret = ax.csd(*ys, NFFT=NFFT, Fs=Fs, + noverlap=noverlap, pad_to=pad_to, **kwargs) + assert len(ret) == 2 + kwargs.get("return_line", False) + ax.set(xlabel="", ylabel="") + + +@image_comparison( + ["magnitude_spectrum_freqs_linear.png", + "magnitude_spectrum_freqs_dB.png", + "angle_spectrum_freqs.png", + "phase_spectrum_freqs.png", + "magnitude_spectrum_noise_linear.png", + "magnitude_spectrum_noise_dB.png", + "angle_spectrum_noise.png", + "phase_spectrum_noise.png"], + remove_text=True) +def test_spectrum(): + n = 10000 + Fs = 100. + + fstims1 = [Fs/4, Fs/5, Fs/11] + NFFT = int(1000 * Fs / min(fstims1)) + pad_to = int(2 ** np.ceil(np.log2(NFFT))) + + x = np.arange(0, n, 1/Fs) + y_freqs = ((np.sin(2 * np.pi * np.outer(x, fstims1)) * 10**np.arange(3)) + .sum(axis=1)) + np.random.seed(0) + y_noise = np.hstack([np.random.standard_normal(n), np.random.rand(n)]) - .5 + + all_sides = ["default", "onesided", "twosided"] + kwargs = {"Fs": Fs, "pad_to": pad_to} + for y in [y_freqs, y_noise]: + for ax, sides in zip(plt.figure().subplots(3), all_sides): + spec, freqs, line = ax.magnitude_spectrum(y, sides=sides, **kwargs) + ax.set(xlabel="", ylabel="") + for ax, sides in zip(plt.figure().subplots(3), all_sides): + spec, freqs, line = ax.magnitude_spectrum(y, sides=sides, **kwargs, + scale="dB") + ax.set(xlabel="", ylabel="") + for ax, sides in zip(plt.figure().subplots(3), all_sides): + spec, freqs, line = ax.angle_spectrum(y, sides=sides, **kwargs) + ax.set(xlabel="", ylabel="") + for ax, sides in zip(plt.figure().subplots(3), all_sides): + spec, freqs, line = ax.phase_spectrum(y, sides=sides, **kwargs) + ax.set(xlabel="", ylabel="") + + +def test_psd_csd_edge_cases(): + # Inverted yaxis or fully zero inputs used to throw exceptions. + axs = plt.figure().subplots(2) + for ax in axs: + ax.yaxis.set(inverted=True) + with np.errstate(divide="ignore"): + axs[0].psd(np.zeros(5)) + axs[1].csd(np.zeros(5), np.zeros(5)) + + +@check_figures_equal(extensions=['png']) +def test_twin_remove(fig_test, fig_ref): + ax_test = fig_test.add_subplot() + ax_twinx = ax_test.twinx() + ax_twiny = ax_test.twiny() + ax_twinx.remove() + ax_twiny.remove() + + ax_ref = fig_ref.add_subplot() + # Ideally we also undo tick changes when calling ``remove()``, but for now + # manually set the ticks of the reference image to match the test image + ax_ref.xaxis.tick_bottom() + ax_ref.yaxis.tick_left() + + +@image_comparison(['twin_spines.png'], remove_text=True, + tol=0 if platform.machine() == 'x86_64' else 0.022) +def test_twin_spines(): + + def make_patch_spines_invisible(ax): + ax.set_frame_on(True) + ax.patch.set_visible(False) + ax.spines[:].set_visible(False) + + fig = plt.figure(figsize=(4, 3)) + fig.subplots_adjust(right=0.75) + + host = fig.add_subplot() + par1 = host.twinx() + par2 = host.twinx() + + # Offset the right spine of par2. The ticks and label have already been + # placed on the right by twinx above. + par2.spines.right.set_position(("axes", 1.2)) + # Having been created by twinx, par2 has its frame off, so the line of + # its detached spine is invisible. First, activate the frame but make + # the patch and spines invisible. + make_patch_spines_invisible(par2) + # Second, show the right spine. + par2.spines.right.set_visible(True) + + p1, = host.plot([0, 1, 2], [0, 1, 2], "b-") + p2, = par1.plot([0, 1, 2], [0, 3, 2], "r-") + p3, = par2.plot([0, 1, 2], [50, 30, 15], "g-") + + host.set_xlim(0, 2) + host.set_ylim(0, 2) + par1.set_ylim(0, 4) + par2.set_ylim(1, 65) + + host.yaxis.label.set_color(p1.get_color()) + par1.yaxis.label.set_color(p2.get_color()) + par2.yaxis.label.set_color(p3.get_color()) + + tkw = dict(size=4, width=1.5) + host.tick_params(axis='y', colors=p1.get_color(), **tkw) + par1.tick_params(axis='y', colors=p2.get_color(), **tkw) + par2.tick_params(axis='y', colors=p3.get_color(), **tkw) + host.tick_params(axis='x', **tkw) + + +@image_comparison(['twin_spines_on_top.png', 'twin_spines_on_top.png'], + remove_text=True) +def test_twin_spines_on_top(): + matplotlib.rcParams['axes.linewidth'] = 48.0 + matplotlib.rcParams['lines.linewidth'] = 48.0 + + fig = plt.figure() + ax1 = fig.add_subplot(1, 1, 1) + + data = np.array([[1000, 1100, 1200, 1250], + [310, 301, 360, 400]]) + + ax2 = ax1.twinx() + + ax1.plot(data[0], data[1]/1E3, color='#BEAED4') + ax1.fill_between(data[0], data[1]/1E3, color='#BEAED4', alpha=.8) + + ax2.plot(data[0], data[1]/1E3, color='#7FC97F') + ax2.fill_between(data[0], data[1]/1E3, color='#7FC97F', alpha=.5) + + # Reuse testcase from above for a labeled data test + data = {"i": data[0], "j": data[1]/1E3} + fig = plt.figure() + ax1 = fig.add_subplot(1, 1, 1) + ax2 = ax1.twinx() + ax1.plot("i", "j", color='#BEAED4', data=data) + ax1.fill_between("i", "j", color='#BEAED4', alpha=.8, data=data) + ax2.plot("i", "j", color='#7FC97F', data=data) + ax2.fill_between("i", "j", color='#7FC97F', alpha=.5, data=data) + + +@pytest.mark.parametrize("grid_which, major_visible, minor_visible", [ + ("both", True, True), + ("major", True, False), + ("minor", False, True), +]) +def test_rcparam_grid_minor(grid_which, major_visible, minor_visible): + mpl.rcParams.update({"axes.grid": True, "axes.grid.which": grid_which}) + fig, ax = plt.subplots() + fig.canvas.draw() + assert all(tick.gridline.get_visible() == major_visible + for tick in ax.xaxis.majorTicks) + assert all(tick.gridline.get_visible() == minor_visible + for tick in ax.xaxis.minorTicks) + + +def test_grid(): + fig, ax = plt.subplots() + ax.grid() + fig.canvas.draw() + assert ax.xaxis.majorTicks[0].gridline.get_visible() + ax.grid(visible=False) + fig.canvas.draw() + assert not ax.xaxis.majorTicks[0].gridline.get_visible() + ax.grid(visible=True) + fig.canvas.draw() + assert ax.xaxis.majorTicks[0].gridline.get_visible() + ax.grid() + fig.canvas.draw() + assert not ax.xaxis.majorTicks[0].gridline.get_visible() + + +def test_reset_grid(): + fig, ax = plt.subplots() + ax.tick_params(reset=True, which='major', labelsize=10) + assert not ax.xaxis.majorTicks[0].gridline.get_visible() + ax.grid(color='red') # enables grid + assert ax.xaxis.majorTicks[0].gridline.get_visible() + + with plt.rc_context({'axes.grid': True}): + ax.clear() + ax.tick_params(reset=True, which='major', labelsize=10) + assert ax.xaxis.majorTicks[0].gridline.get_visible() + + +@check_figures_equal(extensions=['png']) +def test_reset_ticks(fig_test, fig_ref): + for fig in [fig_ref, fig_test]: + ax = fig.add_subplot() + ax.grid(True) + ax.tick_params( + direction='in', length=10, width=5, color='C0', pad=12, + labelsize=14, labelcolor='C1', labelrotation=45, + grid_color='C2', grid_alpha=0.8, grid_linewidth=3, + grid_linestyle='--') + fig.draw_without_rendering() + + # After we've changed any setting on ticks, reset_ticks will mean + # re-creating them from scratch. This *should* appear the same as not + # resetting them. + for ax in fig_test.axes: + ax.xaxis.reset_ticks() + ax.yaxis.reset_ticks() + + +@mpl.style.context('mpl20') +def test_context_ticks(): + with plt.rc_context({ + 'xtick.direction': 'in', 'xtick.major.size': 30, 'xtick.major.width': 5, + 'xtick.color': 'C0', 'xtick.major.pad': 12, + 'xtick.bottom': True, 'xtick.top': True, + 'xtick.labelsize': 14, 'xtick.labelcolor': 'C1'}): + fig, ax = plt.subplots() + # Draw outside the context so that all-but-first tick are generated with the normal + # mpl20 style in place. + fig.draw_without_rendering() + + first_tick = ax.xaxis.majorTicks[0] + for tick in ax.xaxis.majorTicks[1:]: + assert tick._size == first_tick._size + assert tick._width == first_tick._width + assert tick._base_pad == first_tick._base_pad + assert tick._labelrotation == first_tick._labelrotation + assert tick._zorder == first_tick._zorder + assert tick._tickdir == first_tick._tickdir + + +def test_vline_limit(): + fig = plt.figure() + ax = fig.gca() + ax.axvline(0.5) + ax.plot([-0.1, 0, 0.2, 0.1]) + assert_allclose(ax.get_ylim(), (-.1, .2)) + + +@pytest.mark.parametrize('fv, fh, args', [[plt.axvline, plt.axhline, (1,)], + [plt.axvspan, plt.axhspan, (1, 1)]]) +def test_axline_minmax(fv, fh, args): + bad_lim = matplotlib.dates.num2date(1) + # Check vertical functions + with pytest.raises(ValueError, match='ymin must be a single scalar value'): + fv(*args, ymin=bad_lim, ymax=1) + with pytest.raises(ValueError, match='ymax must be a single scalar value'): + fv(*args, ymin=1, ymax=bad_lim) + # Check horizontal functions + with pytest.raises(ValueError, match='xmin must be a single scalar value'): + fh(*args, xmin=bad_lim, xmax=1) + with pytest.raises(ValueError, match='xmax must be a single scalar value'): + fh(*args, xmin=1, xmax=bad_lim) + + +def test_empty_shared_subplots(): + # empty plots with shared axes inherit limits from populated plots + fig, axs = plt.subplots(nrows=1, ncols=2, sharex=True, sharey=True) + axs[0].plot([1, 2, 3], [2, 4, 6]) + x0, x1 = axs[1].get_xlim() + y0, y1 = axs[1].get_ylim() + assert x0 <= 1 + assert x1 >= 3 + assert y0 <= 2 + assert y1 >= 6 + + +def test_shared_with_aspect_1(): + # allow sharing one axis + for adjustable in ['box', 'datalim']: + fig, axs = plt.subplots(nrows=2, sharex=True) + axs[0].set_aspect(2, adjustable=adjustable, share=True) + assert axs[1].get_aspect() == 2 + assert axs[1].get_adjustable() == adjustable + + fig, axs = plt.subplots(nrows=2, sharex=True) + axs[0].set_aspect(2, adjustable=adjustable) + assert axs[1].get_aspect() == 'auto' + + +def test_shared_with_aspect_2(): + # Share 2 axes only with 'box': + fig, axs = plt.subplots(nrows=2, sharex=True, sharey=True) + axs[0].set_aspect(2, share=True) + axs[0].plot([1, 2], [3, 4]) + axs[1].plot([3, 4], [1, 2]) + plt.draw() # Trigger apply_aspect(). + assert axs[0].get_xlim() == axs[1].get_xlim() + assert axs[0].get_ylim() == axs[1].get_ylim() + + +def test_shared_with_aspect_3(): + # Different aspect ratios: + for adjustable in ['box', 'datalim']: + fig, axs = plt.subplots(nrows=2, sharey=True) + axs[0].set_aspect(2, adjustable=adjustable) + axs[1].set_aspect(0.5, adjustable=adjustable) + axs[0].plot([1, 2], [3, 4]) + axs[1].plot([3, 4], [1, 2]) + plt.draw() # Trigger apply_aspect(). + assert axs[0].get_xlim() != axs[1].get_xlim() + assert axs[0].get_ylim() == axs[1].get_ylim() + fig_aspect = fig.bbox_inches.height / fig.bbox_inches.width + for ax in axs: + p = ax.get_position() + box_aspect = p.height / p.width + lim_aspect = ax.viewLim.height / ax.viewLim.width + expected = fig_aspect * box_aspect / lim_aspect + assert round(expected, 4) == round(ax.get_aspect(), 4) + + +def test_shared_aspect_error(): + fig, axes = plt.subplots(1, 2, sharex=True, sharey=True) + axes[0].axis("equal") + with pytest.raises(RuntimeError, match=r"set_aspect\(..., adjustable="): + fig.draw_without_rendering() + + +@pytest.mark.parametrize('err, args, kwargs, match', + ((TypeError, (1, 2), {}, + r"axis\(\) takes from 0 to 1 positional arguments " + "but 2 were given"), + (ValueError, ('foo', ), {}, + "Unrecognized string 'foo' to axis; try 'on' or " + "'off'"), + (TypeError, ([1, 2], ), {}, + "The first argument to axis*"), + (TypeError, tuple(), {'foo': None}, + r"axis\(\) got an unexpected keyword argument " + "'foo'"), + )) +def test_axis_errors(err, args, kwargs, match): + with pytest.raises(err, match=match): + plt.axis(*args, **kwargs) + + +def test_axis_method_errors(): + ax = plt.gca() + with pytest.raises(ValueError, match="unknown value for which: 'foo'"): + ax.get_xaxis_transform('foo') + with pytest.raises(ValueError, match="unknown value for which: 'foo'"): + ax.get_yaxis_transform('foo') + with pytest.raises(TypeError, match="Cannot supply both positional and"): + ax.set_prop_cycle('foo', label='bar') + with pytest.raises(ValueError, match="argument must be among"): + ax.set_anchor('foo') + with pytest.raises(ValueError, match="scilimits must be a sequence"): + ax.ticklabel_format(scilimits=1) + with pytest.raises(TypeError, match="Specifying 'loc' is disallowed"): + ax.set_xlabel('foo', loc='left', x=1) + with pytest.raises(TypeError, match="Specifying 'loc' is disallowed"): + ax.set_ylabel('foo', loc='top', y=1) + with pytest.raises(TypeError, match="Cannot pass both 'left'"): + ax.set_xlim(left=0, xmin=1) + with pytest.raises(TypeError, match="Cannot pass both 'right'"): + ax.set_xlim(right=0, xmax=1) + with pytest.raises(TypeError, match="Cannot pass both 'bottom'"): + ax.set_ylim(bottom=0, ymin=1) + with pytest.raises(TypeError, match="Cannot pass both 'top'"): + ax.set_ylim(top=0, ymax=1) + + +@pytest.mark.parametrize('twin', ('x', 'y')) +def test_twin_with_aspect(twin): + fig, ax = plt.subplots() + # test twinx or twiny + ax_twin = getattr(ax, f'twin{twin}')() + ax.set_aspect(5) + ax_twin.set_aspect(2) + assert_array_equal(ax.bbox.extents, + ax_twin.bbox.extents) + + +def test_relim_visible_only(): + x1 = (0., 10.) + y1 = (0., 10.) + x2 = (-10., 20.) + y2 = (-10., 30.) + + fig = matplotlib.figure.Figure() + ax = fig.add_subplot() + ax.plot(x1, y1) + assert ax.get_xlim() == x1 + assert ax.get_ylim() == y1 + line, = ax.plot(x2, y2) + assert ax.get_xlim() == x2 + assert ax.get_ylim() == y2 + line.set_visible(False) + assert ax.get_xlim() == x2 + assert ax.get_ylim() == y2 + + ax.relim(visible_only=True) + ax.autoscale_view() + + assert ax.get_xlim() == x1 + assert ax.get_ylim() == y1 + + +def test_text_labelsize(): + """ + tests for issue #1172 + """ + fig = plt.figure() + ax = fig.gca() + ax.tick_params(labelsize='large') + ax.tick_params(direction='out') + + +# Note: The `pie` image tests were affected by Numpy 2.0 changing promotions +# (NEP 50). While the changes were only marginal, tolerances were introduced. +# These tolerances could likely go away when numpy 2.0 is the minimum supported +# numpy and the images are regenerated. + +@image_comparison(['pie_default.png'], tol=0.01) +def test_pie_default(): + # The slices will be ordered and plotted counter-clockwise. + labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') + fig1, ax1 = plt.subplots(figsize=(8, 6)) + ax1.pie(sizes, explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90) + + +@image_comparison(['pie_linewidth_0', 'pie_linewidth_0', 'pie_linewidth_0'], + extensions=['png'], style='mpl20', tol=0.01) +def test_pie_linewidth_0(): + # The slices will be ordered and plotted counter-clockwise. + labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') + + plt.pie(sizes, explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90, + wedgeprops={'linewidth': 0}) + # Set aspect ratio to be equal so that pie is drawn as a circle. + plt.axis('equal') + + # Reuse testcase from above for a labeled data test + data = {"l": labels, "s": sizes, "c": colors, "ex": explode} + fig = plt.figure() + ax = fig.gca() + ax.pie("s", explode="ex", labels="l", colors="c", + autopct='%1.1f%%', shadow=True, startangle=90, + wedgeprops={'linewidth': 0}, data=data) + ax.axis('equal') + + # And again to test the pyplot functions which should also be able to be + # called with a data kwarg + plt.figure() + plt.pie("s", explode="ex", labels="l", colors="c", + autopct='%1.1f%%', shadow=True, startangle=90, + wedgeprops={'linewidth': 0}, data=data) + plt.axis('equal') + + +@image_comparison(['pie_center_radius.png'], style='mpl20', tol=0.01) +def test_pie_center_radius(): + # The slices will be ordered and plotted counter-clockwise. + labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') + + plt.pie(sizes, explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90, + wedgeprops={'linewidth': 0}, center=(1, 2), radius=1.5) + + plt.annotate("Center point", xy=(1, 2), xytext=(1, 1.3), + arrowprops=dict(arrowstyle="->", + connectionstyle="arc3"), + bbox=dict(boxstyle="square", facecolor="lightgrey")) + # Set aspect ratio to be equal so that pie is drawn as a circle. + plt.axis('equal') + + +@image_comparison(['pie_linewidth_2.png'], style='mpl20', tol=0.01) +def test_pie_linewidth_2(): + # The slices will be ordered and plotted counter-clockwise. + labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') + + plt.pie(sizes, explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90, + wedgeprops={'linewidth': 2}) + # Set aspect ratio to be equal so that pie is drawn as a circle. + plt.axis('equal') + + +@image_comparison(['pie_ccw_true.png'], style='mpl20', tol=0.01) +def test_pie_ccw_true(): + # The slices will be ordered and plotted counter-clockwise. + labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') + + plt.pie(sizes, explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90, + counterclock=True) + # Set aspect ratio to be equal so that pie is drawn as a circle. + plt.axis('equal') + + +@image_comparison(['pie_frame_grid.png'], style='mpl20', tol=0.002) +def test_pie_frame_grid(): + # The slices will be ordered and plotted counter-clockwise. + labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + # only "explode" the 2nd slice (i.e. 'Hogs') + explode = (0, 0.1, 0, 0) + + plt.pie(sizes, explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90, + wedgeprops={'linewidth': 0}, + frame=True, center=(2, 2)) + + plt.pie(sizes[::-1], explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90, + wedgeprops={'linewidth': 0}, + frame=True, center=(5, 2)) + + plt.pie(sizes, explode=explode[::-1], labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90, + wedgeprops={'linewidth': 0}, + frame=True, center=(3, 5)) + # Set aspect ratio to be equal so that pie is drawn as a circle. + plt.axis('equal') + + +@image_comparison(['pie_rotatelabels_true.png'], style='mpl20', tol=0.009) +def test_pie_rotatelabels_true(): + # The slices will be ordered and plotted counter-clockwise. + labels = 'Hogwarts', 'Frogs', 'Dogs', 'Logs' + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') + + plt.pie(sizes, explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90, + rotatelabels=True) + # Set aspect ratio to be equal so that pie is drawn as a circle. + plt.axis('equal') + + +@image_comparison(['pie_no_label.png'], tol=0.01) +def test_pie_nolabel_but_legend(): + labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') + plt.pie(sizes, explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90, labeldistance=None, + rotatelabels=True) + plt.axis('equal') + plt.ylim(-1.2, 1.2) + plt.legend() + + +@image_comparison(['pie_shadow.png'], style='mpl20', tol=0.002) +def test_pie_shadow(): + # Also acts as a test for the shade argument of Shadow + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice + _, axes = plt.subplots(2, 2) + axes[0][0].pie(sizes, explode=explode, colors=colors, + shadow=True, startangle=90, + wedgeprops={'linewidth': 0}) + + axes[0][1].pie(sizes, explode=explode, colors=colors, + shadow=False, startangle=90, + wedgeprops={'linewidth': 0}) + + axes[1][0].pie(sizes, explode=explode, colors=colors, + shadow={'ox': -0.05, 'oy': -0.05, 'shade': 0.9, 'edgecolor': 'none'}, + startangle=90, wedgeprops={'linewidth': 0}) + + axes[1][1].pie(sizes, explode=explode, colors=colors, + shadow={'ox': 0.05, 'linewidth': 2, 'shade': 0.2}, + startangle=90, wedgeprops={'linewidth': 0}) + + +def test_pie_textprops(): + data = [23, 34, 45] + labels = ["Long name 1", "Long name 2", "Long name 3"] + + textprops = dict(horizontalalignment="center", + verticalalignment="top", + rotation=90, + rotation_mode="anchor", + size=12, color="red") + + _, texts, autopct = plt.gca().pie(data, labels=labels, autopct='%.2f', + textprops=textprops) + for labels in [texts, autopct]: + for tx in labels: + assert tx.get_ha() == textprops["horizontalalignment"] + assert tx.get_va() == textprops["verticalalignment"] + assert tx.get_rotation() == textprops["rotation"] + assert tx.get_rotation_mode() == textprops["rotation_mode"] + assert tx.get_size() == textprops["size"] + assert tx.get_color() == textprops["color"] + + +def test_pie_get_negative_values(): + # Test the ValueError raised when feeding negative values into axes.pie + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.pie([5, 5, -3], explode=[0, .1, .2]) + + +def test_pie_invalid_explode(): + # Test ValueError raised when feeding short explode list to axes.pie + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.pie([1, 2, 3], explode=[0.1, 0.1]) + + +def test_pie_invalid_labels(): + # Test ValueError raised when feeding short labels list to axes.pie + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.pie([1, 2, 3], labels=["One", "Two"]) + + +def test_pie_invalid_radius(): + # Test ValueError raised when feeding negative radius to axes.pie + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.pie([1, 2, 3], radius=-5) + + +def test_normalize_kwarg_pie(): + fig, ax = plt.subplots() + x = [0.3, 0.3, 0.1] + t1 = ax.pie(x=x, normalize=True) + assert abs(t1[0][-1].theta2 - 360.) < 1e-3 + t2 = ax.pie(x=x, normalize=False) + assert abs(t2[0][-1].theta2 - 360.) > 1e-3 + + +@check_figures_equal(extensions=['png']) +def test_pie_hatch_single(fig_test, fig_ref): + x = [0.3, 0.3, 0.1] + hatch = '+' + fig_test.subplots().pie(x, hatch=hatch) + wedges, _ = fig_ref.subplots().pie(x) + [w.set_hatch(hatch) for w in wedges] + + +@check_figures_equal(extensions=['png']) +def test_pie_hatch_multi(fig_test, fig_ref): + x = [0.3, 0.3, 0.1] + hatch = ['/', '+', '.'] + fig_test.subplots().pie(x, hatch=hatch) + wedges, _ = fig_ref.subplots().pie(x) + [w.set_hatch(hp) for w, hp in zip(wedges, hatch)] + + +@image_comparison(['set_get_ticklabels.png'], + tol=0 if platform.machine() == 'x86_64' else 0.025) +def test_set_get_ticklabels(): + # test issue 2246 + fig, ax = plt.subplots(2) + ha = ['normal', 'set_x/yticklabels'] + + ax[0].plot(np.arange(10)) + ax[0].set_title(ha[0]) + + ax[1].plot(np.arange(10)) + ax[1].set_title(ha[1]) + + # set ticklabel to 1 plot in normal way + ax[0].set_xticks(range(10)) + ax[0].set_yticks(range(10)) + ax[0].set_xticklabels(['a', 'b', 'c', 'd'] + 6 * ['']) + ax[0].set_yticklabels(['11', '12', '13', '14'] + 6 * ['']) + + # set ticklabel to the other plot, expect the 2 plots have same label + # setting pass get_ticklabels return value as ticklabels argument + ax[1].set_xticks(ax[0].get_xticks()) + ax[1].set_yticks(ax[0].get_yticks()) + ax[1].set_xticklabels(ax[0].get_xticklabels()) + ax[1].set_yticklabels(ax[0].get_yticklabels()) + + +def test_set_ticks_kwargs_raise_error_without_labels(): + """ + When labels=None and any kwarg is passed, axis.set_ticks() raises a + ValueError. + """ + fig, ax = plt.subplots() + ticks = [1, 2, 3] + with pytest.raises(ValueError, match="Incorrect use of keyword argument 'alpha'"): + ax.xaxis.set_ticks(ticks, alpha=0.5) + + +@check_figures_equal(extensions=["png"]) +def test_set_ticks_with_labels(fig_test, fig_ref): + """ + Test that these two are identical:: + + set_xticks(ticks); set_xticklabels(labels, **kwargs) + set_xticks(ticks, labels, **kwargs) + + """ + ax = fig_ref.subplots() + ax.set_xticks([1, 2, 4, 6]) + ax.set_xticklabels(['a', 'b', 'c', 'd'], fontweight='bold') + ax.set_yticks([1, 3, 5]) + ax.set_yticks([2, 4], minor=True) + ax.set_yticklabels(['A', 'B'], minor=True) + + ax = fig_test.subplots() + ax.set_xticks([1, 2, 4, 6], ['a', 'b', 'c', 'd'], fontweight='bold') + ax.set_yticks([1, 3, 5]) + ax.set_yticks([2, 4], ['A', 'B'], minor=True) + + +def test_xticks_bad_args(): + ax = plt.figure().add_subplot() + with pytest.raises(TypeError, match='must be a sequence'): + ax.set_xticks([2, 9], 3.1) + with pytest.raises(ValueError, match='must be 1D'): + plt.xticks(np.arange(4).reshape((-1, 1))) + with pytest.raises(ValueError, match='must be 1D'): + plt.xticks(np.arange(4).reshape((1, -1))) + with pytest.raises(ValueError, match='must be 1D'): + plt.xticks(np.arange(4).reshape((-1, 1)), labels=range(4)) + with pytest.raises(ValueError, match='must be 1D'): + plt.xticks(np.arange(4).reshape((1, -1)), labels=range(4)) + + +def test_subsampled_ticklabels(): + # test issue 11937 + fig, ax = plt.subplots() + ax.plot(np.arange(10)) + ax.xaxis.set_ticks(np.arange(10) + 0.1) + ax.locator_params(nbins=5) + ax.xaxis.set_ticklabels([c for c in "bcdefghijk"]) + plt.draw() + labels = [t.get_text() for t in ax.xaxis.get_ticklabels()] + assert labels == ['b', 'd', 'f', 'h', 'j'] + + +def test_mismatched_ticklabels(): + fig, ax = plt.subplots() + ax.plot(np.arange(10)) + ax.xaxis.set_ticks([1.5, 2.5]) + with pytest.raises(ValueError): + ax.xaxis.set_ticklabels(['a', 'b', 'c']) + + +def test_empty_ticks_fixed_loc(): + # Smoke test that [] can be used to unset all tick labels + fig, ax = plt.subplots() + ax.bar([1, 2], [1, 2]) + ax.set_xticks([1, 2]) + ax.set_xticklabels([]) + + +@image_comparison(['retain_tick_visibility.png']) +def test_retain_tick_visibility(): + fig, ax = plt.subplots() + plt.plot([0, 1, 2], [0, -1, 4]) + plt.setp(ax.get_yticklabels(), visible=False) + ax.tick_params(axis="y", which="both", length=0) + + +def test_warn_too_few_labels(): + # note that the axis is still using an AutoLocator: + fig, ax = plt.subplots() + with pytest.warns( + UserWarning, + match=r'set_ticklabels\(\) should only be used with a fixed number'): + ax.set_xticklabels(['0', '0.1']) + # note that the axis is still using a FixedLocator: + fig, ax = plt.subplots() + ax.set_xticks([0, 0.5, 1]) + with pytest.raises(ValueError, + match='The number of FixedLocator locations'): + ax.set_xticklabels(['0', '0.1']) + + +def test_tick_label_update(): + # test issue 9397 + + fig, ax = plt.subplots() + + # Set up a dummy formatter + def formatter_func(x, pos): + return "unit value" if x == 1 else "" + ax.xaxis.set_major_formatter(plt.FuncFormatter(formatter_func)) + + # Force some of the x-axis ticks to be outside of the drawn range + ax.set_xticks([-1, 0, 1, 2, 3]) + ax.set_xlim(-0.5, 2.5) + + fig.canvas.draw() + tick_texts = [tick.get_text() for tick in ax.xaxis.get_ticklabels()] + assert tick_texts == ["", "", "unit value", "", ""] + + +@image_comparison(['o_marker_path_snap.png'], savefig_kwarg={'dpi': 72}) +def test_o_marker_path_snap(): + fig, ax = plt.subplots() + ax.margins(.1) + for ms in range(1, 15): + ax.plot([1, 2, ], np.ones(2) + ms, 'o', ms=ms) + + for ms in np.linspace(1, 10, 25): + ax.plot([3, 4, ], np.ones(2) + ms, 'o', ms=ms) + + +def test_margins(): + # test all ways margins can be called + data = [1, 10] + xmin = 0.0 + xmax = len(data) - 1.0 + ymin = min(data) + ymax = max(data) + + fig1, ax1 = plt.subplots(1, 1) + ax1.plot(data) + ax1.margins(1) + assert ax1.margins() == (1, 1) + assert ax1.get_xlim() == (xmin - (xmax - xmin) * 1, + xmax + (xmax - xmin) * 1) + assert ax1.get_ylim() == (ymin - (ymax - ymin) * 1, + ymax + (ymax - ymin) * 1) + + fig2, ax2 = plt.subplots(1, 1) + ax2.plot(data) + ax2.margins(0.5, 2) + assert ax2.margins() == (0.5, 2) + assert ax2.get_xlim() == (xmin - (xmax - xmin) * 0.5, + xmax + (xmax - xmin) * 0.5) + assert ax2.get_ylim() == (ymin - (ymax - ymin) * 2, + ymax + (ymax - ymin) * 2) + + fig3, ax3 = plt.subplots(1, 1) + ax3.plot(data) + ax3.margins(x=-0.2, y=0.5) + assert ax3.margins() == (-0.2, 0.5) + assert ax3.get_xlim() == (xmin - (xmax - xmin) * -0.2, + xmax + (xmax - xmin) * -0.2) + assert ax3.get_ylim() == (ymin - (ymax - ymin) * 0.5, + ymax + (ymax - ymin) * 0.5) + + +def test_margin_getters(): + fig = plt.figure() + ax = fig.add_subplot() + ax.margins(0.2, 0.3) + assert ax.get_xmargin() == 0.2 + assert ax.get_ymargin() == 0.3 + + +def test_set_margin_updates_limits(): + mpl.style.use("default") + fig, ax = plt.subplots() + ax.plot([1, 2], [1, 2]) + ax.set(xscale="log", xmargin=0) + assert ax.get_xlim() == (1, 2) + + +@pytest.mark.parametrize('err, args, kwargs, match', ( + (ValueError, (-1,), {}, r'margin must be greater than -0\.5'), + (ValueError, (1, -1), {}, r'margin must be greater than -0\.5'), + (ValueError, tuple(), {'x': -1}, r'margin must be greater than -0\.5'), + (ValueError, tuple(), {'y': -1}, r'margin must be greater than -0\.5'), + (TypeError, (1, ), {'x': 1, 'y': 1}, + 'Cannot pass both positional and keyword arguments for x and/or y'), + (TypeError, (1, ), {'x': 1}, + 'Cannot pass both positional and keyword arguments for x and/or y'), + (TypeError, (1, 1, 1), {}, 'Must pass a single positional argument'), +)) +def test_margins_errors(err, args, kwargs, match): + with pytest.raises(err, match=match): + fig = plt.figure() + ax = fig.add_subplot() + ax.margins(*args, **kwargs) + + +def test_length_one_hist(): + fig, ax = plt.subplots() + ax.hist(1) + ax.hist([1]) + + +def test_set_xy_bound(): + fig = plt.figure() + ax = fig.add_subplot() + ax.set_xbound(2.0, 3.0) + assert ax.get_xbound() == (2.0, 3.0) + assert ax.get_xlim() == (2.0, 3.0) + ax.set_xbound(upper=4.0) + assert ax.get_xbound() == (2.0, 4.0) + assert ax.get_xlim() == (2.0, 4.0) + ax.set_xbound(lower=3.0) + assert ax.get_xbound() == (3.0, 4.0) + assert ax.get_xlim() == (3.0, 4.0) + + ax.set_ybound(2.0, 3.0) + assert ax.get_ybound() == (2.0, 3.0) + assert ax.get_ylim() == (2.0, 3.0) + ax.set_ybound(upper=4.0) + assert ax.get_ybound() == (2.0, 4.0) + assert ax.get_ylim() == (2.0, 4.0) + ax.set_ybound(lower=3.0) + assert ax.get_ybound() == (3.0, 4.0) + assert ax.get_ylim() == (3.0, 4.0) + + +def test_pathological_hexbin(): + # issue #2863 + mylist = [10] * 100 + fig, ax = plt.subplots(1, 1) + ax.hexbin(mylist, mylist) + fig.savefig(io.BytesIO()) # Check that no warning is emitted. + + +def test_color_None(): + # issue 3855 + fig, ax = plt.subplots() + ax.plot([1, 2], [1, 2], color=None) + + +def test_color_alias(): + # issues 4157 and 4162 + fig, ax = plt.subplots() + line = ax.plot([0, 1], c='lime')[0] + assert 'lime' == line.get_color() + + +def test_numerical_hist_label(): + fig, ax = plt.subplots() + ax.hist([range(15)] * 5, label=range(5)) + ax.legend() + + +def test_unicode_hist_label(): + fig, ax = plt.subplots() + a = (b'\xe5\xbe\x88\xe6\xbc\x82\xe4\xba\xae, ' + + b'r\xc3\xb6m\xc3\xa4n ch\xc3\xa4r\xc3\xa1ct\xc3\xa8rs') + b = b'\xd7\xa9\xd7\x9c\xd7\x95\xd7\x9d' + labels = [a.decode('utf-8'), + 'hi aardvark', + b.decode('utf-8'), + ] + + ax.hist([range(15)] * 3, label=labels) + ax.legend() + + +def test_move_offsetlabel(): + data = np.random.random(10) * 1e-22 + + fig, ax = plt.subplots() + ax.plot(data) + fig.canvas.draw() + before = ax.yaxis.offsetText.get_position() + assert ax.yaxis.offsetText.get_horizontalalignment() == 'left' + ax.yaxis.tick_right() + fig.canvas.draw() + after = ax.yaxis.offsetText.get_position() + assert after[0] > before[0] and after[1] == before[1] + assert ax.yaxis.offsetText.get_horizontalalignment() == 'right' + + fig, ax = plt.subplots() + ax.plot(data) + fig.canvas.draw() + before = ax.xaxis.offsetText.get_position() + assert ax.xaxis.offsetText.get_verticalalignment() == 'top' + ax.xaxis.tick_top() + fig.canvas.draw() + after = ax.xaxis.offsetText.get_position() + assert after[0] == before[0] and after[1] > before[1] + assert ax.xaxis.offsetText.get_verticalalignment() == 'bottom' + + +@image_comparison(['rc_spines.png'], savefig_kwarg={'dpi': 40}) +def test_rc_spines(): + rc_dict = { + 'axes.spines.left': False, + 'axes.spines.right': False, + 'axes.spines.top': False, + 'axes.spines.bottom': False} + with matplotlib.rc_context(rc_dict): + plt.subplots() # create a figure and axes with the spine properties + + +@image_comparison(['rc_grid.png'], savefig_kwarg={'dpi': 40}) +def test_rc_grid(): + fig = plt.figure() + rc_dict0 = { + 'axes.grid': True, + 'axes.grid.axis': 'both' + } + rc_dict1 = { + 'axes.grid': True, + 'axes.grid.axis': 'x' + } + rc_dict2 = { + 'axes.grid': True, + 'axes.grid.axis': 'y' + } + dict_list = [rc_dict0, rc_dict1, rc_dict2] + + for i, rc_dict in enumerate(dict_list, 1): + with matplotlib.rc_context(rc_dict): + fig.add_subplot(3, 1, i) + + +def test_rc_tick(): + d = {'xtick.bottom': False, 'xtick.top': True, + 'ytick.left': True, 'ytick.right': False} + with plt.rc_context(rc=d): + fig = plt.figure() + ax1 = fig.add_subplot(1, 1, 1) + xax = ax1.xaxis + yax = ax1.yaxis + # tick1On bottom/left + assert not xax._major_tick_kw['tick1On'] + assert xax._major_tick_kw['tick2On'] + assert not xax._minor_tick_kw['tick1On'] + assert xax._minor_tick_kw['tick2On'] + + assert yax._major_tick_kw['tick1On'] + assert not yax._major_tick_kw['tick2On'] + assert yax._minor_tick_kw['tick1On'] + assert not yax._minor_tick_kw['tick2On'] + + +def test_rc_major_minor_tick(): + d = {'xtick.top': True, 'ytick.right': True, # Enable all ticks + 'xtick.bottom': True, 'ytick.left': True, + # Selectively disable + 'xtick.minor.bottom': False, 'xtick.major.bottom': False, + 'ytick.major.left': False, 'ytick.minor.left': False} + with plt.rc_context(rc=d): + fig = plt.figure() + ax1 = fig.add_subplot(1, 1, 1) + xax = ax1.xaxis + yax = ax1.yaxis + # tick1On bottom/left + assert not xax._major_tick_kw['tick1On'] + assert xax._major_tick_kw['tick2On'] + assert not xax._minor_tick_kw['tick1On'] + assert xax._minor_tick_kw['tick2On'] + + assert not yax._major_tick_kw['tick1On'] + assert yax._major_tick_kw['tick2On'] + assert not yax._minor_tick_kw['tick1On'] + assert yax._minor_tick_kw['tick2On'] + + +def test_square_plot(): + x = np.arange(4) + y = np.array([1., 3., 5., 7.]) + fig, ax = plt.subplots() + ax.plot(x, y, 'mo') + ax.axis('square') + xlim, ylim = ax.get_xlim(), ax.get_ylim() + assert np.diff(xlim) == np.diff(ylim) + assert ax.get_aspect() == 1 + assert_array_almost_equal( + ax.get_position(original=True).extents, (0.125, 0.1, 0.9, 0.9)) + assert_array_almost_equal( + ax.get_position(original=False).extents, (0.2125, 0.1, 0.8125, 0.9)) + + +def test_bad_plot_args(): + with pytest.raises(ValueError): + plt.plot(None) + with pytest.raises(ValueError): + plt.plot(None, None) + with pytest.raises(ValueError): + plt.plot(np.zeros((2, 2)), np.zeros((2, 3))) + with pytest.raises(ValueError): + plt.plot((np.arange(5).reshape((1, -1)), np.arange(5).reshape(-1, 1))) + + +@pytest.mark.parametrize( + "xy, cls", [ + ((), mpl.image.AxesImage), # (0, N) + (((3, 7), (2, 6)), mpl.image.AxesImage), # (xmin, xmax) + ((range(5), range(4)), mpl.image.AxesImage), # regular grid + (([1, 2, 4, 8, 16], [0, 1, 2, 3]), # irregular grid + mpl.image.PcolorImage), + ((np.random.random((4, 5)), np.random.random((4, 5))), # 2D coords + mpl.collections.QuadMesh), + ] +) +@pytest.mark.parametrize( + "data", [np.arange(12).reshape((3, 4)), np.random.rand(3, 4, 3)] +) +def test_pcolorfast(xy, data, cls): + fig, ax = plt.subplots() + assert type(ax.pcolorfast(*xy, data)) == cls + + +def test_pcolorfast_bad_dims(): + fig, ax = plt.subplots() + with pytest.raises( + TypeError, match=("the given X was 1D and the given Y was 2D")): + ax.pcolorfast(np.empty(6), np.empty((4, 7)), np.empty((8, 8))) + + +def test_pcolorfast_regular_xy_incompatible_size(): + """ + Test that the sizes of X, Y, C are compatible for regularly spaced X, Y. + + Note that after the regualar-spacing check, pcolorfast may go into the + fast "image" mode, where the individual X, Y positions are not used anymore. + Therefore, the algorithm had worked with any regularly number of regularly + spaced values, but discarded their values. + """ + fig, ax = plt.subplots() + with pytest.raises( + ValueError, match=r"Length of X \(5\) must be one larger than the " + r"number of columns in C \(20\)"): + ax.pcolorfast(np.arange(5), np.arange(11), np.random.rand(10, 20)) + + with pytest.raises( + ValueError, match=r"Length of Y \(5\) must be one larger than the " + r"number of rows in C \(10\)"): + ax.pcolorfast(np.arange(21), np.arange(5), np.random.rand(10, 20)) + + +def test_shared_scale(): + fig, axs = plt.subplots(2, 2, sharex=True, sharey=True) + + axs[0, 0].set_xscale("log") + axs[0, 0].set_yscale("log") + + for ax in axs.flat: + assert ax.get_yscale() == 'log' + assert ax.get_xscale() == 'log' + + axs[1, 1].set_xscale("linear") + axs[1, 1].set_yscale("linear") + + for ax in axs.flat: + assert ax.get_yscale() == 'linear' + assert ax.get_xscale() == 'linear' + + +def test_shared_bool(): + with pytest.raises(TypeError): + plt.subplot(sharex=True) + with pytest.raises(TypeError): + plt.subplot(sharey=True) + + +def test_violin_point_mass(): + """Violin plot should handle point mass pdf gracefully.""" + plt.violinplot(np.array([0, 0])) + + +def generate_errorbar_inputs(): + base_xy = cycler('x', [np.arange(5)]) + cycler('y', [np.ones(5)]) + err_cycler = cycler('err', [1, + [1, 1, 1, 1, 1], + [[1, 1, 1, 1, 1], + [1, 1, 1, 1, 1]], + np.ones(5), + np.ones((2, 5)), + None + ]) + xerr_cy = cycler('xerr', err_cycler) + yerr_cy = cycler('yerr', err_cycler) + + empty = ((cycler('x', [[]]) + cycler('y', [[]])) * + cycler('xerr', [[], None]) * cycler('yerr', [[], None])) + xerr_only = base_xy * xerr_cy + yerr_only = base_xy * yerr_cy + both_err = base_xy * yerr_cy * xerr_cy + + return [*xerr_only, *yerr_only, *both_err, *empty] + + +@pytest.mark.parametrize('kwargs', generate_errorbar_inputs()) +def test_errorbar_inputs_shotgun(kwargs): + ax = plt.gca() + eb = ax.errorbar(**kwargs) + eb.remove() + + +@image_comparison(["dash_offset"], remove_text=True) +def test_dash_offset(): + fig, ax = plt.subplots() + x = np.linspace(0, 10) + y = np.ones_like(x) + for j in range(0, 100, 2): + ax.plot(x, j*y, ls=(j, (10, 10)), lw=5, color='k') + + +def test_title_pad(): + # check that title padding puts the title in the right + # place... + fig, ax = plt.subplots() + ax.set_title('aardvark', pad=30.) + m = ax.titleOffsetTrans.get_matrix() + assert m[1, -1] == (30. / 72. * fig.dpi) + ax.set_title('aardvark', pad=0.) + m = ax.titleOffsetTrans.get_matrix() + assert m[1, -1] == 0. + # check that it is reverted... + ax.set_title('aardvark', pad=None) + m = ax.titleOffsetTrans.get_matrix() + assert m[1, -1] == (matplotlib.rcParams['axes.titlepad'] / 72. * fig.dpi) + + +def test_title_location_roundtrip(): + fig, ax = plt.subplots() + # set default title location + plt.rcParams['axes.titlelocation'] = 'center' + ax.set_title('aardvark') + ax.set_title('left', loc='left') + ax.set_title('right', loc='right') + + assert 'left' == ax.get_title(loc='left') + assert 'right' == ax.get_title(loc='right') + assert 'aardvark' == ax.get_title(loc='center') + + with pytest.raises(ValueError): + ax.get_title(loc='foo') + with pytest.raises(ValueError): + ax.set_title('fail', loc='foo') + + +@pytest.mark.parametrize('sharex', [True, False]) +def test_title_location_shared(sharex): + fig, axs = plt.subplots(2, 1, sharex=sharex) + axs[0].set_title('A', pad=-40) + axs[1].set_title('B', pad=-40) + fig.draw_without_rendering() + x, y1 = axs[0].title.get_position() + x, y2 = axs[1].title.get_position() + assert y1 == y2 == 1.0 + + +@image_comparison(["loglog.png"], remove_text=True, tol=0.02) +def test_loglog(): + fig, ax = plt.subplots() + x = np.arange(1, 11) + ax.loglog(x, x**3, lw=5) + ax.tick_params(length=25, width=2) + ax.tick_params(length=15, width=2, which='minor') + + +@image_comparison(["test_loglog_nonpos.png"], remove_text=True, style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.029) +def test_loglog_nonpos(): + fig, axs = plt.subplots(3, 3) + x = np.arange(1, 11) + y = x**3 + y[7] = -3. + x[4] = -10 + for (mcy, mcx), ax in zip(product(['mask', 'clip', ''], repeat=2), + axs.flat): + if mcx == mcy: + if mcx: + ax.loglog(x, y**3, lw=2, nonpositive=mcx) + else: + ax.loglog(x, y**3, lw=2) + else: + ax.loglog(x, y**3, lw=2) + if mcx: + ax.set_xscale("log", nonpositive=mcx) + if mcy: + ax.set_yscale("log", nonpositive=mcy) + + +@mpl.style.context('default') +def test_axes_margins(): + fig, ax = plt.subplots() + ax.plot([0, 1, 2, 3]) + assert ax.get_ybound()[0] != 0 + + fig, ax = plt.subplots() + ax.bar([0, 1, 2, 3], [1, 1, 1, 1]) + assert ax.get_ybound()[0] == 0 + + fig, ax = plt.subplots() + ax.barh([0, 1, 2, 3], [1, 1, 1, 1]) + assert ax.get_xbound()[0] == 0 + + fig, ax = plt.subplots() + ax.pcolor(np.zeros((10, 10))) + assert ax.get_xbound() == (0, 10) + assert ax.get_ybound() == (0, 10) + + fig, ax = plt.subplots() + ax.pcolorfast(np.zeros((10, 10))) + assert ax.get_xbound() == (0, 10) + assert ax.get_ybound() == (0, 10) + + fig, ax = plt.subplots() + ax.hist(np.arange(10)) + assert ax.get_ybound()[0] == 0 + + fig, ax = plt.subplots() + ax.imshow(np.zeros((10, 10))) + assert ax.get_xbound() == (-0.5, 9.5) + assert ax.get_ybound() == (-0.5, 9.5) + + +@pytest.fixture(params=['x', 'y']) +def shared_axis_remover(request): + def _helper_x(ax): + ax2 = ax.twinx() + ax2.remove() + ax.set_xlim(0, 15) + r = ax.xaxis.get_major_locator()() + assert r[-1] > 14 + + def _helper_y(ax): + ax2 = ax.twiny() + ax2.remove() + ax.set_ylim(0, 15) + r = ax.yaxis.get_major_locator()() + assert r[-1] > 14 + + return {"x": _helper_x, "y": _helper_y}[request.param] + + +@pytest.fixture(params=['gca', 'subplots', 'subplots_shared', 'add_axes']) +def shared_axes_generator(request): + # test all of the ways to get fig/ax sets + if request.param == 'gca': + fig = plt.figure() + ax = fig.gca() + elif request.param == 'subplots': + fig, ax = plt.subplots() + elif request.param == 'subplots_shared': + fig, ax_lst = plt.subplots(2, 2, sharex='all', sharey='all') + ax = ax_lst[0][0] + elif request.param == 'add_axes': + fig = plt.figure() + ax = fig.add_axes([.1, .1, .8, .8]) + return fig, ax + + +def test_remove_shared_axes(shared_axes_generator, shared_axis_remover): + # test all of the ways to get fig/ax sets + fig, ax = shared_axes_generator + shared_axis_remover(ax) + + +def test_remove_shared_axes_relim(): + fig, ax_lst = plt.subplots(2, 2, sharex='all', sharey='all') + ax = ax_lst[0][0] + orig_xlim = ax_lst[0][1].get_xlim() + ax.remove() + ax.set_xlim(0, 5) + assert_array_equal(ax_lst[0][1].get_xlim(), orig_xlim) + + +def test_shared_axes_autoscale(): + l = np.arange(-80, 90, 40) + t = np.random.random_sample((l.size, l.size)) + + fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, sharey=True) + + ax1.set_xlim(-1000, 1000) + ax1.set_ylim(-1000, 1000) + ax1.contour(l, l, t) + + ax2.contour(l, l, t) + assert not ax1.get_autoscalex_on() and not ax2.get_autoscalex_on() + assert not ax1.get_autoscaley_on() and not ax2.get_autoscaley_on() + assert ax1.get_xlim() == ax2.get_xlim() == (-1000, 1000) + assert ax1.get_ylim() == ax2.get_ylim() == (-1000, 1000) + + +def test_adjust_numtick_aspect(): + fig, ax = plt.subplots() + ax.yaxis.get_major_locator().set_params(nbins='auto') + ax.set_xlim(0, 1000) + ax.set_aspect('equal') + fig.canvas.draw() + assert len(ax.yaxis.get_major_locator()()) == 2 + ax.set_ylim(0, 1000) + fig.canvas.draw() + assert len(ax.yaxis.get_major_locator()()) > 2 + + +@mpl.style.context("default") +def test_auto_numticks(): + axs = plt.figure().subplots(4, 4) + for ax in axs.flat: # Tiny, empty subplots have only 3 ticks. + assert [*ax.get_xticks()] == [*ax.get_yticks()] == [0, 0.5, 1] + + +@mpl.style.context("default") +def test_auto_numticks_log(): + # Verify that there are not too many ticks with a large log range. + fig, ax = plt.subplots() + mpl.rcParams['axes.autolimit_mode'] = 'round_numbers' + ax.loglog([1e-20, 1e5], [1e-16, 10]) + assert (np.log10(ax.get_xticks()) == np.arange(-26, 18, 4)).all() + assert (np.log10(ax.get_yticks()) == np.arange(-20, 10, 3)).all() + + +def test_broken_barh_empty(): + fig, ax = plt.subplots() + ax.broken_barh([], (.1, .5)) + + +def test_broken_barh_timedelta(): + """Check that timedelta works as x, dx pair for this method.""" + fig, ax = plt.subplots() + d0 = datetime.datetime(2018, 11, 9, 0, 0, 0) + pp = ax.broken_barh([(d0, datetime.timedelta(hours=1))], [1, 2]) + assert pp.get_paths()[0].vertices[0, 0] == mdates.date2num(d0) + assert pp.get_paths()[0].vertices[2, 0] == mdates.date2num(d0) + 1 / 24 + + +def test_pandas_pcolormesh(pd): + time = pd.date_range('2000-01-01', periods=10) + depth = np.arange(20) + data = np.random.rand(19, 9) + + fig, ax = plt.subplots() + ax.pcolormesh(time, depth, data) + + +def test_pandas_indexing_dates(pd): + dates = np.arange('2005-02', '2005-03', dtype='datetime64[D]') + values = np.sin(range(len(dates))) + df = pd.DataFrame({'dates': dates, 'values': values}) + + ax = plt.gca() + + without_zero_index = df[np.array(df.index) % 2 == 1].copy() + ax.plot('dates', 'values', data=without_zero_index) + + +def test_pandas_errorbar_indexing(pd): + df = pd.DataFrame(np.random.uniform(size=(5, 4)), + columns=['x', 'y', 'xe', 'ye'], + index=[1, 2, 3, 4, 5]) + fig, ax = plt.subplots() + ax.errorbar('x', 'y', xerr='xe', yerr='ye', data=df) + + +def test_pandas_index_shape(pd): + df = pd.DataFrame({"XX": [4, 5, 6], "YY": [7, 1, 2]}) + fig, ax = plt.subplots() + ax.plot(df.index, df['YY']) + + +def test_pandas_indexing_hist(pd): + ser_1 = pd.Series(data=[1, 2, 2, 3, 3, 4, 4, 4, 4, 5]) + ser_2 = ser_1.iloc[1:] + fig, ax = plt.subplots() + ax.hist(ser_2) + + +def test_pandas_bar_align_center(pd): + # Tests fix for issue 8767 + df = pd.DataFrame({'a': range(2), 'b': range(2)}) + + fig, ax = plt.subplots(1) + + ax.bar(df.loc[df['a'] == 1, 'b'], + df.loc[df['a'] == 1, 'b'], + align='center') + + fig.canvas.draw() + + +def test_axis_get_tick_params(): + axis = plt.subplot().yaxis + initial_major_style_translated = {**axis.get_tick_params(which='major')} + initial_minor_style_translated = {**axis.get_tick_params(which='minor')} + + translated_major_kw = axis._translate_tick_params( + axis._major_tick_kw, reverse=True + ) + translated_minor_kw = axis._translate_tick_params( + axis._minor_tick_kw, reverse=True + ) + + assert translated_major_kw == initial_major_style_translated + assert translated_minor_kw == initial_minor_style_translated + axis.set_tick_params(labelsize=30, labelcolor='red', + direction='out', which='both') + + new_major_style_translated = {**axis.get_tick_params(which='major')} + new_minor_style_translated = {**axis.get_tick_params(which='minor')} + new_major_style = axis._translate_tick_params(new_major_style_translated) + new_minor_style = axis._translate_tick_params(new_minor_style_translated) + assert initial_major_style_translated != new_major_style_translated + assert axis._major_tick_kw == new_major_style + assert initial_minor_style_translated != new_minor_style_translated + assert axis._minor_tick_kw == new_minor_style + + +def test_axis_set_tick_params_labelsize_labelcolor(): + # Tests fix for issue 4346 + axis_1 = plt.subplot() + axis_1.yaxis.set_tick_params(labelsize=30, labelcolor='red', + direction='out') + + # Expected values after setting the ticks + assert axis_1.yaxis.majorTicks[0]._size == 4.0 + assert axis_1.yaxis.majorTicks[0].tick1line.get_color() == 'k' + assert axis_1.yaxis.majorTicks[0].label1.get_size() == 30.0 + assert axis_1.yaxis.majorTicks[0].label1.get_color() == 'red' + + +def test_axes_tick_params_gridlines(): + # Now treating grid params like other Tick params + ax = plt.subplot() + ax.tick_params(grid_color='b', grid_linewidth=5, grid_alpha=0.5, + grid_linestyle='dashdot') + for axis in ax.xaxis, ax.yaxis: + assert axis.majorTicks[0].gridline.get_color() == 'b' + assert axis.majorTicks[0].gridline.get_linewidth() == 5 + assert axis.majorTicks[0].gridline.get_alpha() == 0.5 + assert axis.majorTicks[0].gridline.get_linestyle() == '-.' + + +def test_axes_tick_params_ylabelside(): + # Tests fix for issue 10267 + ax = plt.subplot() + ax.tick_params(labelleft=False, labelright=True, + which='major') + ax.tick_params(labelleft=False, labelright=True, + which='minor') + # expects left false, right true + assert ax.yaxis.majorTicks[0].label1.get_visible() is False + assert ax.yaxis.majorTicks[0].label2.get_visible() is True + assert ax.yaxis.minorTicks[0].label1.get_visible() is False + assert ax.yaxis.minorTicks[0].label2.get_visible() is True + + +def test_axes_tick_params_xlabelside(): + # Tests fix for issue 10267 + ax = plt.subplot() + ax.tick_params(labeltop=True, labelbottom=False, + which='major') + ax.tick_params(labeltop=True, labelbottom=False, + which='minor') + # expects top True, bottom False + # label1.get_visible() mapped to labelbottom + # label2.get_visible() mapped to labeltop + assert ax.xaxis.majorTicks[0].label1.get_visible() is False + assert ax.xaxis.majorTicks[0].label2.get_visible() is True + assert ax.xaxis.minorTicks[0].label1.get_visible() is False + assert ax.xaxis.minorTicks[0].label2.get_visible() is True + + +def test_none_kwargs(): + ax = plt.figure().subplots() + ln, = ax.plot(range(32), linestyle=None) + assert ln.get_linestyle() == '-' + + +def test_bar_uint8(): + xs = [0, 1, 2, 3] + b = plt.bar(np.array(xs, dtype=np.uint8), [2, 3, 4, 5], align="edge") + for (patch, x) in zip(b.patches, xs): + assert patch.xy[0] == x + + +@image_comparison(['date_timezone_x.png'], tol=1.0) +def test_date_timezone_x(): + # Tests issue 5575 + time_index = [datetime.datetime(2016, 2, 22, hour=x, + tzinfo=dateutil.tz.gettz('Canada/Eastern')) + for x in range(3)] + + # Same Timezone + plt.figure(figsize=(20, 12)) + plt.subplot(2, 1, 1) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + plt.plot_date(time_index, [3] * 3, tz='Canada/Eastern') + + # Different Timezone + plt.subplot(2, 1, 2) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + plt.plot_date(time_index, [3] * 3, tz='UTC') + + +@image_comparison(['date_timezone_y.png']) +def test_date_timezone_y(): + # Tests issue 5575 + time_index = [datetime.datetime(2016, 2, 22, hour=x, + tzinfo=dateutil.tz.gettz('Canada/Eastern')) + for x in range(3)] + + # Same Timezone + plt.figure(figsize=(20, 12)) + plt.subplot(2, 1, 1) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + plt.plot_date([3] * 3, time_index, tz='Canada/Eastern', xdate=False, ydate=True) + + # Different Timezone + plt.subplot(2, 1, 2) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + plt.plot_date([3] * 3, time_index, tz='UTC', xdate=False, ydate=True) + + +@image_comparison(['date_timezone_x_and_y.png'], tol=1.0) +def test_date_timezone_x_and_y(): + # Tests issue 5575 + UTC = datetime.timezone.utc + time_index = [datetime.datetime(2016, 2, 22, hour=x, tzinfo=UTC) + for x in range(3)] + + # Same Timezone + plt.figure(figsize=(20, 12)) + plt.subplot(2, 1, 1) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + plt.plot_date(time_index, time_index, tz='UTC', ydate=True) + + # Different Timezone + plt.subplot(2, 1, 2) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + plt.plot_date(time_index, time_index, tz='US/Eastern', ydate=True) + + +@image_comparison(['axisbelow.png'], remove_text=True) +def test_axisbelow(): + # Test 'line' setting added in 6287. + # Show only grids, not frame or ticks, to make this test + # independent of future change to drawing order of those elements. + axs = plt.figure().subplots(ncols=3, sharex=True, sharey=True) + settings = (False, 'line', True) + + for ax, setting in zip(axs, settings): + ax.plot((0, 10), (0, 10), lw=10, color='m') + circ = mpatches.Circle((3, 3), color='r') + ax.add_patch(circ) + ax.grid(color='c', linestyle='-', linewidth=3) + ax.tick_params(top=False, bottom=False, + left=False, right=False) + ax.spines[:].set_visible(False) + ax.set_axisbelow(setting) + assert ax.get_axisbelow() == setting + + +def test_titletwiny(): + plt.style.use('mpl20') + fig, ax = plt.subplots(dpi=72) + ax2 = ax.twiny() + xlabel2 = ax2.set_xlabel('Xlabel2') + title = ax.set_title('Title') + fig.canvas.draw() + renderer = fig.canvas.get_renderer() + # ------- Test that title is put above Xlabel2 (Xlabel2 at top) ---------- + bbox_y0_title = title.get_window_extent(renderer).y0 # bottom of title + bbox_y1_xlabel2 = xlabel2.get_window_extent(renderer).y1 # top of xlabel2 + y_diff = bbox_y0_title - bbox_y1_xlabel2 + assert np.isclose(y_diff, 3) + + +def test_titlesetpos(): + # Test that title stays put if we set it manually + fig, ax = plt.subplots() + fig.subplots_adjust(top=0.8) + ax2 = ax.twiny() + ax.set_xlabel('Xlabel') + ax2.set_xlabel('Xlabel2') + ax.set_title('Title') + pos = (0.5, 1.11) + ax.title.set_position(pos) + renderer = fig.canvas.get_renderer() + ax._update_title_position(renderer) + assert ax.title.get_position() == pos + + +def test_title_xticks_top(): + # Test that title moves if xticks on top of axes. + mpl.rcParams['axes.titley'] = None + fig, ax = plt.subplots() + ax.xaxis.set_ticks_position('top') + ax.set_title('xlabel top') + fig.canvas.draw() + assert ax.title.get_position()[1] > 1.04 + + +def test_title_xticks_top_both(): + # Test that title moves if xticks on top of axes. + mpl.rcParams['axes.titley'] = None + fig, ax = plt.subplots() + ax.tick_params(axis="x", + bottom=True, top=True, labelbottom=True, labeltop=True) + ax.set_title('xlabel top') + fig.canvas.draw() + assert ax.title.get_position()[1] > 1.04 + + +@pytest.mark.parametrize( + 'left, center', [ + ('left', ''), + ('', 'center'), + ('left', 'center') + ], ids=[ + 'left title moved', + 'center title kept', + 'both titles aligned' + ] +) +def test_title_above_offset(left, center): + # Test that title moves if overlaps with yaxis offset text. + mpl.rcParams['axes.titley'] = None + fig, ax = plt.subplots() + ax.set_ylim(1e11) + ax.set_title(left, loc='left') + ax.set_title(center) + fig.draw_without_rendering() + if left and not center: + assert ax._left_title.get_position()[1] > 1.0 + elif not left and center: + assert ax.title.get_position()[1] == 1.0 + else: + yleft = ax._left_title.get_position()[1] + ycenter = ax.title.get_position()[1] + assert yleft > 1.0 + assert ycenter == yleft + + +def test_title_no_move_off_page(): + # If an Axes is off the figure (ie. if it is cropped during a save) + # make sure that the automatic title repositioning does not get done. + mpl.rcParams['axes.titley'] = None + fig = plt.figure() + ax = fig.add_axes([0.1, -0.5, 0.8, 0.2]) + ax.tick_params(axis="x", + bottom=True, top=True, labelbottom=True, labeltop=True) + tt = ax.set_title('Boo') + fig.canvas.draw() + assert tt.get_position()[1] == 1.0 + + +def test_title_inset_ax(): + # Title should be above any child axes + mpl.rcParams['axes.titley'] = None + fig, ax = plt.subplots() + ax.set_title('Title') + fig.draw_without_rendering() + assert ax.title.get_position()[1] == 1 + ax.inset_axes([0, 1, 1, 0.1]) + fig.draw_without_rendering() + assert ax.title.get_position()[1] == 1.1 + + +def test_offset_label_color(): + # Tests issue 6440 + fig, ax = plt.subplots() + ax.plot([1.01e9, 1.02e9, 1.03e9]) + ax.yaxis.set_tick_params(labelcolor='red') + assert ax.yaxis.get_offset_text().get_color() == 'red' + + +def test_offset_text_visible(): + fig, ax = plt.subplots() + ax.plot([1.01e9, 1.02e9, 1.03e9]) + ax.yaxis.set_tick_params(label1On=False, label2On=True) + assert ax.yaxis.get_offset_text().get_visible() + ax.yaxis.set_tick_params(label2On=False) + assert not ax.yaxis.get_offset_text().get_visible() + + +def test_large_offset(): + fig, ax = plt.subplots() + ax.plot((1 + np.array([0, 1.e-12])) * 1.e27) + fig.canvas.draw() + + +def test_barb_units(): + fig, ax = plt.subplots() + dates = [datetime.datetime(2017, 7, 15, 18, i) for i in range(0, 60, 10)] + y = np.linspace(0, 5, len(dates)) + u = v = np.linspace(0, 50, len(dates)) + ax.barbs(dates, y, u, v) + + +def test_quiver_units(): + fig, ax = plt.subplots() + dates = [datetime.datetime(2017, 7, 15, 18, i) for i in range(0, 60, 10)] + y = np.linspace(0, 5, len(dates)) + u = v = np.linspace(0, 50, len(dates)) + ax.quiver(dates, y, u, v) + + +def test_bar_color_cycle(): + to_rgb = mcolors.to_rgb + fig, ax = plt.subplots() + for j in range(5): + ln, = ax.plot(range(3)) + brs = ax.bar(range(3), range(3)) + for br in brs: + assert to_rgb(ln.get_color()) == to_rgb(br.get_facecolor()) + + +def test_tick_param_label_rotation(): + fix, (ax, ax2) = plt.subplots(1, 2) + ax.plot([0, 1], [0, 1]) + ax2.plot([0, 1], [0, 1]) + ax.xaxis.set_tick_params(which='both', rotation=75) + ax.yaxis.set_tick_params(which='both', rotation=90) + for text in ax.get_xticklabels(which='both'): + assert text.get_rotation() == 75 + for text in ax.get_yticklabels(which='both'): + assert text.get_rotation() == 90 + + ax2.tick_params(axis='x', labelrotation=53) + ax2.tick_params(axis='y', rotation=35) + for text in ax2.get_xticklabels(which='major'): + assert text.get_rotation() == 53 + for text in ax2.get_yticklabels(which='major'): + assert text.get_rotation() == 35 + + +@mpl.style.context('default') +def test_fillbetween_cycle(): + fig, ax = plt.subplots() + + for j in range(3): + cc = ax.fill_between(range(3), range(3)) + target = mcolors.to_rgba(f'C{j}') + assert tuple(cc.get_facecolors().squeeze()) == tuple(target) + + for j in range(3, 6): + cc = ax.fill_betweenx(range(3), range(3)) + target = mcolors.to_rgba(f'C{j}') + assert tuple(cc.get_facecolors().squeeze()) == tuple(target) + + target = mcolors.to_rgba('k') + + for al in ['facecolor', 'facecolors', 'color']: + cc = ax.fill_between(range(3), range(3), **{al: 'k'}) + assert tuple(cc.get_facecolors().squeeze()) == tuple(target) + + edge_target = mcolors.to_rgba('k') + for j, el in enumerate(['edgecolor', 'edgecolors'], start=6): + cc = ax.fill_between(range(3), range(3), **{el: 'k'}) + face_target = mcolors.to_rgba(f'C{j}') + assert tuple(cc.get_facecolors().squeeze()) == tuple(face_target) + assert tuple(cc.get_edgecolors().squeeze()) == tuple(edge_target) + + +def test_log_margins(): + plt.rcParams['axes.autolimit_mode'] = 'data' + fig, ax = plt.subplots() + margin = 0.05 + ax.set_xmargin(margin) + ax.semilogx([10, 100], [10, 100]) + xlim0, xlim1 = ax.get_xlim() + transform = ax.xaxis.get_transform() + xlim0t, xlim1t = transform.transform([xlim0, xlim1]) + x0t, x1t = transform.transform([10, 100]) + delta = (x1t - x0t) * margin + assert_allclose([xlim0t + delta, xlim1t - delta], [x0t, x1t]) + + +def test_color_length_mismatch(): + N = 5 + x, y = np.arange(N), np.arange(N) + colors = np.arange(N+1) + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.scatter(x, y, c=colors) + with pytest.warns(match="argument looks like a single numeric RGB"): + ax.scatter(x, y, c=(0.5, 0.5, 0.5)) + ax.scatter(x, y, c=[(0.5, 0.5, 0.5)] * N) + + +def test_eventplot_legend(): + plt.eventplot([1.0], label='Label') + plt.legend() + + +@pytest.mark.parametrize('err, args, kwargs, match', ( + (ValueError, [[1]], {'lineoffsets': []}, 'lineoffsets cannot be empty'), + (ValueError, [[1]], {'linelengths': []}, 'linelengths cannot be empty'), + (ValueError, [[1]], {'linewidths': []}, 'linewidths cannot be empty'), + (ValueError, [[1]], {'linestyles': []}, 'linestyles cannot be empty'), + (ValueError, [[1]], {'alpha': []}, 'alpha cannot be empty'), + (ValueError, [1], {}, 'positions must be one-dimensional'), + (ValueError, [[1]], {'lineoffsets': [1, 2]}, + 'lineoffsets and positions are unequal sized sequences'), + (ValueError, [[1]], {'linelengths': [1, 2]}, + 'linelengths and positions are unequal sized sequences'), + (ValueError, [[1]], {'linewidths': [1, 2]}, + 'linewidths and positions are unequal sized sequences'), + (ValueError, [[1]], {'linestyles': [1, 2]}, + 'linestyles and positions are unequal sized sequences'), + (ValueError, [[1]], {'alpha': [1, 2]}, + 'alpha and positions are unequal sized sequences'), + (ValueError, [[1]], {'colors': [1, 2]}, + 'colors and positions are unequal sized sequences'), +)) +def test_eventplot_errors(err, args, kwargs, match): + with pytest.raises(err, match=match): + plt.eventplot(*args, **kwargs) + + +def test_bar_broadcast_args(): + fig, ax = plt.subplots() + # Check that a bar chart with a single height for all bars works. + ax.bar(range(4), 1) + # Check that a horizontal chart with one width works. + ax.barh(0, 1, left=range(4), height=1) + # Check that edgecolor gets broadcast. + rect1, rect2 = ax.bar([0, 1], [0, 1], edgecolor=(.1, .2, .3, .4)) + assert rect1.get_edgecolor() == rect2.get_edgecolor() == (.1, .2, .3, .4) + + +def test_invalid_axis_limits(): + plt.plot([0, 1], [0, 1]) + with pytest.raises(ValueError): + plt.xlim(np.nan) + with pytest.raises(ValueError): + plt.xlim(np.inf) + with pytest.raises(ValueError): + plt.ylim(np.nan) + with pytest.raises(ValueError): + plt.ylim(np.inf) + + +# Test all 4 combinations of logs/symlogs for minorticks_on() +@pytest.mark.parametrize('xscale', ['symlog', 'log']) +@pytest.mark.parametrize('yscale', ['symlog', 'log']) +def test_minorticks_on(xscale, yscale): + ax = plt.subplot() + ax.plot([1, 2, 3, 4]) + ax.set_xscale(xscale) + ax.set_yscale(yscale) + ax.minorticks_on() + + +def test_twinx_knows_limits(): + fig, ax = plt.subplots() + + ax.axvspan(1, 2) + xtwin = ax.twinx() + xtwin.plot([0, 0.5], [1, 2]) + # control axis + fig2, ax2 = plt.subplots() + + ax2.axvspan(1, 2) + ax2.plot([0, 0.5], [1, 2]) + + assert_array_equal(xtwin.viewLim.intervalx, ax2.viewLim.intervalx) + + +def test_zero_linewidth(): + # Check that setting a zero linewidth doesn't error + plt.plot([0, 1], [0, 1], ls='--', lw=0) + + +def test_empty_errorbar_legend(): + fig, ax = plt.subplots() + ax.errorbar([], [], xerr=[], label='empty y') + ax.errorbar([], [], yerr=[], label='empty x') + ax.legend() + + +@check_figures_equal(extensions=["png"]) +def test_plot_decimal(fig_test, fig_ref): + x0 = np.arange(-10, 10, 0.3) + y0 = [5.2 * x ** 3 - 2.1 * x ** 2 + 7.34 * x + 4.5 for x in x0] + x = [Decimal(i) for i in x0] + y = [Decimal(i) for i in y0] + # Test image - line plot with Decimal input + fig_test.subplots().plot(x, y) + # Reference image + fig_ref.subplots().plot(x0, y0) + + +# pdf and svg tests fail using travis' old versions of gs and inkscape. +@check_figures_equal(extensions=["png"]) +def test_markerfacecolor_none_alpha(fig_test, fig_ref): + fig_test.subplots().plot(0, "o", mfc="none", alpha=.5) + fig_ref.subplots().plot(0, "o", mfc="w", alpha=.5) + + +def test_tick_padding_tightbbox(): + """Test that tick padding gets turned off if axis is off""" + plt.rcParams["xtick.direction"] = "out" + plt.rcParams["ytick.direction"] = "out" + fig, ax = plt.subplots() + bb = ax.get_tightbbox(fig.canvas.get_renderer()) + ax.axis('off') + bb2 = ax.get_tightbbox(fig.canvas.get_renderer()) + assert bb.x0 < bb2.x0 + assert bb.y0 < bb2.y0 + + +def test_inset(): + """ + Ensure that inset_ax argument is indeed optional + """ + dx, dy = 0.05, 0.05 + # generate 2 2d grids for the x & y bounds + y, x = np.mgrid[slice(1, 5 + dy, dy), + slice(1, 5 + dx, dx)] + z = np.sin(x) ** 10 + np.cos(10 + y * x) * np.cos(x) + + fig, ax = plt.subplots() + ax.pcolormesh(x, y, z[:-1, :-1]) + ax.set_aspect(1.) + ax.apply_aspect() + # we need to apply_aspect to make the drawing below work. + + xlim = [1.5, 2.15] + ylim = [2, 2.5] + + rect = [xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]] + + inset = ax.indicate_inset(bounds=rect) + assert inset.connectors is None + fig.canvas.draw() + xx = np.array([[1.5, 2.], + [2.15, 2.5]]) + assert np.all(inset.rectangle.get_bbox().get_points() == xx) + + +def test_zoom_inset(): + dx, dy = 0.05, 0.05 + # generate 2 2d grids for the x & y bounds + y, x = np.mgrid[slice(1, 5 + dy, dy), + slice(1, 5 + dx, dx)] + z = np.sin(x)**10 + np.cos(10 + y*x) * np.cos(x) + + fig, ax = plt.subplots() + ax.pcolormesh(x, y, z[:-1, :-1]) + ax.set_aspect(1.) + ax.apply_aspect() + # we need to apply_aspect to make the drawing below work. + + # Make the inset_axes... Position axes coordinates... + axin1 = ax.inset_axes([0.7, 0.7, 0.35, 0.35]) + # redraw the data in the inset axes... + axin1.pcolormesh(x, y, z[:-1, :-1]) + axin1.set_xlim([1.5, 2.15]) + axin1.set_ylim([2, 2.5]) + axin1.set_aspect(ax.get_aspect()) + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + rec, connectors = ax.indicate_inset_zoom(axin1) + fig.canvas.draw() + assert len(connectors) == 4 + xx = np.array([[1.5, 2.], + [2.15, 2.5]]) + assert np.all(rec.get_bbox().get_points() == xx) + xx = np.array([[0.6325, 0.692308], + [0.8425, 0.907692]]) + np.testing.assert_allclose( + axin1.get_position().get_points(), xx, rtol=1e-4) + + +@image_comparison(['inset_polar.png'], remove_text=True, style='mpl20') +def test_inset_polar(): + _, ax = plt.subplots() + axins = ax.inset_axes([0.5, 0.1, 0.45, 0.45], polar=True) + assert isinstance(axins, PolarAxes) + + r = np.arange(0, 2, 0.01) + theta = 2 * np.pi * r + + ax.plot(theta, r) + axins.plot(theta, r) + + +def test_inset_projection(): + _, ax = plt.subplots() + axins = ax.inset_axes([0.2, 0.2, 0.3, 0.3], projection="hammer") + assert isinstance(axins, HammerAxes) + + +def test_inset_subclass(): + _, ax = plt.subplots() + axins = ax.inset_axes([0.2, 0.2, 0.3, 0.3], axes_class=AA.Axes) + assert isinstance(axins, AA.Axes) + + +@pytest.mark.parametrize('x_inverted', [False, True]) +@pytest.mark.parametrize('y_inverted', [False, True]) +def test_indicate_inset_inverted(x_inverted, y_inverted): + """ + Test that the inset lines are correctly located with inverted data axes. + """ + fig, (ax1, ax2) = plt.subplots(1, 2) + + x = np.arange(10) + ax1.plot(x, x, 'o') + if x_inverted: + ax1.invert_xaxis() + if y_inverted: + ax1.invert_yaxis() + + inset = ax1.indicate_inset([2, 2, 5, 4], ax2) + lower_left, upper_left, lower_right, upper_right = inset.connectors + + sign_x = -1 if x_inverted else 1 + sign_y = -1 if y_inverted else 1 + assert sign_x * (lower_right.xy2[0] - lower_left.xy2[0]) > 0 + assert sign_x * (upper_right.xy2[0] - upper_left.xy2[0]) > 0 + assert sign_y * (upper_left.xy2[1] - lower_left.xy2[1]) > 0 + assert sign_y * (upper_right.xy2[1] - lower_right.xy2[1]) > 0 + + +def test_set_position(): + fig, ax = plt.subplots() + ax.set_aspect(3.) + ax.set_position([0.1, 0.1, 0.4, 0.4], which='both') + assert np.allclose(ax.get_position().width, 0.1) + ax.set_aspect(2.) + ax.set_position([0.1, 0.1, 0.4, 0.4], which='original') + assert np.allclose(ax.get_position().width, 0.15) + ax.set_aspect(3.) + ax.set_position([0.1, 0.1, 0.4, 0.4], which='active') + assert np.allclose(ax.get_position().width, 0.1) + + +def test_spines_properbbox_after_zoom(): + fig, ax = plt.subplots() + bb = ax.spines.bottom.get_window_extent(fig.canvas.get_renderer()) + # this is what zoom calls: + ax._set_view_from_bbox((320, 320, 500, 500), 'in', + None, False, False) + bb2 = ax.spines.bottom.get_window_extent(fig.canvas.get_renderer()) + np.testing.assert_allclose(bb.get_points(), bb2.get_points(), rtol=1e-6) + + +def test_limits_after_scroll_zoom(): + fig, ax = plt.subplots() + # + xlim = (-0.5, 0.5) + ylim = (-1, 2) + ax.set_xlim(xlim) + ax.set_ylim(ymin=ylim[0], ymax=ylim[1]) + # This is what scroll zoom calls: + # Zoom with factor 1, small numerical change + ax._set_view_from_bbox((200, 200, 1.)) + np.testing.assert_allclose(xlim, ax.get_xlim(), atol=1e-16) + np.testing.assert_allclose(ylim, ax.get_ylim(), atol=1e-16) + + # Zoom in + ax._set_view_from_bbox((200, 200, 2.)) + # Hard-coded values + new_xlim = (-0.3790322580645161, 0.12096774193548387) + new_ylim = (-0.40625, 1.09375) + + res_xlim = ax.get_xlim() + res_ylim = ax.get_ylim() + np.testing.assert_allclose(res_xlim[1] - res_xlim[0], 0.5) + np.testing.assert_allclose(res_ylim[1] - res_ylim[0], 1.5) + np.testing.assert_allclose(new_xlim, res_xlim, atol=1e-16) + np.testing.assert_allclose(new_ylim, res_ylim) + + # Zoom out, should be same as before, except for numerical issues + ax._set_view_from_bbox((200, 200, 0.5)) + res_xlim = ax.get_xlim() + res_ylim = ax.get_ylim() + np.testing.assert_allclose(res_xlim[1] - res_xlim[0], 1) + np.testing.assert_allclose(res_ylim[1] - res_ylim[0], 3) + np.testing.assert_allclose(xlim, res_xlim, atol=1e-16) + np.testing.assert_allclose(ylim, res_ylim, atol=1e-16) + + +def test_gettightbbox_ignore_nan(): + fig, ax = plt.subplots() + remove_ticks_and_titles(fig) + ax.text(np.nan, 1, 'Boo') + renderer = fig.canvas.get_renderer() + np.testing.assert_allclose(ax.get_tightbbox(renderer).width, 496) + + +def test_scatter_series_non_zero_index(pd): + # create non-zero index + ids = range(10, 18) + x = pd.Series(np.random.uniform(size=8), index=ids) + y = pd.Series(np.random.uniform(size=8), index=ids) + c = pd.Series([1, 1, 1, 1, 1, 0, 0, 0], index=ids) + plt.scatter(x, y, c) + + +def test_scatter_empty_data(): + # making sure this does not raise an exception + plt.scatter([], []) + plt.scatter([], [], s=[], c=[]) + + +@image_comparison(['annotate_across_transforms.png'], style='mpl20', remove_text=True, + tol=0 if platform.machine() == 'x86_64' else 0.025) +def test_annotate_across_transforms(): + x = np.linspace(0, 10, 200) + y = np.exp(-x) * np.sin(x) + + fig, ax = plt.subplots(figsize=(3.39, 3)) + ax.plot(x, y) + axins = ax.inset_axes([0.4, 0.5, 0.3, 0.3]) + axins.set_aspect(0.2) + axins.xaxis.set_visible(False) + axins.yaxis.set_visible(False) + ax.annotate("", xy=(x[150], y[150]), xycoords=ax.transData, + xytext=(1, 0), textcoords=axins.transAxes, + arrowprops=dict(arrowstyle="->")) + + +class _Translation(mtransforms.Transform): + input_dims = 1 + output_dims = 1 + + def __init__(self, dx): + self.dx = dx + + def transform(self, values): + return values + self.dx + + def inverted(self): + return _Translation(-self.dx) + + +@image_comparison(['secondary_xy.png'], style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.027) +def test_secondary_xy(): + fig, axs = plt.subplots(1, 2, figsize=(10, 5), constrained_layout=True) + + def invert(x): + with np.errstate(divide='ignore'): + return 1 / x + + for nn, ax in enumerate(axs): + ax.plot(np.arange(2, 11), np.arange(2, 11)) + if nn == 0: + secax = ax.secondary_xaxis + else: + secax = ax.secondary_yaxis + + secax(0.2, functions=(invert, invert)) + secax(0.4, functions=(lambda x: 2 * x, lambda x: x / 2)) + secax(0.6, functions=(lambda x: x**2, lambda x: x**(1/2))) + secax(0.8) + secax("top" if nn == 0 else "right", functions=_Translation(2)) + secax(6.25, transform=ax.transData) + + +def test_secondary_fail(): + fig, ax = plt.subplots() + ax.plot(np.arange(2, 11), np.arange(2, 11)) + with pytest.raises(ValueError): + ax.secondary_xaxis(0.2, functions=(lambda x: 1 / x)) + with pytest.raises(ValueError): + ax.secondary_xaxis('right') + with pytest.raises(ValueError): + ax.secondary_yaxis('bottom') + with pytest.raises(TypeError): + ax.secondary_xaxis(0.2, transform='error') + + +def test_secondary_resize(): + fig, ax = plt.subplots(figsize=(10, 5)) + ax.plot(np.arange(2, 11), np.arange(2, 11)) + + def invert(x): + with np.errstate(divide='ignore'): + return 1 / x + + ax.secondary_xaxis('top', functions=(invert, invert)) + fig.canvas.draw() + fig.set_size_inches((7, 4)) + assert_allclose(ax.get_position().extents, [0.125, 0.1, 0.9, 0.9]) + + +def test_secondary_minorloc(): + fig, ax = plt.subplots(figsize=(10, 5)) + ax.plot(np.arange(2, 11), np.arange(2, 11)) + + def invert(x): + with np.errstate(divide='ignore'): + return 1 / x + + secax = ax.secondary_xaxis('top', functions=(invert, invert)) + assert isinstance(secax._axis.get_minor_locator(), + mticker.NullLocator) + secax.minorticks_on() + assert isinstance(secax._axis.get_minor_locator(), + mticker.AutoMinorLocator) + ax.set_xscale('log') + plt.draw() + assert isinstance(secax._axis.get_minor_locator(), + mticker.LogLocator) + ax.set_xscale('linear') + plt.draw() + assert isinstance(secax._axis.get_minor_locator(), + mticker.NullLocator) + + +def test_secondary_formatter(): + fig, ax = plt.subplots() + ax.set_xscale("log") + secax = ax.secondary_xaxis("top") + secax.xaxis.set_major_formatter(mticker.ScalarFormatter()) + fig.canvas.draw() + assert isinstance( + secax.xaxis.get_major_formatter(), mticker.ScalarFormatter) + + +def test_secondary_repr(): + fig, ax = plt.subplots() + secax = ax.secondary_xaxis("top") + assert repr(secax) == '' + + +@image_comparison(['axis_options.png'], remove_text=True, style='mpl20') +def test_axis_options(): + fig, axes = plt.subplots(2, 3) + for i, option in enumerate(('scaled', 'tight', 'image')): + # Draw a line and a circle fitting within the boundaries of the line + # The circle should look like a circle for 'scaled' and 'image' + # High/narrow aspect ratio + axes[0, i].plot((1, 2), (1, 3.2)) + axes[0, i].axis(option) + axes[0, i].add_artist(mpatches.Circle((1.5, 1.5), radius=0.5, + facecolor='none', edgecolor='k')) + # Low/wide aspect ratio + axes[1, i].plot((1, 2.25), (1, 1.75)) + axes[1, i].axis(option) + axes[1, i].add_artist(mpatches.Circle((1.5, 1.25), radius=0.25, + facecolor='none', edgecolor='k')) + + +def color_boxes(fig, ax): + """ + Helper for the tests below that test the extents of various Axes elements + """ + fig.canvas.draw() + + renderer = fig.canvas.get_renderer() + bbaxis = [] + for nn, axx in enumerate([ax.xaxis, ax.yaxis]): + bb = axx.get_tightbbox(renderer) + if bb: + axisr = mpatches.Rectangle( + (bb.x0, bb.y0), width=bb.width, height=bb.height, + linewidth=0.7, edgecolor='y', facecolor="none", transform=None, + zorder=3) + fig.add_artist(axisr) + bbaxis += [bb] + + bbspines = [] + for nn, a in enumerate(['bottom', 'top', 'left', 'right']): + bb = ax.spines[a].get_window_extent(renderer) + spiner = mpatches.Rectangle( + (bb.x0, bb.y0), width=bb.width, height=bb.height, + linewidth=0.7, edgecolor="green", facecolor="none", transform=None, + zorder=3) + fig.add_artist(spiner) + bbspines += [bb] + + bb = ax.get_window_extent() + rect2 = mpatches.Rectangle( + (bb.x0, bb.y0), width=bb.width, height=bb.height, + linewidth=1.5, edgecolor="magenta", facecolor="none", transform=None, + zorder=2) + fig.add_artist(rect2) + bbax = bb + + bb2 = ax.get_tightbbox(renderer) + rect2 = mpatches.Rectangle( + (bb2.x0, bb2.y0), width=bb2.width, height=bb2.height, + linewidth=3, edgecolor="red", facecolor="none", transform=None, + zorder=1) + fig.add_artist(rect2) + bbtb = bb2 + return bbaxis, bbspines, bbax, bbtb + + +def test_normal_axes(): + with rc_context({'_internal.classic_mode': False}): + fig, ax = plt.subplots(dpi=200, figsize=(6, 6)) + fig.canvas.draw() + plt.close(fig) + bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) + + # test the axis bboxes + target = [ + [123.375, 75.88888888888886, 983.25, 33.0], + [85.51388888888889, 99.99999999999997, 53.375, 993.0] + ] + for nn, b in enumerate(bbaxis): + targetbb = mtransforms.Bbox.from_bounds(*target[nn]) + assert_array_almost_equal(b.bounds, targetbb.bounds, decimal=2) + + target = [ + [150.0, 119.999, 930.0, 11.111], + [150.0, 1080.0, 930.0, 0.0], + [150.0, 119.9999, 11.111, 960.0], + [1068.8888, 119.9999, 11.111, 960.0] + ] + for nn, b in enumerate(bbspines): + targetbb = mtransforms.Bbox.from_bounds(*target[nn]) + assert_array_almost_equal(b.bounds, targetbb.bounds, decimal=2) + + target = [150.0, 119.99999999999997, 930.0, 960.0] + targetbb = mtransforms.Bbox.from_bounds(*target) + assert_array_almost_equal(bbax.bounds, targetbb.bounds, decimal=2) + + target = [85.5138, 75.88888, 1021.11, 1017.11] + targetbb = mtransforms.Bbox.from_bounds(*target) + assert_array_almost_equal(bbtb.bounds, targetbb.bounds, decimal=2) + + # test that get_position roundtrips to get_window_extent + axbb = ax.get_position().transformed(fig.transFigure).bounds + assert_array_almost_equal(axbb, ax.get_window_extent().bounds, decimal=2) + + +def test_nodecorator(): + with rc_context({'_internal.classic_mode': False}): + fig, ax = plt.subplots(dpi=200, figsize=(6, 6)) + fig.canvas.draw() + ax.set(xticklabels=[], yticklabels=[]) + bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) + + # test the axis bboxes + for nn, b in enumerate(bbaxis): + assert b is None + + target = [ + [150.0, 119.999, 930.0, 11.111], + [150.0, 1080.0, 930.0, 0.0], + [150.0, 119.9999, 11.111, 960.0], + [1068.8888, 119.9999, 11.111, 960.0] + ] + for nn, b in enumerate(bbspines): + targetbb = mtransforms.Bbox.from_bounds(*target[nn]) + assert_allclose(b.bounds, targetbb.bounds, atol=1e-2) + + target = [150.0, 119.99999999999997, 930.0, 960.0] + targetbb = mtransforms.Bbox.from_bounds(*target) + assert_allclose(bbax.bounds, targetbb.bounds, atol=1e-2) + + target = [150., 120., 930., 960.] + targetbb = mtransforms.Bbox.from_bounds(*target) + assert_allclose(bbtb.bounds, targetbb.bounds, atol=1e-2) + + +def test_displaced_spine(): + with rc_context({'_internal.classic_mode': False}): + fig, ax = plt.subplots(dpi=200, figsize=(6, 6)) + ax.set(xticklabels=[], yticklabels=[]) + ax.spines.bottom.set_position(('axes', -0.1)) + fig.canvas.draw() + bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) + + targets = [ + [150., 24., 930., 11.111111], + [150.0, 1080.0, 930.0, 0.0], + [150.0, 119.9999, 11.111, 960.0], + [1068.8888, 119.9999, 11.111, 960.0] + ] + for target, bbspine in zip(targets, bbspines): + targetbb = mtransforms.Bbox.from_bounds(*target) + assert_allclose(bbspine.bounds, targetbb.bounds, atol=1e-2) + + target = [150.0, 119.99999999999997, 930.0, 960.0] + targetbb = mtransforms.Bbox.from_bounds(*target) + assert_allclose(bbax.bounds, targetbb.bounds, atol=1e-2) + + target = [150., 24., 930., 1056.] + targetbb = mtransforms.Bbox.from_bounds(*target) + assert_allclose(bbtb.bounds, targetbb.bounds, atol=1e-2) + + +def test_tickdirs(): + """ + Switch the tickdirs and make sure the bboxes switch with them + """ + targets = [[[150.0, 120.0, 930.0, 11.1111], + [150.0, 120.0, 11.111, 960.0]], + [[150.0, 108.8889, 930.0, 11.111111111111114], + [138.889, 120, 11.111, 960.0]], + [[150.0, 114.44444444444441, 930.0, 11.111111111111114], + [144.44444444444446, 119.999, 11.111, 960.0]]] + for dnum, dirs in enumerate(['in', 'out', 'inout']): + with rc_context({'_internal.classic_mode': False}): + fig, ax = plt.subplots(dpi=200, figsize=(6, 6)) + ax.tick_params(direction=dirs) + fig.canvas.draw() + bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) + for nn, num in enumerate([0, 2]): + targetbb = mtransforms.Bbox.from_bounds(*targets[dnum][nn]) + assert_allclose( + bbspines[num].bounds, targetbb.bounds, atol=1e-2) + + +def test_minor_accountedfor(): + with rc_context({'_internal.classic_mode': False}): + fig, ax = plt.subplots(dpi=200, figsize=(6, 6)) + fig.canvas.draw() + ax.tick_params(which='both', direction='out') + + bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) + bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) + targets = [[150.0, 108.88888888888886, 930.0, 11.111111111111114], + [138.8889, 119.9999, 11.1111, 960.0]] + for n in range(2): + targetbb = mtransforms.Bbox.from_bounds(*targets[n]) + assert_allclose( + bbspines[n * 2].bounds, targetbb.bounds, atol=1e-2) + + fig, ax = plt.subplots(dpi=200, figsize=(6, 6)) + fig.canvas.draw() + ax.tick_params(which='both', direction='out') + ax.minorticks_on() + ax.tick_params(axis='both', which='minor', length=30) + fig.canvas.draw() + bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) + targets = [[150.0, 36.66666666666663, 930.0, 83.33333333333334], + [66.6667, 120.0, 83.3333, 960.0]] + + for n in range(2): + targetbb = mtransforms.Bbox.from_bounds(*targets[n]) + assert_allclose( + bbspines[n * 2].bounds, targetbb.bounds, atol=1e-2) + + +@check_figures_equal(extensions=["png"]) +def test_axis_bool_arguments(fig_test, fig_ref): + # Test if False and "off" give the same + fig_test.add_subplot(211).axis(False) + fig_ref.add_subplot(211).axis("off") + # Test if True after False gives the same as "on" + ax = fig_test.add_subplot(212) + ax.axis(False) + ax.axis(True) + fig_ref.add_subplot(212).axis("on") + + +def test_axis_extent_arg(): + fig, ax = plt.subplots() + xmin = 5 + xmax = 10 + ymin = 15 + ymax = 20 + extent = ax.axis([xmin, xmax, ymin, ymax]) + + # test that the docstring is correct + assert tuple(extent) == (xmin, xmax, ymin, ymax) + + # test that limits were set per the docstring + assert (xmin, xmax) == ax.get_xlim() + assert (ymin, ymax) == ax.get_ylim() + + +def test_axis_extent_arg2(): + # Same as test_axis_extent_arg, but with keyword arguments + fig, ax = plt.subplots() + xmin = 5 + xmax = 10 + ymin = 15 + ymax = 20 + extent = ax.axis(xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax) + + # test that the docstring is correct + assert tuple(extent) == (xmin, xmax, ymin, ymax) + + # test that limits were set per the docstring + assert (xmin, xmax) == ax.get_xlim() + assert (ymin, ymax) == ax.get_ylim() + + +def test_hist_auto_bins(): + _, bins, _ = plt.hist([[1, 2, 3], [3, 4, 5, 6]], bins='auto') + assert bins[0] <= 1 + assert bins[-1] >= 6 + + +def test_hist_nan_data(): + fig, (ax1, ax2) = plt.subplots(2) + + data = [1, 2, 3] + nan_data = data + [np.nan] + + bins, edges, _ = ax1.hist(data) + with np.errstate(invalid='ignore'): + nanbins, nanedges, _ = ax2.hist(nan_data) + + np.testing.assert_allclose(bins, nanbins) + np.testing.assert_allclose(edges, nanedges) + + +def test_hist_range_and_density(): + _, bins, _ = plt.hist(np.random.rand(10), "auto", + range=(0, 1), density=True) + assert bins[0] == 0 + assert bins[-1] == 1 + + +def test_bar_errbar_zorder(): + # Check that the zorder of errorbars is always greater than the bar they + # are plotted on + fig, ax = plt.subplots() + x = [1, 2, 3] + barcont = ax.bar(x=x, height=x, yerr=x, capsize=5, zorder=3) + + data_line, caplines, barlinecols = barcont.errorbar.lines + for bar in barcont.patches: + for capline in caplines: + assert capline.zorder > bar.zorder + for barlinecol in barlinecols: + assert barlinecol.zorder > bar.zorder + + +def test_set_ticks_inverted(): + fig, ax = plt.subplots() + ax.invert_xaxis() + ax.set_xticks([.3, .7]) + assert ax.get_xlim() == (1, 0) + ax.set_xticks([-1]) + assert ax.get_xlim() == (1, -1) + + +def test_aspect_nonlinear_adjustable_box(): + fig = plt.figure(figsize=(10, 10)) # Square. + + ax = fig.add_subplot() + ax.plot([.4, .6], [.4, .6]) # Set minpos to keep logit happy. + ax.set(xscale="log", xlim=(1, 10), + yscale="logit", ylim=(1/11, 1/1001), + aspect=1, adjustable="box") + ax.margins(0) + pos = fig.transFigure.transform_bbox(ax.get_position()) + assert pos.height / pos.width == pytest.approx(2) + + +def test_aspect_nonlinear_adjustable_datalim(): + fig = plt.figure(figsize=(10, 10)) # Square. + + ax = fig.add_axes([.1, .1, .8, .8]) # Square. + ax.plot([.4, .6], [.4, .6]) # Set minpos to keep logit happy. + ax.set(xscale="log", xlim=(1, 100), + yscale="logit", ylim=(1 / 101, 1 / 11), + aspect=1, adjustable="datalim") + ax.margins(0) + ax.apply_aspect() + + assert ax.get_xlim() == pytest.approx([1*10**(1/2), 100/10**(1/2)]) + assert ax.get_ylim() == (1 / 101, 1 / 11) + + +def test_box_aspect(): + # Test if axes with box_aspect=1 has same dimensions + # as axes with aspect equal and adjustable="box" + + fig1, ax1 = plt.subplots() + axtwin = ax1.twinx() + axtwin.plot([12, 344]) + + ax1.set_box_aspect(1) + assert ax1.get_box_aspect() == 1.0 + + fig2, ax2 = plt.subplots() + ax2.margins(0) + ax2.plot([0, 2], [6, 8]) + ax2.set_aspect("equal", adjustable="box") + + fig1.canvas.draw() + fig2.canvas.draw() + + bb1 = ax1.get_position() + bbt = axtwin.get_position() + bb2 = ax2.get_position() + + assert_array_equal(bb1.extents, bb2.extents) + assert_array_equal(bbt.extents, bb2.extents) + + +def test_box_aspect_custom_position(): + # Test if axes with custom position and box_aspect + # behaves the same independent of the order of setting those. + + fig1, ax1 = plt.subplots() + ax1.set_position([0.1, 0.1, 0.9, 0.2]) + fig1.canvas.draw() + ax1.set_box_aspect(1.) + + fig2, ax2 = plt.subplots() + ax2.set_box_aspect(1.) + fig2.canvas.draw() + ax2.set_position([0.1, 0.1, 0.9, 0.2]) + + fig1.canvas.draw() + fig2.canvas.draw() + + bb1 = ax1.get_position() + bb2 = ax2.get_position() + + assert_array_equal(bb1.extents, bb2.extents) + + +def test_bbox_aspect_axes_init(): + # Test that box_aspect can be given to axes init and produces + # all equal square axes. + fig, axs = plt.subplots(2, 3, subplot_kw=dict(box_aspect=1), + constrained_layout=True) + fig.canvas.draw() + renderer = fig.canvas.get_renderer() + sizes = [] + for ax in axs.flat: + bb = ax.get_window_extent(renderer) + sizes.extend([bb.width, bb.height]) + + assert_allclose(sizes, sizes[0]) + + +def test_set_aspect_negative(): + fig, ax = plt.subplots() + with pytest.raises(ValueError, match="must be finite and positive"): + ax.set_aspect(-1) + with pytest.raises(ValueError, match="must be finite and positive"): + ax.set_aspect(0) + with pytest.raises(ValueError, match="must be finite and positive"): + ax.set_aspect(np.inf) + with pytest.raises(ValueError, match="must be finite and positive"): + ax.set_aspect(-np.inf) + + +def test_redraw_in_frame(): + fig, ax = plt.subplots(1, 1) + ax.plot([1, 2, 3]) + fig.canvas.draw() + ax.redraw_in_frame() + + +def test_invisible_axes_events(): + # invisible axes should not respond to events... + fig, ax = plt.subplots() + assert fig.canvas.inaxes((200, 200)) is not None + ax.set_visible(False) + assert fig.canvas.inaxes((200, 200)) is None + + +def test_xtickcolor_is_not_markercolor(): + plt.rcParams['lines.markeredgecolor'] = 'white' + ax = plt.axes() + ticks = ax.xaxis.get_major_ticks() + for tick in ticks: + assert tick.tick1line.get_markeredgecolor() != 'white' + + +def test_ytickcolor_is_not_markercolor(): + plt.rcParams['lines.markeredgecolor'] = 'white' + ax = plt.axes() + ticks = ax.yaxis.get_major_ticks() + for tick in ticks: + assert tick.tick1line.get_markeredgecolor() != 'white' + + +@pytest.mark.parametrize('axis', ('x', 'y')) +@pytest.mark.parametrize('auto', (True, False, None)) +def test_unautoscale(axis, auto): + fig, ax = plt.subplots() + x = np.arange(100) + y = np.linspace(-.1, .1, 100) + ax.scatter(y, x) + + get_autoscale_on = getattr(ax, f'get_autoscale{axis}_on') + set_lim = getattr(ax, f'set_{axis}lim') + get_lim = getattr(ax, f'get_{axis}lim') + + post_auto = get_autoscale_on() if auto is None else auto + + set_lim((-0.5, 0.5), auto=auto) + assert post_auto == get_autoscale_on() + fig.canvas.draw() + assert_array_equal(get_lim(), (-0.5, 0.5)) + + +@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)) + + +@mpl.style.context('default') +def test_autoscale_tiny_sticky(): + fig, ax = plt.subplots() + ax.bar(0, 1e-9) + fig.canvas.draw() + assert ax.get_ylim() == (0, 1.05e-9) + + +def test_xtickcolor_is_not_xticklabelcolor(): + plt.rcParams['xtick.color'] = 'yellow' + plt.rcParams['xtick.labelcolor'] = 'blue' + ax = plt.axes() + ticks = ax.xaxis.get_major_ticks() + for tick in ticks: + assert tick.tick1line.get_color() == 'yellow' + assert tick.label1.get_color() == 'blue' + + +def test_ytickcolor_is_not_yticklabelcolor(): + plt.rcParams['ytick.color'] = 'yellow' + plt.rcParams['ytick.labelcolor'] = 'blue' + ax = plt.axes() + ticks = ax.yaxis.get_major_ticks() + for tick in ticks: + assert tick.tick1line.get_color() == 'yellow' + assert tick.label1.get_color() == 'blue' + + +def test_xaxis_offsetText_color(): + plt.rcParams['xtick.labelcolor'] = 'blue' + ax = plt.axes() + assert ax.xaxis.offsetText.get_color() == 'blue' + + plt.rcParams['xtick.color'] = 'yellow' + plt.rcParams['xtick.labelcolor'] = 'inherit' + ax = plt.axes() + assert ax.xaxis.offsetText.get_color() == 'yellow' + + +def test_yaxis_offsetText_color(): + plt.rcParams['ytick.labelcolor'] = 'green' + ax = plt.axes() + assert ax.yaxis.offsetText.get_color() == 'green' + + plt.rcParams['ytick.color'] = 'red' + plt.rcParams['ytick.labelcolor'] = 'inherit' + ax = plt.axes() + assert ax.yaxis.offsetText.get_color() == 'red' + + +@pytest.mark.parametrize('size', [size for size in mfont_manager.font_scalings + if size is not None] + [8, 10, 12]) +@mpl.style.context('default') +def test_relative_ticklabel_sizes(size): + mpl.rcParams['xtick.labelsize'] = size + mpl.rcParams['ytick.labelsize'] = size + fig, ax = plt.subplots() + fig.canvas.draw() + + for name, axis in zip(['x', 'y'], [ax.xaxis, ax.yaxis]): + for tick in axis.get_major_ticks(): + assert tick.label1.get_size() == axis._get_tick_label_size(name) + + +def test_multiplot_autoscale(): + fig = plt.figure() + ax1, ax2 = fig.subplots(2, 1, sharex='all') + ax1.plot([18000, 18250, 18500, 18750], [2, 3, 2, 3]) + ax2.axhspan(-5, 5) + xlim = ax1.get_xlim() + assert np.allclose(xlim, [18000, 18800]) + + +def test_sharing_does_not_link_positions(): + fig = plt.figure() + ax0 = fig.add_subplot(221) + ax1 = fig.add_axes([.6, .6, .3, .3], sharex=ax0) + init_pos = ax1.get_position() + fig.subplots_adjust(left=0) + assert (ax1.get_position().get_points() == init_pos.get_points()).all() + + +@check_figures_equal(extensions=["pdf"]) +def test_2dcolor_plot(fig_test, fig_ref): + color = np.array([0.1, 0.2, 0.3]) + # plot with 1D-color: + axs = fig_test.subplots(5) + axs[0].plot([1, 2], [1, 2], c=color.reshape(-1)) + with pytest.warns(match="argument looks like a single numeric RGB"): + axs[1].scatter([1, 2], [1, 2], c=color.reshape(-1)) + axs[2].step([1, 2], [1, 2], c=color.reshape(-1)) + axs[3].hist(np.arange(10), color=color.reshape(-1)) + axs[4].bar(np.arange(10), np.arange(10), color=color.reshape(-1)) + # plot with 2D-color: + axs = fig_ref.subplots(5) + axs[0].plot([1, 2], [1, 2], c=color.reshape((1, -1))) + axs[1].scatter([1, 2], [1, 2], c=color.reshape((1, -1))) + axs[2].step([1, 2], [1, 2], c=color.reshape((1, -1))) + axs[3].hist(np.arange(10), color=color.reshape((1, -1))) + axs[4].bar(np.arange(10), np.arange(10), color=color.reshape((1, -1))) + + +@check_figures_equal(extensions=['png']) +def test_shared_axes_clear(fig_test, fig_ref): + x = np.arange(0.0, 2*np.pi, 0.01) + y = np.sin(x) + + axs = fig_ref.subplots(2, 2, sharex=True, sharey=True) + for ax in axs.flat: + ax.plot(x, y) + + axs = fig_test.subplots(2, 2, sharex=True, sharey=True) + for ax in axs.flat: + ax.clear() + ax.plot(x, y) + + +def test_shared_axes_retick(): + fig, axs = plt.subplots(2, 2, sharex='all', sharey='all') + + for ax in axs.flat: + ax.plot([0, 2], 'o-') + + axs[0, 0].set_xticks([-0.5, 0, 1, 1.5]) # should affect all axes xlims + for ax in axs.flat: + assert ax.get_xlim() == axs[0, 0].get_xlim() + + axs[0, 0].set_yticks([-0.5, 0, 2, 2.5]) # should affect all axes ylims + for ax in axs.flat: + assert ax.get_ylim() == axs[0, 0].get_ylim() + + +@pytest.mark.parametrize('ha', ['left', 'center', 'right']) +def test_ylabel_ha_with_position(ha): + fig = Figure() + ax = fig.subplots() + ax.set_ylabel("test", y=1, ha=ha) + ax.yaxis.set_label_position("right") + assert ax.yaxis.label.get_ha() == ha + + +def test_bar_label_location_vertical(): + ax = plt.gca() + xs, heights = [1, 2], [3, -4] + rects = ax.bar(xs, heights) + labels = ax.bar_label(rects) + assert labels[0].xy == (xs[0], heights[0]) + assert labels[0].get_horizontalalignment() == 'center' + assert labels[0].get_verticalalignment() == 'bottom' + assert labels[1].xy == (xs[1], heights[1]) + assert labels[1].get_horizontalalignment() == 'center' + assert labels[1].get_verticalalignment() == 'top' + + +def test_bar_label_location_vertical_yinverted(): + ax = plt.gca() + ax.invert_yaxis() + xs, heights = [1, 2], [3, -4] + rects = ax.bar(xs, heights) + labels = ax.bar_label(rects) + assert labels[0].xy == (xs[0], heights[0]) + assert labels[0].get_horizontalalignment() == 'center' + assert labels[0].get_verticalalignment() == 'top' + assert labels[1].xy == (xs[1], heights[1]) + assert labels[1].get_horizontalalignment() == 'center' + assert labels[1].get_verticalalignment() == 'bottom' + + +def test_bar_label_location_horizontal(): + ax = plt.gca() + ys, widths = [1, 2], [3, -4] + rects = ax.barh(ys, widths) + labels = ax.bar_label(rects) + assert labels[0].xy == (widths[0], ys[0]) + assert labels[0].get_horizontalalignment() == 'left' + assert labels[0].get_verticalalignment() == 'center' + assert labels[1].xy == (widths[1], ys[1]) + assert labels[1].get_horizontalalignment() == 'right' + assert labels[1].get_verticalalignment() == 'center' + + +def test_bar_label_location_horizontal_yinverted(): + ax = plt.gca() + ax.invert_yaxis() + ys, widths = [1, 2], [3, -4] + rects = ax.barh(ys, widths) + labels = ax.bar_label(rects) + assert labels[0].xy == (widths[0], ys[0]) + assert labels[0].get_horizontalalignment() == 'left' + assert labels[0].get_verticalalignment() == 'center' + assert labels[1].xy == (widths[1], ys[1]) + assert labels[1].get_horizontalalignment() == 'right' + assert labels[1].get_verticalalignment() == 'center' + + +def test_bar_label_location_horizontal_xinverted(): + ax = plt.gca() + ax.invert_xaxis() + ys, widths = [1, 2], [3, -4] + rects = ax.barh(ys, widths) + labels = ax.bar_label(rects) + assert labels[0].xy == (widths[0], ys[0]) + assert labels[0].get_horizontalalignment() == 'right' + assert labels[0].get_verticalalignment() == 'center' + assert labels[1].xy == (widths[1], ys[1]) + assert labels[1].get_horizontalalignment() == 'left' + assert labels[1].get_verticalalignment() == 'center' + + +def test_bar_label_location_horizontal_xyinverted(): + ax = plt.gca() + ax.invert_xaxis() + ax.invert_yaxis() + ys, widths = [1, 2], [3, -4] + rects = ax.barh(ys, widths) + labels = ax.bar_label(rects) + assert labels[0].xy == (widths[0], ys[0]) + assert labels[0].get_horizontalalignment() == 'right' + assert labels[0].get_verticalalignment() == 'center' + assert labels[1].xy == (widths[1], ys[1]) + assert labels[1].get_horizontalalignment() == 'left' + assert labels[1].get_verticalalignment() == 'center' + + +def test_bar_label_location_center(): + ax = plt.gca() + ys, widths = [1, 2], [3, -4] + rects = ax.barh(ys, widths) + labels = ax.bar_label(rects, label_type='center') + assert labels[0].xy == (0.5, 0.5) + assert labels[0].get_horizontalalignment() == 'center' + assert labels[0].get_verticalalignment() == 'center' + assert labels[1].xy == (0.5, 0.5) + assert labels[1].get_horizontalalignment() == 'center' + assert labels[1].get_verticalalignment() == 'center' + + +@image_comparison(['test_centered_bar_label_nonlinear.svg']) +def test_centered_bar_label_nonlinear(): + _, ax = plt.subplots() + bar_container = ax.barh(['c', 'b', 'a'], [1_000, 5_000, 7_000]) + ax.set_xscale('log') + ax.set_xlim(1, None) + ax.bar_label(bar_container, label_type='center') + ax.set_axis_off() + + +def test_centered_bar_label_label_beyond_limits(): + fig, ax = plt.subplots() + + last = 0 + for label, value in zip(['a', 'b', 'c'], [10, 20, 50]): + bar_container = ax.barh('col', value, label=label, left=last) + ax.bar_label(bar_container, label_type='center') + last += value + ax.set_xlim(None, 20) + + fig.draw_without_rendering() + + +def test_bar_label_location_errorbars(): + ax = plt.gca() + xs, heights = [1, 2], [3, -4] + rects = ax.bar(xs, heights, yerr=1) + labels = ax.bar_label(rects) + assert labels[0].xy == (xs[0], heights[0] + 1) + assert labels[0].get_horizontalalignment() == 'center' + assert labels[0].get_verticalalignment() == 'bottom' + assert labels[1].xy == (xs[1], heights[1] - 1) + assert labels[1].get_horizontalalignment() == 'center' + assert labels[1].get_verticalalignment() == 'top' + + +@pytest.mark.parametrize('fmt', [ + '%.2f', '{:.2f}', '{:.2f}'.format +]) +def test_bar_label_fmt(fmt): + ax = plt.gca() + rects = ax.bar([1, 2], [3, -4]) + labels = ax.bar_label(rects, fmt=fmt) + assert labels[0].get_text() == '3.00' + assert labels[1].get_text() == '-4.00' + + +def test_bar_label_fmt_error(): + ax = plt.gca() + rects = ax.bar([1, 2], [3, -4]) + with pytest.raises(TypeError, match='str or callable'): + _ = ax.bar_label(rects, fmt=10) + + +def test_bar_label_labels(): + ax = plt.gca() + rects = ax.bar([1, 2], [3, -4]) + labels = ax.bar_label(rects, labels=['A', 'B']) + assert labels[0].get_text() == 'A' + assert labels[1].get_text() == 'B' + + +def test_bar_label_nan_ydata(): + ax = plt.gca() + bars = ax.bar([2, 3], [np.nan, 1]) + labels = ax.bar_label(bars) + assert [l.get_text() for l in labels] == ['', '1'] + assert labels[0].xy == (2, 0) + assert labels[0].get_verticalalignment() == 'bottom' + + +def test_bar_label_nan_ydata_inverted(): + ax = plt.gca() + ax.yaxis_inverted() + bars = ax.bar([2, 3], [np.nan, 1]) + labels = ax.bar_label(bars) + assert [l.get_text() for l in labels] == ['', '1'] + assert labels[0].xy == (2, 0) + assert labels[0].get_verticalalignment() == 'bottom' + + +def test_nan_barlabels(): + fig, ax = plt.subplots() + bars = ax.bar([1, 2, 3], [np.nan, 1, 2], yerr=[0.2, 0.4, 0.6]) + labels = ax.bar_label(bars) + assert [l.get_text() for l in labels] == ['', '1', '2'] + assert np.allclose(ax.get_ylim(), (0.0, 3.0)) + + fig, ax = plt.subplots() + bars = ax.bar([1, 2, 3], [0, 1, 2], yerr=[0.2, np.nan, 0.6]) + labels = ax.bar_label(bars) + assert [l.get_text() for l in labels] == ['0', '1', '2'] + assert np.allclose(ax.get_ylim(), (-0.5, 3.0)) + + fig, ax = plt.subplots() + bars = ax.bar([1, 2, 3], [np.nan, 1, 2], yerr=[np.nan, np.nan, 0.6]) + labels = ax.bar_label(bars) + assert [l.get_text() for l in labels] == ['', '1', '2'] + assert np.allclose(ax.get_ylim(), (0.0, 3.0)) + + +def test_patch_bounds(): # PR 19078 + fig, ax = plt.subplots() + ax.add_patch(mpatches.Wedge((0, -1), 1.05, 60, 120, width=0.1)) + bot = 1.9*np.sin(15*np.pi/180)**2 + np.testing.assert_array_almost_equal_nulp( + np.array((-0.525, -(bot+0.05), 1.05, bot+0.1)), ax.dataLim.bounds, 16) + + +@mpl.style.context('default') +def test_warn_ignored_scatter_kwargs(): + with pytest.warns(UserWarning, + match=r"You passed a edgecolor/edgecolors"): + plt.scatter([0], [0], marker="+", s=500, facecolor="r", edgecolor="b") + + +def test_artist_sublists(): + fig, ax = plt.subplots() + lines = [ax.plot(np.arange(i, i + 5))[0] for i in range(6)] + col = ax.scatter(np.arange(5), np.arange(5)) + im = ax.imshow(np.zeros((5, 5))) + patch = ax.add_patch(mpatches.Rectangle((0, 0), 5, 5)) + text = ax.text(0, 0, 'foo') + + # Get items, which should not be mixed. + assert list(ax.collections) == [col] + assert list(ax.images) == [im] + assert list(ax.lines) == lines + assert list(ax.patches) == [patch] + assert not ax.tables + assert list(ax.texts) == [text] + + # Get items should work like lists/tuple. + assert ax.lines[0] is lines[0] + assert ax.lines[-1] is lines[-1] + with pytest.raises(IndexError, match='out of range'): + ax.lines[len(lines) + 1] + + # Adding to other lists should produce a regular list. + assert ax.lines + [1, 2, 3] == [*lines, 1, 2, 3] + assert [1, 2, 3] + ax.lines == [1, 2, 3, *lines] + + # Adding to other tuples should produce a regular tuples. + assert ax.lines + (1, 2, 3) == (*lines, 1, 2, 3) + assert (1, 2, 3) + ax.lines == (1, 2, 3, *lines) + + # Lists should be empty after removing items. + col.remove() + assert not ax.collections + im.remove() + assert not ax.images + patch.remove() + assert not ax.patches + assert not ax.tables + text.remove() + assert not ax.texts + + for ln in ax.lines: + ln.remove() + assert len(ax.lines) == 0 + + +def test_empty_line_plots(): + # Incompatible nr columns, plot "nothing" + x = np.ones(10) + y = np.ones((10, 0)) + _, ax = plt.subplots() + line = ax.plot(x, y) + assert len(line) == 0 + + # Ensure plot([],[]) creates line + _, ax = plt.subplots() + line = ax.plot([], []) + assert len(line) == 1 + + +@pytest.mark.parametrize('fmt, match', ( + ("f", r"'f' is not a valid format string \(unrecognized character 'f'\)"), + ("o+", r"'o\+' is not a valid format string \(two marker symbols\)"), + (":-", r"':-' is not a valid format string \(two linestyle symbols\)"), + ("rk", r"'rk' is not a valid format string \(two color symbols\)"), + (":o-r", r"':o-r' is not a valid format string \(two linestyle symbols\)"), + ("C", r"'C' is not a valid format string \('C' must be followed by a number\)"), + (".C", r"'.C' is not a valid format string \('C' must be followed by a number\)"), +)) +@pytest.mark.parametrize("data", [None, {"string": range(3)}]) +def test_plot_format_errors(fmt, match, data): + fig, ax = plt.subplots() + if data is not None: + match = match.replace("not", "neither a data key nor") + with pytest.raises(ValueError, match=r"\A" + match + r"\Z"): + ax.plot("string", fmt, data=data) + + +def test_plot_format(): + fig, ax = plt.subplots() + line = ax.plot([1, 2, 3], '1.0') + assert line[0].get_color() == (1.0, 1.0, 1.0, 1.0) + assert line[0].get_marker() == 'None' + fig, ax = plt.subplots() + line = ax.plot([1, 2, 3], '1') + assert line[0].get_marker() == '1' + fig, ax = plt.subplots() + line = ax.plot([1, 2], [1, 2], '1.0', "1") + fig.canvas.draw() + assert line[0].get_color() == (1.0, 1.0, 1.0, 1.0) + assert ax.get_yticklabels()[0].get_text() == '1' + fig, ax = plt.subplots() + line = ax.plot([1, 2], [1, 2], '1', "1.0") + fig.canvas.draw() + assert line[0].get_marker() == '1' + assert ax.get_yticklabels()[0].get_text() == '1.0' + fig, ax = plt.subplots() + line = ax.plot([1, 2, 3], 'k3') + assert line[0].get_marker() == '3' + assert line[0].get_color() == 'k' + fig, ax = plt.subplots() + line = ax.plot([1, 2, 3], '.C12:') + assert line[0].get_marker() == '.' + assert line[0].get_color() == mcolors.to_rgba('C12') + assert line[0].get_linestyle() == ':' + + +def test_automatic_legend(): + fig, ax = plt.subplots() + ax.plot("a", "b", data={"d": 2}) + leg = ax.legend() + fig.canvas.draw() + assert leg.get_texts()[0].get_text() == 'a' + assert ax.get_yticklabels()[0].get_text() == 'a' + + fig, ax = plt.subplots() + ax.plot("a", "b", "c", data={"d": 2}) + leg = ax.legend() + fig.canvas.draw() + assert leg.get_texts()[0].get_text() == 'b' + assert ax.get_xticklabels()[0].get_text() == 'a' + assert ax.get_yticklabels()[0].get_text() == 'b' + + +def test_plot_errors(): + with pytest.raises(TypeError, match=r"plot\(\) got an unexpected keyword"): + plt.plot([1, 2, 3], x=1) + with pytest.raises(ValueError, match=r"plot\(\) with multiple groups"): + plt.plot([1, 2, 3], [1, 2, 3], [2, 3, 4], [2, 3, 4], label=['1', '2']) + with pytest.raises(ValueError, match="x and y must have same first"): + plt.plot([1, 2, 3], [1]) + with pytest.raises(ValueError, match="x and y can be no greater than"): + plt.plot(np.ones((2, 2, 2))) + with pytest.raises(ValueError, match="Using arbitrary long args with"): + plt.plot("a", "b", "c", "d", data={"a": 2}) + + +def test_clim(): + ax = plt.figure().add_subplot() + for plot_method in [ + partial(ax.scatter, range(3), range(3), c=range(3)), + partial(ax.imshow, [[0, 1], [2, 3]]), + partial(ax.pcolor, [[0, 1], [2, 3]]), + partial(ax.pcolormesh, [[0, 1], [2, 3]]), + partial(ax.pcolorfast, [[0, 1], [2, 3]]), + ]: + clim = (7, 8) + norm = plot_method(clim=clim).norm + assert (norm.vmin, norm.vmax) == clim + + +def test_bezier_autoscale(): + # Check that bezier curves autoscale to their curves, and not their + # control points + verts = [[-1, 0], + [0, -1], + [1, 0], + [1, 0]] + codes = [mpath.Path.MOVETO, + mpath.Path.CURVE3, + mpath.Path.CURVE3, + mpath.Path.CLOSEPOLY] + p = mpath.Path(verts, codes) + + fig, ax = plt.subplots() + ax.add_patch(mpatches.PathPatch(p)) + ax.autoscale() + # Bottom ylim should be at the edge of the curve (-0.5), and not include + # the control point (at -1) + assert ax.get_ylim()[0] == -0.5 + + +def test_small_autoscale(): + # Check that paths with small values autoscale correctly #24097. + verts = np.array([ + [-5.45, 0.00], [-5.45, 0.00], [-5.29, 0.00], [-5.29, 0.00], + [-5.13, 0.00], [-5.13, 0.00], [-4.97, 0.00], [-4.97, 0.00], + [-4.81, 0.00], [-4.81, 0.00], [-4.65, 0.00], [-4.65, 0.00], + [-4.49, 0.00], [-4.49, 0.00], [-4.33, 0.00], [-4.33, 0.00], + [-4.17, 0.00], [-4.17, 0.00], [-4.01, 0.00], [-4.01, 0.00], + [-3.85, 0.00], [-3.85, 0.00], [-3.69, 0.00], [-3.69, 0.00], + [-3.53, 0.00], [-3.53, 0.00], [-3.37, 0.00], [-3.37, 0.00], + [-3.21, 0.00], [-3.21, 0.01], [-3.05, 0.01], [-3.05, 0.01], + [-2.89, 0.01], [-2.89, 0.01], [-2.73, 0.01], [-2.73, 0.02], + [-2.57, 0.02], [-2.57, 0.04], [-2.41, 0.04], [-2.41, 0.04], + [-2.25, 0.04], [-2.25, 0.06], [-2.09, 0.06], [-2.09, 0.08], + [-1.93, 0.08], [-1.93, 0.10], [-1.77, 0.10], [-1.77, 0.12], + [-1.61, 0.12], [-1.61, 0.14], [-1.45, 0.14], [-1.45, 0.17], + [-1.30, 0.17], [-1.30, 0.19], [-1.14, 0.19], [-1.14, 0.22], + [-0.98, 0.22], [-0.98, 0.25], [-0.82, 0.25], [-0.82, 0.27], + [-0.66, 0.27], [-0.66, 0.29], [-0.50, 0.29], [-0.50, 0.30], + [-0.34, 0.30], [-0.34, 0.32], [-0.18, 0.32], [-0.18, 0.33], + [-0.02, 0.33], [-0.02, 0.32], [0.13, 0.32], [0.13, 0.33], [0.29, 0.33], + [0.29, 0.31], [0.45, 0.31], [0.45, 0.30], [0.61, 0.30], [0.61, 0.28], + [0.77, 0.28], [0.77, 0.25], [0.93, 0.25], [0.93, 0.22], [1.09, 0.22], + [1.09, 0.19], [1.25, 0.19], [1.25, 0.17], [1.41, 0.17], [1.41, 0.15], + [1.57, 0.15], [1.57, 0.12], [1.73, 0.12], [1.73, 0.10], [1.89, 0.10], + [1.89, 0.08], [2.05, 0.08], [2.05, 0.07], [2.21, 0.07], [2.21, 0.05], + [2.37, 0.05], [2.37, 0.04], [2.53, 0.04], [2.53, 0.02], [2.69, 0.02], + [2.69, 0.02], [2.85, 0.02], [2.85, 0.01], [3.01, 0.01], [3.01, 0.01], + [3.17, 0.01], [3.17, 0.00], [3.33, 0.00], [3.33, 0.00], [3.49, 0.00], + [3.49, 0.00], [3.65, 0.00], [3.65, 0.00], [3.81, 0.00], [3.81, 0.00], + [3.97, 0.00], [3.97, 0.00], [4.13, 0.00], [4.13, 0.00], [4.29, 0.00], + [4.29, 0.00], [4.45, 0.00], [4.45, 0.00], [4.61, 0.00], [4.61, 0.00], + [4.77, 0.00], [4.77, 0.00], [4.93, 0.00], [4.93, 0.00], + ]) + + minx = np.min(verts[:, 0]) + miny = np.min(verts[:, 1]) + maxx = np.max(verts[:, 0]) + maxy = np.max(verts[:, 1]) + + p = mpath.Path(verts) + + fig, ax = plt.subplots() + ax.add_patch(mpatches.PathPatch(p)) + ax.autoscale() + + assert ax.get_xlim()[0] <= minx + assert ax.get_xlim()[1] >= maxx + assert ax.get_ylim()[0] <= miny + assert ax.get_ylim()[1] >= maxy + + +def test_get_xticklabel(): + fig, ax = plt.subplots() + ax.plot(np.arange(10)) + for ind in range(10): + assert ax.get_xticklabels()[ind].get_text() == f'{ind}' + assert ax.get_yticklabels()[ind].get_text() == f'{ind}' + + +def test_bar_leading_nan(): + + barx = np.arange(3, dtype=float) + barheights = np.array([0.5, 1.5, 2.0]) + barstarts = np.array([0.77]*3) + + barx[0] = np.nan + + fig, ax = plt.subplots() + + bars = ax.bar(barx, barheights, bottom=barstarts) + + hbars = ax.barh(barx, barheights, left=barstarts) + + for bar_set in (bars, hbars): + # the first bar should have a nan in the location + nanful, *rest = bar_set + assert (~np.isfinite(nanful.xy)).any() + assert np.isfinite(nanful.get_width()) + for b in rest: + assert np.isfinite(b.xy).all() + assert np.isfinite(b.get_width()) + + +@check_figures_equal(extensions=["png"]) +def test_bar_all_nan(fig_test, fig_ref): + mpl.style.use("mpl20") + ax_test = fig_test.subplots() + ax_ref = fig_ref.subplots() + + ax_test.bar([np.nan], [np.nan]) + ax_test.bar([1], [1]) + + ax_ref.bar([1], [1]).remove() + ax_ref.bar([1], [1]) + + +@image_comparison(["extent_units.png"], style="mpl20") +def test_extent_units(): + _, axs = plt.subplots(2, 2) + date_first = np.datetime64('2020-01-01', 'D') + date_last = np.datetime64('2020-01-11', 'D') + arr = [[i+j for i in range(10)] for j in range(10)] + + axs[0, 0].set_title('Date extents on y axis') + im = axs[0, 0].imshow(arr, origin='lower', + extent=[1, 11, date_first, date_last], + cmap=mpl.colormaps["plasma"]) + + axs[0, 1].set_title('Date extents on x axis (Day of Jan 2020)') + im = axs[0, 1].imshow(arr, origin='lower', + extent=[date_first, date_last, 1, 11], + cmap=mpl.colormaps["plasma"]) + axs[0, 1].xaxis.set_major_formatter(mdates.DateFormatter('%d')) + + im = axs[1, 0].imshow(arr, origin='lower', + extent=[date_first, date_last, + date_first, date_last], + cmap=mpl.colormaps["plasma"]) + axs[1, 0].xaxis.set_major_formatter(mdates.DateFormatter('%d')) + axs[1, 0].set(xlabel='Day of Jan 2020') + + im = axs[1, 1].imshow(arr, origin='lower', + cmap=mpl.colormaps["plasma"]) + im.set_extent([date_last, date_first, date_last, date_first]) + axs[1, 1].xaxis.set_major_formatter(mdates.DateFormatter('%d')) + axs[1, 1].set(xlabel='Day of Jan 2020') + + with pytest.raises(TypeError, match=r"set_extent\(\) got an unexpected"): + im.set_extent([2, 12, date_first, date_last], clip=False) + + +def test_cla_clears_children_axes_and_fig(): + fig, ax = plt.subplots() + lines = ax.plot([], [], [], []) + img = ax.imshow([[1]]) + for art in lines + [img]: + assert art.axes is ax + assert art.get_figure() is fig + ax.clear() + for art in lines + [img]: + assert art.axes is None + assert art.get_figure() is None + + +def test_child_axes_removal(): + fig, ax = plt.subplots() + marginal = ax.inset_axes([1, 0, .1, 1], sharey=ax) + marginal_twin = marginal.twinx() + marginal.remove() + ax.set(xlim=(-1, 1), ylim=(10, 20)) + + +def test_scatter_color_repr_error(): + + def get_next_color(): # pragma: no cover + return 'blue' # currently unused + msg = ( + r"'c' argument must be a color, a sequence of colors" + r", or a sequence of numbers, not 'red\\n'" + ) + with pytest.raises(ValueError, match=msg): + c = 'red\n' + mpl.axes.Axes._parse_scatter_color_args( + c, None, kwargs={}, xsize=2, get_next_color_func=get_next_color) + + +def test_zorder_and_explicit_rasterization(): + fig, ax = plt.subplots() + ax.set_rasterization_zorder(5) + ln, = ax.plot(range(5), rasterized=True, zorder=1) + with io.BytesIO() as b: + fig.savefig(b, format='pdf') + + +@image_comparison(["preset_clip_paths.png"], remove_text=True, style="mpl20", + tol=0 if platform.machine() == 'x86_64' else 0.027) +def test_preset_clip_paths(): + fig, ax = plt.subplots() + + poly = mpl.patches.Polygon( + [[1, 0], [0, 1], [-1, 0], [0, -1]], facecolor="#ddffdd", + edgecolor="#00ff00", linewidth=2, alpha=0.5) + + ax.add_patch(poly) + + line = mpl.lines.Line2D((-1, 1), (0.5, 0.5), clip_on=True, clip_path=poly) + line.set_path_effects([patheffects.withTickedStroke()]) + ax.add_artist(line) + + line = mpl.lines.Line2D((-1, 1), (-0.5, -0.5), color='r', clip_on=True, + clip_path=poly) + ax.add_artist(line) + + poly2 = mpl.patches.Polygon( + [[-1, 1], [0, 1], [0, -0.25]], facecolor="#beefc0", alpha=0.3, + edgecolor="#faded0", linewidth=2, clip_on=True, clip_path=poly) + ax.add_artist(poly2) + + # When text clipping works, the "Annotation" text should be clipped + ax.annotate('Annotation', (-0.75, -0.75), xytext=(0.1, 0.75), + arrowprops={'color': 'k'}, clip_on=True, clip_path=poly) + + poly3 = mpl.patches.Polygon( + [[0, 0], [0, 0.5], [0.5, 0.5], [0.5, 0]], facecolor="g", edgecolor="y", + linewidth=2, alpha=0.3, clip_on=True, clip_path=poly) + + fig.add_artist(poly3, clip=True) + + ax.set_xlim(-1, 1) + ax.set_ylim(-1, 1) + + +@mpl.style.context('default') +def test_rc_axes_label_formatting(): + mpl.rcParams['axes.labelcolor'] = 'red' + mpl.rcParams['axes.labelsize'] = 20 + mpl.rcParams['axes.labelweight'] = 'bold' + + ax = plt.axes() + assert ax.xaxis.label.get_color() == 'red' + assert ax.xaxis.label.get_fontsize() == 20 + assert ax.xaxis.label.get_fontweight() == 'bold' + + +@check_figures_equal(extensions=["png"]) +def test_ecdf(fig_test, fig_ref): + data = np.array([0, -np.inf, -np.inf, np.inf, 1, 1, 2]) + weights = range(len(data)) + axs_test = fig_test.subplots(1, 2) + for ax, orientation in zip(axs_test, ["vertical", "horizontal"]): + l0 = ax.ecdf(data, orientation=orientation) + l1 = ax.ecdf("d", "w", data={"d": np.ma.array(data), "w": weights}, + orientation=orientation, + complementary=True, compress=True, ls=":") + assert len(l0.get_xdata()) == (~np.isnan(data)).sum() + 1 + assert len(l1.get_xdata()) == len({*data[~np.isnan(data)]}) + 1 + axs_ref = fig_ref.subplots(1, 2) + axs_ref[0].plot([-np.inf, -np.inf, -np.inf, 0, 1, 1, 2, np.inf], + np.arange(8) / 7, ds="steps-post") + axs_ref[0].plot([-np.inf, 0, 1, 2, np.inf, np.inf], + np.array([21, 20, 18, 14, 3, 0]) / 21, + ds="steps-pre", ls=":") + axs_ref[1].plot(np.arange(8) / 7, + [-np.inf, -np.inf, -np.inf, 0, 1, 1, 2, np.inf], + ds="steps-pre") + axs_ref[1].plot(np.array([21, 20, 18, 14, 3, 0]) / 21, + [-np.inf, 0, 1, 2, np.inf, np.inf], + ds="steps-post", ls=":") + + +def test_ecdf_invalid(): + with pytest.raises(ValueError): + plt.ecdf([1, np.nan]) + with pytest.raises(ValueError): + plt.ecdf(np.ma.array([1, 2], mask=[True, False])) + + +def test_fill_between_axes_limits(): + fig, ax = plt.subplots() + x = np.arange(0, 4 * np.pi, 0.01) + y = 0.1*np.sin(x) + threshold = 0.075 + ax.plot(x, y, color='black') + + original_lims = (ax.get_xlim(), ax.get_ylim()) + + ax.axhline(threshold, color='green', lw=2, alpha=0.7) + ax.fill_between(x, 0, 1, where=y > threshold, + color='green', alpha=0.5, transform=ax.get_xaxis_transform()) + + assert (ax.get_xlim(), ax.get_ylim()) == original_lims + + +def test_tick_param_labelfont(): + fig, ax = plt.subplots() + ax.plot([1, 2, 3, 4], [1, 2, 3, 4]) + ax.set_xlabel('X label in Impact font', fontname='Impact') + ax.set_ylabel('Y label in xkcd script', fontname='xkcd script') + ax.tick_params(color='r', labelfontfamily='monospace') + plt.title('Title in sans-serif') + for text in ax.get_xticklabels(): + assert text.get_fontfamily()[0] == 'monospace' + + +def test_set_secondary_axis_color(): + fig, ax = plt.subplots() + sax = ax.secondary_xaxis("top", color="red") + assert mcolors.same_color(sax.spines["bottom"].get_edgecolor(), "red") + assert mcolors.same_color(sax.spines["top"].get_edgecolor(), "red") + assert mcolors.same_color(sax.xaxis.get_tick_params()["color"], "red") + assert mcolors.same_color(sax.xaxis.get_tick_params()["labelcolor"], "red") + assert mcolors.same_color(sax.xaxis.label.get_color(), "red") + + +def test_xylim_changed_shared(): + fig, axs = plt.subplots(2, sharex=True, sharey=True) + events = [] + axs[1].callbacks.connect("xlim_changed", events.append) + axs[1].callbacks.connect("ylim_changed", events.append) + axs[0].set(xlim=[1, 3], ylim=[2, 4]) + assert events == [axs[1], axs[1]] + + +@image_comparison(["axhvlinespan_interpolation.png"], style="default") +def test_axhvlinespan_interpolation(): + ax = plt.figure().add_subplot(projection="polar") + ax.set_axis_off() + ax.axvline(.1, c="C0") + ax.axvspan(.2, .3, fc="C1") + ax.axvspan(.4, .5, .1, .2, fc="C2") + ax.axhline(1, c="C0", alpha=.5) + ax.axhspan(.8, .9, fc="C1", alpha=.5) + ax.axhspan(.6, .7, .8, .9, fc="C2", alpha=.5) + + +@check_figures_equal(extensions=["png"]) +@pytest.mark.parametrize("which", ("x", "y")) +def test_axes_clear_behavior(fig_ref, fig_test, which): + """Test that the given tick params are not reset by ax.clear().""" + ax_test = fig_test.subplots() + ax_ref = fig_ref.subplots() + # the following tick params values are chosen to each create a visual difference + # from their defaults + target = { + "direction": "in", + "length": 10, + "width": 10, + "color": "xkcd:wine red", + "pad": 0, + "labelfontfamily": "serif", + "zorder": 7, + "labelrotation": 45, + "labelcolor": "xkcd:shocking pink", + # this overrides color + labelcolor, skip + # colors: , + "grid_color": "xkcd:fluorescent green", + "grid_alpha": 0.5, + "grid_linewidth": 3, + "grid_linestyle": ":", + "bottom": False, + "top": True, + "left": False, + "right": True, + "labelbottom": True, + "labeltop": True, + "labelleft": True, + "labelright": True, + } + + ax_ref.tick_params(axis=which, **target) + + ax_test.tick_params(axis=which, **target) + ax_test.clear() + + ax_ref.grid(True) + ax_test.grid(True) + + +@pytest.mark.skipif( + sys.version_info[:3] == (3, 13, 0) and sys.version_info.releaselevel != "final", + reason="https://github.com/python/cpython/issues/124538", +) +def test_axes_clear_reference_cycle(): + def assert_not_in_reference_cycle(start): + # Breadth first search. Return True if we encounter the starting node + to_visit = deque([start]) + explored = set() + while len(to_visit) > 0: + parent = to_visit.popleft() + for child in gc.get_referents(parent): + if id(child) in explored: + continue + assert child is not start + explored.add(id(child)) + to_visit.append(child) + + fig = Figure() + ax = fig.add_subplot() + points = np.random.rand(1000) + ax.plot(points, points) + ax.scatter(points, points) + ax_children = ax.get_children() + fig.clear() # This should break the reference cycle + + # Care most about the objects that scale with number of points + big_artists = [ + a for a in ax_children + if isinstance(a, (Line2D, PathCollection)) + ] + assert len(big_artists) > 0 + for big_artist in big_artists: + assert_not_in_reference_cycle(big_artist) + assert len(ax_children) > 0 + for child in ax_children: + # Make sure this doesn't raise because the child is already removed. + try: + child.remove() + except NotImplementedError: + pass # not implemented is expected for some artists + + +def test_boxplot_tick_labels(): + # Test the renamed `tick_labels` parameter. + # Test for deprecation of old name `labels`. + np.random.seed(19680801) + data = np.random.random((10, 3)) + + fig, axs = plt.subplots(nrows=1, ncols=2, sharey=True) + # Should get deprecation warning for `labels` + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='has been renamed \'tick_labels\''): + axs[0].boxplot(data, labels=['A', 'B', 'C']) + assert [l.get_text() for l in axs[0].get_xticklabels()] == ['A', 'B', 'C'] + + # Test the new tick_labels parameter + axs[1].boxplot(data, tick_labels=['A', 'B', 'C']) + assert [l.get_text() for l in axs[1].get_xticklabels()] == ['A', 'B', 'C'] + + +@needs_usetex +@check_figures_equal(extensions=['png']) +def test_latex_pie_percent(fig_test, fig_ref): + + data = [20, 10, 70] + + ax = fig_test.subplots() + ax.pie(data, autopct="%1.0f%%", textprops={'usetex': True}) + + ax1 = fig_ref.subplots() + ax1.pie(data, autopct=r"%1.0f\%%", textprops={'usetex': True}) + + +@check_figures_equal(extensions=['png']) +def test_violinplot_orientation(fig_test, fig_ref): + # Test the `orientation : {'vertical', 'horizontal'}` + # parameter and deprecation of `vert: bool`. + fig, axs = plt.subplots(nrows=1, ncols=3) + np.random.seed(19680801) + all_data = [np.random.normal(0, std, 100) for std in range(6, 10)] + + axs[0].violinplot(all_data) # Default vertical plot. + # xticks and yticks should be at their default position. + assert all(axs[0].get_xticks() == np.array( + [0.5, 1., 1.5, 2., 2.5, 3., 3.5, 4., 4.5])) + assert all(axs[0].get_yticks() == np.array( + [-30., -20., -10., 0., 10., 20., 30.])) + + # Horizontal plot using new `orientation` keyword. + axs[1].violinplot(all_data, orientation='horizontal') + # xticks and yticks should be swapped. + assert all(axs[1].get_xticks() == np.array( + [-30., -20., -10., 0., 10., 20., 30.])) + assert all(axs[1].get_yticks() == np.array( + [0.5, 1., 1.5, 2., 2.5, 3., 3.5, 4., 4.5])) + + plt.close() + + # Compare images between a figure that + # uses vert and one that uses orientation. + ax_ref = fig_ref.subplots() + + with pytest.warns(PendingDeprecationWarning, match='vert: bool'): + ax_ref.violinplot(all_data, vert=False) + + ax_test = fig_test.subplots() + ax_test.violinplot(all_data, orientation='horizontal') + + +@check_figures_equal(extensions=['png']) +def test_boxplot_orientation(fig_test, fig_ref): + # Test the `orientation : {'vertical', 'horizontal'}` + # parameter and deprecation of `vert: bool`. + fig, axs = plt.subplots(nrows=1, ncols=2) + np.random.seed(19680801) + all_data = [np.random.normal(0, std, 100) for std in range(6, 10)] + + axs[0].boxplot(all_data) # Default vertical plot. + # xticks and yticks should be at their default position. + assert all(axs[0].get_xticks() == np.array( + [1, 2, 3, 4])) + assert all(axs[0].get_yticks() == np.array( + [-30., -20., -10., 0., 10., 20., 30.])) + + # Horizontal plot using new `orientation` keyword. + axs[1].boxplot(all_data, orientation='horizontal') + # xticks and yticks should be swapped. + assert all(axs[1].get_xticks() == np.array( + [-30., -20., -10., 0., 10., 20., 30.])) + assert all(axs[1].get_yticks() == np.array( + [1, 2, 3, 4])) + + plt.close() + + # Deprecation of `vert: bool` keyword and + # 'boxplot.vertical' rcparam. + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='was deprecated in Matplotlib 3.10'): + # Compare images between a figure that + # uses vert and one that uses orientation. + with mpl.rc_context({'boxplot.vertical': False}): + ax_ref = fig_ref.subplots() + ax_ref.boxplot(all_data) + + ax_test = fig_test.subplots() + ax_test.boxplot(all_data, orientation='horizontal') + + +@image_comparison(["use_colorizer_keyword.png"], + tol=0 if platform.machine() == 'x86_64' else 0.05) +def test_use_colorizer_keyword(): + # test using the colorizer keyword + np.random.seed(0) + rand_x = np.random.random(100) + rand_y = np.random.random(100) + c = np.arange(25, dtype='float32').reshape((5, 5)) + + fig, axes = plt.subplots(3, 4) + norm = mpl.colors.Normalize(4, 20) + cl = mpl.colorizer.Colorizer(norm=norm, cmap='RdBu') + + axes[0, 0].scatter(c, c, c=c, colorizer=cl) + axes[0, 1].hexbin(rand_x, rand_y, colorizer=cl, gridsize=(2, 2)) + axes[0, 2].imshow(c, colorizer=cl) + axes[0, 3].pcolor(c, colorizer=cl) + axes[1, 0].pcolormesh(c, colorizer=cl) + axes[1, 1].pcolorfast(c, colorizer=cl) # style = image + axes[1, 2].pcolorfast((0, 1, 2, 3, 4, 5), (0, 1, 2, 3, 5, 6), c, + colorizer=cl) # style = pcolorimage + axes[1, 3].pcolorfast(c.T, c, c[:4, :4], colorizer=cl) # style = quadmesh + axes[2, 0].contour(c, colorizer=cl) + axes[2, 1].contourf(c, colorizer=cl) + axes[2, 2].tricontour(c.T.ravel(), c.ravel(), c.ravel(), colorizer=cl) + axes[2, 3].tricontourf(c.T.ravel(), c.ravel(), c.ravel(), colorizer=cl) + + fig.figimage(np.repeat(np.repeat(c, 15, axis=0), 15, axis=1), colorizer=cl) + remove_ticks_and_titles(fig) + + +def test_wrong_use_colorizer(): + # test using the colorizer keyword and norm or cmap + np.random.seed(0) + rand_x = np.random.random(100) + rand_y = np.random.random(100) + c = np.arange(25, dtype='float32').reshape((5, 5)) + + fig, axes = plt.subplots(3, 4) + norm = mpl.colors.Normalize(4, 20) + cl = mpl.colorizer.Colorizer(norm=norm, cmap='RdBu') + + match_str = "The `colorizer` keyword cannot be used simultaneously" + kwrds = [{'vmin': 0}, {'vmax': 0}, {'norm': 'log'}, {'cmap': 'viridis'}] + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[0, 0].scatter(c, c, c=c, colorizer=cl, **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[0, 0].scatter(c, c, c=c, colorizer=cl, **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[0, 1].hexbin(rand_x, rand_y, colorizer=cl, gridsize=(2, 2), **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[0, 2].imshow(c, colorizer=cl, **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[0, 3].pcolor(c, colorizer=cl, **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[1, 0].pcolormesh(c, colorizer=cl, **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[1, 1].pcolorfast(c, colorizer=cl, **kwrd) # style = image + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[1, 2].pcolorfast((0, 1, 2, 3, 4, 5), (0, 1, 2, 3, 5, 6), c, + colorizer=cl, **kwrd) # style = pcolorimage + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[1, 3].pcolorfast(c.T, c, c[:4, :4], colorizer=cl, **kwrd) # quadmesh + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[2, 0].contour(c, colorizer=cl, **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[2, 1].contourf(c, colorizer=cl, **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[2, 2].tricontour(c.T.ravel(), c.ravel(), c.ravel(), colorizer=cl, + **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + axes[2, 3].tricontourf(c.T.ravel(), c.ravel(), c.ravel(), colorizer=cl, + **kwrd) + for kwrd in kwrds: + with pytest.raises(ValueError, match=match_str): + fig.figimage(c, colorizer=cl, **kwrd) + + +def test_bar_color_precedence(): + # Test the precedence of 'color' and 'facecolor' in bar plots + fig, ax = plt.subplots() + + # case 1: no color specified + bars = ax.bar([1, 2, 3], [4, 5, 6]) + for bar in bars: + assert mcolors.same_color(bar.get_facecolor(), 'blue') + + # case 2: Only 'color' + bars = ax.bar([11, 12, 13], [4, 5, 6], color='red') + for bar in bars: + assert mcolors.same_color(bar.get_facecolor(), 'red') + + # case 3: Only 'facecolor' + bars = ax.bar([21, 22, 23], [4, 5, 6], facecolor='yellow') + for bar in bars: + assert mcolors.same_color(bar.get_facecolor(), 'yellow') + + # case 4: 'facecolor' and 'color' + bars = ax.bar([31, 32, 33], [4, 5, 6], color='red', facecolor='green') + for bar in bars: + assert mcolors.same_color(bar.get_facecolor(), 'green') + + +@check_figures_equal(extensions=['png']) +def test_axes_set_position_external_bbox_unchanged(fig_test, fig_ref): + # From #29410: Modifying Axes' position also alters the original Bbox + # object used for initialization + bbox = mtransforms.Bbox([[0.0, 0.0], [1.0, 1.0]]) + ax_test = fig_test.add_axes(bbox) + ax_test.set_position([0.25, 0.25, 0.5, 0.5]) + assert (bbox.x0, bbox.y0, bbox.width, bbox.height) == (0.0, 0.0, 1.0, 1.0) + ax_ref = fig_ref.add_axes([0.25, 0.25, 0.5, 0.5]) diff --git a/lib/python3.12/site-packages/matplotlib/tests/test_backend_inline.py b/lib/python3.12/site-packages/matplotlib/tests/test_backend_inline.py new file mode 100644 index 0000000000000000000000000000000000000000..6f0d67d51756b8f280522e8f2a9f5d6482f5c9ea --- /dev/null +++ b/lib/python3.12/site-packages/matplotlib/tests/test_backend_inline.py @@ -0,0 +1,46 @@ +import os +from pathlib import Path +from tempfile import TemporaryDirectory + +import pytest + +from matplotlib.testing import subprocess_run_for_testing + +nbformat = pytest.importorskip('nbformat') +pytest.importorskip('nbconvert') +pytest.importorskip('ipykernel') +pytest.importorskip('matplotlib_inline') + + +def test_ipynb(): + nb_path = Path(__file__).parent / 'test_inline_01.ipynb' + + with TemporaryDirectory() as tmpdir: + out_path = Path(tmpdir, "out.ipynb") + + subprocess_run_for_testing( + ["jupyter", "nbconvert", "--to", "notebook", + "--execute", "--ExecutePreprocessor.timeout=500", + "--output", str(out_path), str(nb_path)], + env={**os.environ, "IPYTHONDIR": tmpdir}, + check=True) + with out_path.open() as out: + nb = nbformat.read(out, nbformat.current_nbformat) + + errors = [output for cell in nb.cells for output in cell.get("outputs", []) + if output.output_type == "error"] + assert not errors + + import IPython + if IPython.version_info[:2] >= (8, 24): + expected_backend = "inline" + else: + # This code can be removed when Python 3.12, the latest version supported by + # IPython < 8.24, reaches end-of-life in late 2028. + expected_backend = "module://matplotlib_inline.backend_inline" + backend_outputs = nb.cells[2]["outputs"] + assert backend_outputs[0]["data"]["text/plain"] == f"'{expected_backend}'" + + image = nb.cells[1]["outputs"][1]["data"] + assert image["text/plain"] == "
" + 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='