Initial commit

This commit is contained in:
Untriex Programming
2021-08-31 22:06:02 +02:00
commit 9b6723e11e
5142 changed files with 1455625 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,262 @@
# Javascript template for HTMLWriter
JS_INCLUDE = """
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<script language="javascript">
function isInternetExplorer() {
ua = navigator.userAgent;
/* MSIE used to detect old browsers and Trident used to newer ones*/
return ua.indexOf("MSIE ") > -1 || ua.indexOf("Trident/") > -1;
}
/* Define the Animation class */
function Animation(frames, img_id, slider_id, interval, loop_select_id){
this.img_id = img_id;
this.slider_id = slider_id;
this.loop_select_id = loop_select_id;
this.interval = interval;
this.current_frame = 0;
this.direction = 0;
this.timer = null;
this.frames = new Array(frames.length);
for (var i=0; i<frames.length; i++)
{
this.frames[i] = new Image();
this.frames[i].src = frames[i];
}
var slider = document.getElementById(this.slider_id);
slider.max = this.frames.length - 1;
if (isInternetExplorer()) {
// switch from oninput to onchange because IE <= 11 does not conform
// with W3C specification. It ignores oninput and onchange behaves
// like oninput. In contrast, Microsoft Edge behaves correctly.
slider.setAttribute('onchange', slider.getAttribute('oninput'));
slider.setAttribute('oninput', null);
}
this.set_frame(this.current_frame);
}
Animation.prototype.get_loop_state = function(){
var button_group = document[this.loop_select_id].state;
for (var i = 0; i < button_group.length; i++) {
var button = button_group[i];
if (button.checked) {
return button.value;
}
}
return undefined;
}
Animation.prototype.set_frame = function(frame){
this.current_frame = frame;
document.getElementById(this.img_id).src =
this.frames[this.current_frame].src;
document.getElementById(this.slider_id).value = this.current_frame;
}
Animation.prototype.next_frame = function()
{
this.set_frame(Math.min(this.frames.length - 1, this.current_frame + 1));
}
Animation.prototype.previous_frame = function()
{
this.set_frame(Math.max(0, this.current_frame - 1));
}
Animation.prototype.first_frame = function()
{
this.set_frame(0);
}
Animation.prototype.last_frame = function()
{
this.set_frame(this.frames.length - 1);
}
Animation.prototype.slower = function()
{
this.interval /= 0.7;
if(this.direction > 0){this.play_animation();}
else if(this.direction < 0){this.reverse_animation();}
}
Animation.prototype.faster = function()
{
this.interval *= 0.7;
if(this.direction > 0){this.play_animation();}
else if(this.direction < 0){this.reverse_animation();}
}
Animation.prototype.anim_step_forward = function()
{
this.current_frame += 1;
if(this.current_frame < this.frames.length){
this.set_frame(this.current_frame);
}else{
var loop_state = this.get_loop_state();
if(loop_state == "loop"){
this.first_frame();
}else if(loop_state == "reflect"){
this.last_frame();
this.reverse_animation();
}else{
this.pause_animation();
this.last_frame();
}
}
}
Animation.prototype.anim_step_reverse = function()
{
this.current_frame -= 1;
if(this.current_frame >= 0){
this.set_frame(this.current_frame);
}else{
var loop_state = this.get_loop_state();
if(loop_state == "loop"){
this.last_frame();
}else if(loop_state == "reflect"){
this.first_frame();
this.play_animation();
}else{
this.pause_animation();
this.first_frame();
}
}
}
Animation.prototype.pause_animation = function()
{
this.direction = 0;
if (this.timer){
clearInterval(this.timer);
this.timer = null;
}
}
Animation.prototype.play_animation = function()
{
this.pause_animation();
this.direction = 1;
var t = this;
if (!this.timer) this.timer = setInterval(function() {
t.anim_step_forward();
}, this.interval);
}
Animation.prototype.reverse_animation = function()
{
this.pause_animation();
this.direction = -1;
var t = this;
if (!this.timer) this.timer = setInterval(function() {
t.anim_step_reverse();
}, this.interval);
}
</script>
"""
# Style definitions for the HTML template
STYLE_INCLUDE = """
<style>
.animation {
display: inline-block;
text-align: center;
}
input[type=range].anim-slider {
width: 374px;
margin-left: auto;
margin-right: auto;
}
.anim-buttons {
margin: 8px 0px;
}
.anim-buttons button {
padding: 0;
width: 36px;
}
.anim-state label {
margin-right: 8px;
}
.anim-state input {
margin: 0;
vertical-align: middle;
}
</style>
"""
# HTML template for HTMLWriter
DISPLAY_TEMPLATE = """
<div class="animation">
<img id="_anim_img{id}">
<div class="anim-controls">
<input id="_anim_slider{id}" type="range" class="anim-slider"
name="points" min="0" max="1" step="1" value="0"
oninput="anim{id}.set_frame(parseInt(this.value));"></input>
<div class="anim-buttons">
<button title="Decrease speed" aria-label="Decrease speed" onclick="anim{id}.slower()">
<i class="fa fa-minus"></i></button>
<button title="First frame" aria-label="First frame" onclick="anim{id}.first_frame()">
<i class="fa fa-fast-backward"></i></button>
<button title="Previous frame" aria-label="Previous frame" onclick="anim{id}.previous_frame()">
<i class="fa fa-step-backward"></i></button>
<button title="Play backwards" aria-label="Play backwards" onclick="anim{id}.reverse_animation()">
<i class="fa fa-play fa-flip-horizontal"></i></button>
<button title="Pause" aria-label="Pause" onclick="anim{id}.pause_animation()">
<i class="fa fa-pause"></i></button>
<button title="Play" aria-label="Play" onclick="anim{id}.play_animation()">
<i class="fa fa-play"></i></button>
<button title="Next frame" aria-label="Next frame" onclick="anim{id}.next_frame()">
<i class="fa fa-step-forward"></i></button>
<button title="Last frame" aria-label="Last frame" onclick="anim{id}.last_frame()">
<i class="fa fa-fast-forward"></i></button>
<button title="Increase speed" aria-label="Increase speed" onclick="anim{id}.faster()">
<i class="fa fa-plus"></i></button>
</div>
<form title="Repetition mode" aria-label="Repetition mode" action="#n" name="_anim_loop_select{id}"
class="anim-state">
<input type="radio" name="state" value="once" id="_anim_radio1_{id}"
{once_checked}>
<label for="_anim_radio1_{id}">Once</label>
<input type="radio" name="state" value="loop" id="_anim_radio2_{id}"
{loop_checked}>
<label for="_anim_radio2_{id}">Loop</label>
<input type="radio" name="state" value="reflect" id="_anim_radio3_{id}"
{reflect_checked}>
<label for="_anim_radio3_{id}">Reflect</label>
</form>
</div>
</div>
<script language="javascript">
/* Instantiate the Animation class. */
/* The IDs given should match those used in the template above. */
(function() {{
var img_id = "_anim_img{id}";
var slider_id = "_anim_slider{id}";
var loop_select_id = "_anim_loop_select{id}";
var frames = new Array({Nframes});
{fill_frames}
/* set a timeout to make sure all the above elements are created before
the object is initialized. */
setTimeout(function() {{
anim{id} = new Animation(frames, img_id, slider_id, {interval},
loop_select_id);
}}, 0);
}})()
</script>
"""
INCLUDED_FRAMES = """
for (var i=0; i<{Nframes}; i++){{
frames[i] = "{frame_dir}/frame" + ("0000000" + i).slice(-7) +
".{frame_format}";
}}
"""

View File

@@ -0,0 +1,213 @@
"""
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 itertools
import re
import sys
import warnings
from .deprecation import (
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 starts with an
# underscore because it is intended to be positional-only (e.g., so that
# `_api.check_isinstance([...], types=foo)` doesn't fail.
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)
"""
types = _types
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*.
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)
"""
values = _values
for key, val in kwargs.items():
if val not in values:
if _print_supported_values:
raise ValueError(
f"{val!r} is not a valid value for {key}; "
f"supported values are {', '.join(map(repr, values))}")
else:
raise ValueError(f"{val!r} is not a valid value for {key}")
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)
"""
target_shape = _shape
for k, v in kwargs.items():
data_shape = v.shape
if len(target_shape) != len(data_shape) or any(
t not in [s, None]
for t, s in zip(target_shape, data_shape)
):
dim_labels = iter(itertools.chain(
'MNLIJKLH',
(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 target_shape))
raise ValueError(
f"{k!r} must be {len(target_shape)}D "
f"with shape ({text_shape}). "
f"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)
"""
mapping = _mapping
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(
"{!r} is not a valid value for {}; supported values are {}"
.format(v, k, ', '.join(map(repr, mapping)))) from None
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.).
"""
frame = sys._getframe()
for stacklevel in itertools.count(1): # lgtm[py/unused-loop-variable]
if frame is None:
# when called in embedded context may hit frame is None
break
if not re.match(r"\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))",
# Work around sphinx-gallery not setting __name__.
frame.f_globals.get("__name__", "")):
break
frame = frame.f_back
warnings.warn(message, category, stacklevel)

View File

@@ -0,0 +1,522 @@
"""
Helper functions for deprecating parts of the Matplotlib API.
This documentation is only relevant for Matplotlib developers, not for users.
.. warning:
This module is for internal use only. Do not use it in your own code.
We may change the API at any time with no warning.
"""
import contextlib
import functools
import inspect
import warnings
class MatplotlibDeprecationWarning(UserWarning):
"""
A class for issuing deprecation warnings for Matplotlib users.
In light of the fact that Python builtin DeprecationWarnings are ignored
by default as of Python 2.7 (see link below), this class was put in to
allow for the signaling of deprecation, but via UserWarnings which are not
ignored by default.
https://docs.python.org/dev/whatsnew/2.7.html#the-future-for-python-2-x
"""
# mplDeprecation is deprecated. Use MatplotlibDeprecationWarning instead.
# remove when removing the re-import from cbook
mplDeprecation = MatplotlibDeprecationWarning
def _generate_deprecation_warning(
since, message='', name='', alternative='', pending=False, obj_type='',
addendum='', *, removal=''):
if pending:
if removal:
raise ValueError(
"A pending deprecation cannot have a scheduled removal")
else:
removal = f"in {removal}" if removal else "two minor releases later"
if not message:
message = (
"\nThe %(name)s %(obj_type)s"
+ (" will be deprecated in a future version"
if pending else
(" was deprecated in Matplotlib %(since)s"
+ (" and will be removed %(removal)s"
if removal else
"")))
+ "."
+ (" Use %(alternative)s instead." if alternative else "")
+ (" %(addendum)s" if addendum else ""))
warning_cls = (PendingDeprecationWarning if pending
else MatplotlibDeprecationWarning)
return warning_cls(message % dict(
func=name, name=name, obj_type=obj_type, since=since, removal=removal,
alternative=alternative, addendum=addendum))
def warn_deprecated(
since, *, message='', name='', alternative='', pending=False,
obj_type='', addendum='', removal=''):
"""
Display a standardized deprecation.
Parameters
----------
since : str
The release at which this API became deprecated.
message : str, optional
Override the default deprecation message. The ``%(since)s``,
``%(name)s``, ``%(alternative)s``, ``%(obj_type)s``, ``%(addendum)s``,
and ``%(removal)s`` format specifiers will be replaced by the values
of the respective arguments passed to this function.
name : str, optional
The name of the deprecated object.
alternative : str, optional
An alternative API that the user may use in place of the deprecated
API. The deprecation warning will tell the user about this alternative
if provided.
pending : bool, optional
If True, uses a PendingDeprecationWarning instead of a
DeprecationWarning. Cannot be used together with *removal*.
obj_type : str, optional
The object type being deprecated.
addendum : str, optional
Additional text appended directly to the final message.
removal : str, optional
The expected removal version. With the default (an empty string), a
removal version is automatically computed from *since*. Set to other
Falsy values to not schedule a removal date. Cannot be used together
with *pending*.
Examples
--------
Basic example::
# To warn of the deprecation of "matplotlib.name_of_module"
warn_deprecated('1.4.0', name='matplotlib.name_of_module',
obj_type='module')
"""
warning = _generate_deprecation_warning(
since, message, name, alternative, pending, obj_type, addendum,
removal=removal)
from . import warn_external
warn_external(warning, category=MatplotlibDeprecationWarning)
def deprecated(since, *, message='', name='', alternative='', pending=False,
obj_type=None, addendum='', removal=''):
"""
Decorator to mark a function, a class, or a property as deprecated.
When deprecating a classmethod, a staticmethod, or a property, the
``@deprecated`` decorator should go *under* ``@classmethod`` and
``@staticmethod`` (i.e., `deprecated` should directly decorate the
underlying callable), but *over* ``@property``.
When deprecating a class ``C`` intended to be used as a base class in a
multiple inheritance hierarchy, ``C`` *must* define an ``__init__`` method
(if ``C`` instead inherited its ``__init__`` from its own base class, then
``@deprecated`` would mess up ``__init__`` inheritance when installing its
own (deprecation-emitting) ``C.__init__``).
Parameters
----------
since : str
The release at which this API became deprecated.
message : str, optional
Override the default deprecation message. The ``%(since)s``,
``%(name)s``, ``%(alternative)s``, ``%(obj_type)s``, ``%(addendum)s``,
and ``%(removal)s`` format specifiers will be replaced by the values
of the respective arguments passed to this function.
name : str, optional
The name used in the deprecation message; if not provided, the name
is automatically determined from the deprecated object.
alternative : str, optional
An alternative API that the user may use in place of the deprecated
API. The deprecation warning will tell the user about this alternative
if provided.
pending : bool, optional
If True, uses a PendingDeprecationWarning instead of a
DeprecationWarning. Cannot be used together with *removal*.
obj_type : str, optional
The object type being deprecated; by default, 'class' if decorating
a class, 'attribute' if decorating a property, 'function' otherwise.
addendum : str, optional
Additional text appended directly to the final message.
removal : str, optional
The expected removal version. With the default (an empty string), a
removal version is automatically computed from *since*. Set to other
Falsy values to not schedule a removal date. Cannot be used together
with *pending*.
Examples
--------
Basic example::
@deprecated('1.4.0')
def the_function_to_deprecate():
pass
"""
def deprecate(obj, message=message, name=name, alternative=alternative,
pending=pending, obj_type=obj_type, addendum=addendum):
from matplotlib._api import classproperty
if isinstance(obj, type):
if obj_type is None:
obj_type = "class"
func = obj.__init__
name = name or obj.__name__
old_doc = obj.__doc__
def finalize(wrapper, new_doc):
try:
obj.__doc__ = new_doc
except AttributeError: # Can't set on some extension objects.
pass
obj.__init__ = functools.wraps(obj.__init__)(wrapper)
return obj
elif isinstance(obj, (property, classproperty)):
obj_type = "attribute"
func = None
name = name or obj.fget.__name__
old_doc = obj.__doc__
class _deprecated_property(type(obj)):
def __get__(self, instance, owner):
if instance is not None or owner is not None \
and isinstance(self, classproperty):
emit_warning()
return super().__get__(instance, owner)
def __set__(self, instance, value):
if instance is not None:
emit_warning()
return super().__set__(instance, value)
def __delete__(self, instance):
if instance is not None:
emit_warning()
return super().__delete__(instance)
def __set_name__(self, owner, set_name):
nonlocal name
if name == "<lambda>":
name = set_name
def finalize(_, new_doc):
return _deprecated_property(
fget=obj.fget, fset=obj.fset, fdel=obj.fdel, doc=new_doc)
else:
if obj_type is None:
obj_type = "function"
func = obj
name = name or obj.__name__
old_doc = func.__doc__
def finalize(wrapper, new_doc):
wrapper = functools.wraps(func)(wrapper)
wrapper.__doc__ = new_doc
return wrapper
def emit_warning():
warn_deprecated(
since, message=message, name=name, alternative=alternative,
pending=pending, obj_type=obj_type, addendum=addendum,
removal=removal)
def wrapper(*args, **kwargs):
emit_warning()
return func(*args, **kwargs)
old_doc = inspect.cleandoc(old_doc or '').strip('\n')
notes_header = '\nNotes\n-----'
new_doc = (f"[*Deprecated*] {old_doc}\n"
f"{notes_header if notes_header not in old_doc else ''}\n"
f".. deprecated:: {since}\n"
f" {message.strip()}")
if not old_doc:
# This is to prevent a spurious 'unexpected unindent' warning from
# docutils when the original docstring was blank.
new_doc += r'\ '
return finalize(wrapper, new_doc)
return deprecate
class deprecate_privatize_attribute:
"""
Helper to deprecate public access to an attribute.
This helper should only be used at class scope, as follows::
class Foo:
attr = _deprecate_privatize_attribute(*args, **kwargs)
where *all* parameters are forwarded to `deprecated`. This form makes
``attr`` a property which forwards access to ``self._attr`` (same name but
with a leading underscore), with a deprecation warning. Note that the
attribute name is derived from *the name this helper is assigned to*.
"""
def __init__(self, *args, **kwargs):
self.deprecator = deprecated(*args, **kwargs)
def __set_name__(self, owner, name):
setattr(owner, name, self.deprecator(
property(lambda self: getattr(self, f"_{name}")), name=name))
def rename_parameter(since, old, new, func=None):
"""
Decorator indicating that parameter *old* of *func* is renamed to *new*.
The actual implementation of *func* should use *new*, not *old*. If *old*
is passed to *func*, a DeprecationWarning is emitted, and its value is
used, even if *new* is also passed by keyword (this is to simplify pyplot
wrapper functions, which always pass *new* explicitly to the Axes method).
If *new* is also passed but positionally, a TypeError will be raised by the
underlying function during argument binding.
Examples
--------
::
@_api.rename_parameter("3.1", "bad_name", "good_name")
def func(good_name): ...
"""
if func is None:
return functools.partial(rename_parameter, since, old, new)
signature = inspect.signature(func)
assert old not in signature.parameters, (
f"Matplotlib internal error: {old!r} cannot be a parameter for "
f"{func.__name__}()")
assert new in signature.parameters, (
f"Matplotlib internal error: {new!r} must be a parameter for "
f"{func.__name__}()")
@functools.wraps(func)
def wrapper(*args, **kwargs):
if old in kwargs:
warn_deprecated(
since, message=f"The {old!r} parameter of {func.__name__}() "
f"has been renamed {new!r} since Matplotlib {since}; support "
f"for the old name will be dropped %(removal)s.")
kwargs[new] = kwargs.pop(old)
return func(*args, **kwargs)
# wrapper() must keep the same documented signature as func(): if we
# instead made both *old* and *new* appear in wrapper()'s signature, they
# would both show up in the pyplot function for an Axes method as well and
# pyplot would explicitly pass both arguments to the Axes method.
return wrapper
class _deprecated_parameter_class:
def __repr__(self):
return "<deprecated parameter>"
_deprecated_parameter = _deprecated_parameter_class()
def delete_parameter(since, name, func=None, **kwargs):
"""
Decorator indicating that parameter *name* of *func* is being deprecated.
The actual implementation of *func* should keep the *name* parameter in its
signature, or accept a ``**kwargs`` argument (through which *name* would be
passed).
Parameters that come after the deprecated parameter effectively become
keyword-only (as they cannot be passed positionally without triggering the
DeprecationWarning on the deprecated parameter), and should be marked as
such after the deprecation period has passed and the deprecated parameter
is removed.
Parameters other than *since*, *name*, and *func* are keyword-only and
forwarded to `.warn_deprecated`.
Examples
--------
::
@_api.delete_parameter("3.1", "unused")
def func(used_arg, other_arg, unused, more_args): ...
"""
if func is None:
return functools.partial(delete_parameter, since, name, **kwargs)
signature = inspect.signature(func)
# Name of `**kwargs` parameter of the decorated function, typically
# "kwargs" if such a parameter exists, or None if the decorated function
# doesn't accept `**kwargs`.
kwargs_name = next((param.name for param in signature.parameters.values()
if param.kind == inspect.Parameter.VAR_KEYWORD), None)
if name in signature.parameters:
kind = signature.parameters[name].kind
is_varargs = kind is inspect.Parameter.VAR_POSITIONAL
is_varkwargs = kind is inspect.Parameter.VAR_KEYWORD
if not is_varargs and not is_varkwargs:
func.__signature__ = signature = signature.replace(parameters=[
param.replace(default=_deprecated_parameter)
if param.name == name else param
for param in signature.parameters.values()])
else:
is_varargs = is_varkwargs = False
assert kwargs_name, (
f"Matplotlib internal error: {name!r} must be a parameter for "
f"{func.__name__}()")
addendum = kwargs.pop('addendum', None)
@functools.wraps(func)
def wrapper(*inner_args, **inner_kwargs):
arguments = signature.bind(*inner_args, **inner_kwargs).arguments
if is_varargs and arguments.get(name):
warn_deprecated(
since, message=f"Additional positional arguments to "
f"{func.__name__}() are deprecated since %(since)s and "
f"support for them will be removed %(removal)s.")
elif is_varkwargs and arguments.get(name):
warn_deprecated(
since, message=f"Additional keyword arguments to "
f"{func.__name__}() are deprecated since %(since)s and "
f"support for them will be removed %(removal)s.")
# We cannot just check `name not in arguments` because the pyplot
# wrappers always pass all arguments explicitly.
elif any(name in d and d[name] != _deprecated_parameter
for d in [arguments, arguments.get(kwargs_name, {})]):
deprecation_addendum = (
f"If any parameter follows {name!r}, they should be passed as "
f"keyword, not positionally.")
warn_deprecated(
since,
name=repr(name),
obj_type=f"parameter of {func.__name__}()",
addendum=(addendum + " " + deprecation_addendum) if addendum
else deprecation_addendum,
**kwargs)
return func(*inner_args, **inner_kwargs)
return wrapper
def make_keyword_only(since, name, func=None):
"""
Decorator indicating that passing parameter *name* (or any of the following
ones) positionally to *func* is being deprecated.
"""
if func is None:
return functools.partial(make_keyword_only, since, name)
signature = inspect.signature(func)
POK = inspect.Parameter.POSITIONAL_OR_KEYWORD
KWO = inspect.Parameter.KEYWORD_ONLY
assert (name in signature.parameters
and signature.parameters[name].kind == POK), (
f"Matplotlib internal error: {name!r} must be a positional-or-keyword "
f"parameter for {func.__name__}()")
names = [*signature.parameters]
kwonly = [name for name in names[names.index(name):]
if signature.parameters[name].kind == POK]
func.__signature__ = signature.replace(parameters=[
param.replace(kind=KWO) if param.name in kwonly else param
for param in signature.parameters.values()])
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Don't use signature.bind here, as it would fail when stacked with
# rename_parameter and an "old" argument name is passed in
# (signature.bind would fail, but the actual call would succeed).
idx = [*func.__signature__.parameters].index(name)
if len(args) > idx:
warn_deprecated(
since, message="Passing the %(name)s %(obj_type)s "
"positionally is deprecated since Matplotlib %(since)s; the "
"parameter will become keyword-only %(removal)s.",
name=name, obj_type=f"parameter of {func.__name__}()")
return func(*args, **kwargs)
return wrapper
def deprecate_method_override(method, obj, *, allow_empty=False, **kwargs):
"""
Return ``obj.method`` with a deprecation if it was overridden, else None.
Parameters
----------
method
An unbound method, i.e. an expression of the form
``Class.method_name``. Remember that within the body of a method, one
can always use ``__class__`` to refer to the class that is currently
being defined.
obj
Either an object of the class where *method* is defined, or a subclass
of that class.
allow_empty : bool, default: False
Whether to allow overrides by "empty" methods without emitting a
warning.
**kwargs
Additional parameters passed to `warn_deprecated` to generate the
deprecation warning; must at least include the "since" key.
"""
def empty(): pass
def empty_with_docstring(): """doc"""
name = method.__name__
bound_child = getattr(obj, name)
bound_base = (
method # If obj is a class, then we need to use unbound methods.
if isinstance(bound_child, type(empty)) and isinstance(obj, type)
else method.__get__(obj))
if (bound_child != bound_base
and (not allow_empty
or (getattr(getattr(bound_child, "__code__", None),
"co_code", None)
not in [empty.__code__.co_code,
empty_with_docstring.__code__.co_code]))):
warn_deprecated(**{"name": name, "obj_type": "method", **kwargs})
return bound_child
return None
@contextlib.contextmanager
def suppress_matplotlib_deprecation_warning():
with warnings.catch_warnings():
warnings.simplefilter("ignore", MatplotlibDeprecationWarning)
yield

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,619 @@
"""
Adjust subplot layouts so that there are no overlapping axes or axes
decorations. All axes decorations are dealt with (labels, ticks, titles,
ticklabels) and some dependent artists are also dealt with (colorbar,
suptitle).
Layout is done via `~matplotlib.gridspec`, with one constraint per gridspec,
so it is possible to have overlapping axes if the gridspecs overlap (i.e.
using `~matplotlib.gridspec.GridSpecFromSubplotSpec`). Axes placed using
``figure.subplots()`` or ``figure.add_subplots()`` will participate in the
layout. Axes manually placed via ``figure.add_axes()`` will not.
See Tutorial: :doc:`/tutorials/intermediate/constrainedlayout_guide`
"""
import logging
import numpy as np
from matplotlib import _api
import matplotlib.transforms as mtransforms
_log = logging.getLogger(__name__)
"""
General idea:
-------------
First, a figure has a gridspec that divides the figure into nrows and ncols,
with heights and widths set by ``height_ratios`` and ``width_ratios``,
often just set to 1 for an equal grid.
Subplotspecs that are derived from this gridspec can contain either a
``SubPanel``, a ``GridSpecFromSubplotSpec``, or an axes. The ``SubPanel`` and
``GridSpecFromSubplotSpec`` are dealt with recursively and each contain an
analogous layout.
Each ``GridSpec`` has a ``_layoutgrid`` attached to it. The ``_layoutgrid``
has the same logical layout as the ``GridSpec``. Each row of the grid spec
has a top and bottom "margin" and each column has a left and right "margin".
The "inner" height of each row is constrained to be the same (or as modified
by ``height_ratio``), and the "inner" width of each column is
constrained to be the same (as modified by ``width_ratio``), where "inner"
is the width or height of each column/row minus the size of the margins.
Then the size of the margins for each row and column are determined as the
max width of the decorators on each axes that has decorators in that margin.
For instance, a normal axes would have a left margin that includes the
left ticklabels, and the ylabel if it exists. The right margin may include a
colorbar, the bottom margin the xaxis decorations, and the top margin the
title.
With these constraints, the solver then finds appropriate bounds for the
columns and rows. It's possible that the margins take up the whole figure,
in which case the algorithm is not applied and a warning is raised.
See the tutorial doc:`/tutorials/intermediate/constrainedlayout_guide`
for more discussion of the algorithm with examples.
"""
######################################################
def do_constrained_layout(fig, renderer, h_pad, w_pad,
hspace=None, wspace=None):
"""
Do the constrained_layout. Called at draw time in
``figure.constrained_layout()``
Parameters
----------
fig : Figure
``Figure`` instance to do the layout in.
renderer : Renderer
Renderer to use.
h_pad, w_pad : float
Padding around the axes elements in figure-normalized units.
hspace, wspace : float
Fraction of the figure to dedicate to space between the
axes. These are evenly spread between the gaps between the axes.
A value of 0.2 for a three-column layout would have a space
of 0.1 of the figure width between each column.
If h/wspace < h/w_pad, then the pads are used instead.
"""
# list of unique gridspecs that contain child axes:
gss = set()
for ax in fig.axes:
if hasattr(ax, 'get_subplotspec'):
gs = ax.get_subplotspec().get_gridspec()
if gs._layoutgrid is not None:
gss.add(gs)
gss = list(gss)
if len(gss) == 0:
_api.warn_external('There are no gridspecs with layoutgrids. '
'Possibly did not call parent GridSpec with the'
' "figure" keyword')
for _ in range(2):
# do the algorithm twice. This has to be done because decorations
# change size after the first re-position (i.e. x/yticklabels get
# larger/smaller). This second reposition tends to be much milder,
# so doing twice makes things work OK.
# make margins for all the axes and subfigures in the
# figure. Add margins for colorbars...
_make_layout_margins(fig, renderer, h_pad=h_pad, w_pad=w_pad,
hspace=hspace, wspace=wspace)
_make_margin_suptitles(fig, renderer, h_pad=h_pad, w_pad=w_pad)
# if a layout is such that a columns (or rows) margin has no
# constraints, we need to make all such instances in the grid
# match in margin size.
_match_submerged_margins(fig)
# update all the variables in the layout.
fig._layoutgrid.update_variables()
if _check_no_collapsed_axes(fig):
_reposition_axes(fig, renderer, h_pad=h_pad, w_pad=w_pad,
hspace=hspace, wspace=wspace)
else:
_api.warn_external('constrained_layout not applied because '
'axes sizes collapsed to zero. Try making '
'figure larger or axes decorations smaller.')
_reset_margins(fig)
def _check_no_collapsed_axes(fig):
"""
Check that no axes have collapsed to zero size.
"""
for panel in fig.subfigs:
ok = _check_no_collapsed_axes(panel)
if not ok:
return False
for ax in fig.axes:
if hasattr(ax, 'get_subplotspec'):
gs = ax.get_subplotspec().get_gridspec()
lg = gs._layoutgrid
if lg is not None:
for i in range(gs.nrows):
for j in range(gs.ncols):
bb = lg.get_inner_bbox(i, j)
if bb.width <= 0 or bb.height <= 0:
return False
return True
def _get_margin_from_padding(object, *, w_pad=0, h_pad=0,
hspace=0, wspace=0):
ss = object._subplotspec
gs = ss.get_gridspec()
lg = gs._layoutgrid
if hasattr(gs, 'hspace'):
_hspace = (gs.hspace if gs.hspace is not None else hspace)
_wspace = (gs.wspace if gs.wspace is not None else wspace)
else:
_hspace = (gs._hspace if gs._hspace is not None else hspace)
_wspace = (gs._wspace if gs._wspace is not None else wspace)
_wspace = _wspace / 2
_hspace = _hspace / 2
nrows, ncols = gs.get_geometry()
# there are two margins for each direction. The "cb"
# margins are for pads and colorbars, the non-"cb" are
# for the axes decorations (labels etc).
margin = {'leftcb': w_pad, 'rightcb': w_pad,
'bottomcb': h_pad, 'topcb': h_pad,
'left': 0, 'right': 0,
'top': 0, 'bottom': 0}
if _wspace / ncols > w_pad:
if ss.colspan.start > 0:
margin['leftcb'] = _wspace / ncols
if ss.colspan.stop < ncols:
margin['rightcb'] = _wspace / ncols
if _hspace / nrows > h_pad:
if ss.rowspan.stop < nrows:
margin['bottomcb'] = _hspace / nrows
if ss.rowspan.start > 0:
margin['topcb'] = _hspace / nrows
return margin
def _make_layout_margins(fig, renderer, *, w_pad=0, h_pad=0,
hspace=0, wspace=0):
"""
For each axes, make a margin between the *pos* layoutbox and the
*axes* layoutbox be a minimum size that can accommodate the
decorations on the axis.
Then make room for colorbars.
"""
for panel in fig.subfigs: # recursively make child panel margins
ss = panel._subplotspec
_make_layout_margins(panel, renderer, w_pad=w_pad, h_pad=h_pad,
hspace=hspace, wspace=wspace)
margins = _get_margin_from_padding(panel, w_pad=0, h_pad=0,
hspace=hspace, wspace=wspace)
panel._layoutgrid.parent.edit_outer_margin_mins(margins, ss)
for ax in fig._localaxes.as_list():
if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout():
continue
ss = ax.get_subplotspec()
gs = ss.get_gridspec()
nrows, ncols = gs.get_geometry()
if gs._layoutgrid is None:
return
margin = _get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
hspace=hspace, wspace=wspace)
margin0 = margin.copy()
pos, bbox = _get_pos_and_bbox(ax, renderer)
# the margin is the distance between the bounding box of the axes
# and its position (plus the padding from above)
margin['left'] += pos.x0 - bbox.x0
margin['right'] += bbox.x1 - pos.x1
# remember that rows are ordered from top:
margin['bottom'] += pos.y0 - bbox.y0
margin['top'] += bbox.y1 - pos.y1
# make margin for colorbars. These margins go in the
# padding margin, versus the margin for axes decorators.
for cbax in ax._colorbars:
# note pad is a fraction of the parent width...
pad = _colorbar_get_pad(cbax)
# colorbars can be child of more than one subplot spec:
cbp_rspan, cbp_cspan = _get_cb_parent_spans(cbax)
loc = cbax._colorbar_info['location']
cbpos, cbbbox = _get_pos_and_bbox(cbax, renderer)
if loc == 'right':
if cbp_cspan.stop == ss.colspan.stop:
# only increase if the colorbar is on the right edge
margin['rightcb'] += cbbbox.width + pad
elif loc == 'left':
if cbp_cspan.start == ss.colspan.start:
# only increase if the colorbar is on the left edge
margin['leftcb'] += cbbbox.width + pad
elif loc == 'top':
if cbp_rspan.start == ss.rowspan.start:
margin['topcb'] += cbbbox.height + pad
else:
if cbp_rspan.stop == ss.rowspan.stop:
margin['bottomcb'] += cbbbox.height + pad
# If the colorbars are wider than the parent box in the
# cross direction
if loc in ['top', 'bottom']:
if (cbp_cspan.start == ss.colspan.start and
cbbbox.x0 < bbox.x0):
margin['left'] += bbox.x0 - cbbbox.x0
if (cbp_cspan.stop == ss.colspan.stop and
cbbbox.x1 > bbox.x1):
margin['right'] += cbbbox.x1 - bbox.x1
# or taller:
if loc in ['left', 'right']:
if (cbp_rspan.stop == ss.rowspan.stop and
cbbbox.y0 < bbox.y0):
margin['bottom'] += bbox.y0 - cbbbox.y0
if (cbp_rspan.start == ss.rowspan.start and
cbbbox.y1 > bbox.y1):
margin['top'] += cbbbox.y1 - bbox.y1
# pass the new margins down to the layout grid for the solution...
gs._layoutgrid.edit_outer_margin_mins(margin, ss)
def _make_margin_suptitles(fig, renderer, *, w_pad=0, h_pad=0):
# Figure out how large the suptitle is and make the
# top level figure margin larger.
inv_trans_fig = fig.transFigure.inverted().transform_bbox
# get the h_pad and w_pad as distances in the local subfigure coordinates:
padbox = mtransforms.Bbox([[0, 0], [w_pad, h_pad]])
padbox = (fig.transFigure -
fig.transSubfigure).transform_bbox(padbox)
h_pad_local = padbox.height
w_pad_local = padbox.width
for panel in fig.subfigs:
_make_margin_suptitles(panel, renderer, w_pad=w_pad, h_pad=h_pad)
if fig._suptitle is not None and fig._suptitle.get_in_layout():
p = fig._suptitle.get_position()
if getattr(fig._suptitle, '_autopos', False):
fig._suptitle.set_position((p[0], 1 - h_pad_local))
bbox = inv_trans_fig(fig._suptitle.get_tightbbox(renderer))
fig._layoutgrid.edit_margin_min('top', bbox.height + 2 * h_pad)
if fig._supxlabel is not None and fig._supxlabel.get_in_layout():
p = fig._supxlabel.get_position()
if getattr(fig._supxlabel, '_autopos', False):
fig._supxlabel.set_position((p[0], h_pad_local))
bbox = inv_trans_fig(fig._supxlabel.get_tightbbox(renderer))
fig._layoutgrid.edit_margin_min('bottom', bbox.height + 2 * h_pad)
if fig._supylabel is not None and fig._supylabel.get_in_layout():
p = fig._supylabel.get_position()
if getattr(fig._supylabel, '_autopos', False):
fig._supylabel.set_position((w_pad_local, p[1]))
bbox = inv_trans_fig(fig._supylabel.get_tightbbox(renderer))
fig._layoutgrid.edit_margin_min('left', bbox.width + 2 * w_pad)
def _match_submerged_margins(fig):
"""
Make the margins that are submerged inside an Axes the same size.
This allows axes that span two columns (or rows) that are offset
from one another to have the same size.
This gives the proper layout for something like::
fig = plt.figure(constrained_layout=True)
axs = fig.subplot_mosaic("AAAB\nCCDD")
Without this routine, the axes D will be wider than C, because the
margin width between the two columns in C has no width by default,
whereas the margins between the two columns of D are set by the
width of the margin between A and B. However, obviously the user would
like C and D to be the same size, so we need to add constraints to these
"submerged" margins.
This routine makes all the interior margins the same, and the spacing
between the three columns in A and the two column in C are all set to the
margins between the two columns of D.
See test_constrained_layout::test_constrained_layout12 for an example.
"""
for panel in fig.subfigs:
_match_submerged_margins(panel)
axs = [a for a in fig.get_axes() if (hasattr(a, 'get_subplotspec')
and a.get_in_layout())]
for ax1 in axs:
ss1 = ax1.get_subplotspec()
lg1 = ss1.get_gridspec()._layoutgrid
if lg1 is None:
axs.remove(ax1)
continue
# interior columns:
if len(ss1.colspan) > 1:
maxsubl = np.max(
lg1.margin_vals['left'][ss1.colspan[1:]] +
lg1.margin_vals['leftcb'][ss1.colspan[1:]]
)
maxsubr = np.max(
lg1.margin_vals['right'][ss1.colspan[:-1]] +
lg1.margin_vals['rightcb'][ss1.colspan[:-1]]
)
for ax2 in axs:
ss2 = ax2.get_subplotspec()
lg2 = ss2.get_gridspec()._layoutgrid
if lg2 is not None and len(ss2.colspan) > 1:
maxsubl2 = np.max(
lg2.margin_vals['left'][ss2.colspan[1:]] +
lg2.margin_vals['leftcb'][ss2.colspan[1:]])
if maxsubl2 > maxsubl:
maxsubl = maxsubl2
maxsubr2 = np.max(
lg2.margin_vals['right'][ss2.colspan[:-1]] +
lg2.margin_vals['rightcb'][ss2.colspan[:-1]])
if maxsubr2 > maxsubr:
maxsubr = maxsubr2
for i in ss1.colspan[1:]:
lg1.edit_margin_min('left', maxsubl, cell=i)
for i in ss1.colspan[:-1]:
lg1.edit_margin_min('right', maxsubr, cell=i)
# interior rows:
if len(ss1.rowspan) > 1:
maxsubt = np.max(
lg1.margin_vals['top'][ss1.rowspan[1:]] +
lg1.margin_vals['topcb'][ss1.rowspan[1:]]
)
maxsubb = np.max(
lg1.margin_vals['bottom'][ss1.rowspan[:-1]] +
lg1.margin_vals['bottomcb'][ss1.rowspan[:-1]]
)
for ax2 in axs:
ss2 = ax2.get_subplotspec()
lg2 = ss2.get_gridspec()._layoutgrid
if lg2 is not None:
if len(ss2.rowspan) > 1:
maxsubt = np.max([np.max(
lg2.margin_vals['top'][ss2.rowspan[1:]] +
lg2.margin_vals['topcb'][ss2.rowspan[1:]]
), maxsubt])
maxsubb = np.max([np.max(
lg2.margin_vals['bottom'][ss2.rowspan[:-1]] +
lg2.margin_vals['bottomcb'][ss2.rowspan[:-1]]
), maxsubb])
for i in ss1.rowspan[1:]:
lg1.edit_margin_min('top', maxsubt, cell=i)
for i in ss1.rowspan[:-1]:
lg1.edit_margin_min('bottom', maxsubb, cell=i)
def _get_cb_parent_spans(cbax):
"""
Figure out which subplotspecs this colorbar belongs to:
"""
rowstart = np.inf
rowstop = -np.inf
colstart = np.inf
colstop = -np.inf
for parent in cbax._colorbar_info['parents']:
ss = parent.get_subplotspec()
rowstart = min(ss.rowspan.start, rowstart)
rowstop = max(ss.rowspan.stop, rowstop)
colstart = min(ss.colspan.start, colstart)
colstop = max(ss.colspan.stop, colstop)
rowspan = range(rowstart, rowstop)
colspan = range(colstart, colstop)
return rowspan, colspan
def _get_pos_and_bbox(ax, renderer):
"""
Get the position and the bbox for the axes.
Parameters
----------
ax
renderer
Returns
-------
pos : Bbox
Position in figure coordinates.
bbox : Bbox
Tight bounding box in figure coordinates.
"""
fig = ax.figure
pos = ax.get_position(original=True)
# pos is in panel co-ords, but we need in figure for the layout
pos = pos.transformed(fig.transSubfigure - fig.transFigure)
try:
tightbbox = ax.get_tightbbox(renderer=renderer, for_layout_only=True)
except TypeError:
tightbbox = ax.get_tightbbox(renderer=renderer)
if tightbbox is None:
bbox = pos
else:
bbox = tightbbox.transformed(fig.transFigure.inverted())
return pos, bbox
def _reposition_axes(fig, renderer, *, w_pad=0, h_pad=0, hspace=0, wspace=0):
"""
Reposition all the axes based on the new inner bounding box.
"""
trans_fig_to_subfig = fig.transFigure - fig.transSubfigure
for sfig in fig.subfigs:
bbox = sfig._layoutgrid.get_outer_bbox()
sfig._redo_transform_rel_fig(
bbox=bbox.transformed(trans_fig_to_subfig))
_reposition_axes(sfig, renderer,
w_pad=w_pad, h_pad=h_pad,
wspace=wspace, hspace=hspace)
for ax in fig._localaxes.as_list():
if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout():
continue
# grid bbox is in Figure coordinates, but we specify in panel
# coordinates...
ss = ax.get_subplotspec()
gs = ss.get_gridspec()
nrows, ncols = gs.get_geometry()
if gs._layoutgrid is None:
return
bbox = gs._layoutgrid.get_inner_bbox(rows=ss.rowspan, cols=ss.colspan)
bboxouter = gs._layoutgrid.get_outer_bbox(rows=ss.rowspan,
cols=ss.colspan)
# transform from figure to panel for set_position:
newbbox = trans_fig_to_subfig.transform_bbox(bbox)
ax._set_position(newbbox)
# move the colorbars:
# we need to keep track of oldw and oldh if there is more than
# one colorbar:
offset = {'left': 0, 'right': 0, 'bottom': 0, 'top': 0}
for nn, cbax in enumerate(ax._colorbars[::-1]):
if ax == cbax._colorbar_info['parents'][0]:
margin = _reposition_colorbar(
cbax, renderer, offset=offset)
def _reposition_colorbar(cbax, renderer, *, offset=None):
"""
Place the colorbar in its new place.
Parameters
----------
cbax : Axes
Axes for the colorbar
renderer :
w_pad, h_pad : float
width and height padding (in fraction of figure)
hspace, wspace : float
width and height padding as fraction of figure size divided by
number of columns or rows
margin : array-like
offset the colorbar needs to be pushed to in order to
account for multiple colorbars
"""
parents = cbax._colorbar_info['parents']
gs = parents[0].get_gridspec()
ncols, nrows = gs.ncols, gs.nrows
fig = cbax.figure
trans_fig_to_subfig = fig.transFigure - fig.transSubfigure
cb_rspans, cb_cspans = _get_cb_parent_spans(cbax)
bboxparent = gs._layoutgrid.get_bbox_for_cb(rows=cb_rspans, cols=cb_cspans)
pb = gs._layoutgrid.get_inner_bbox(rows=cb_rspans, cols=cb_cspans)
location = cbax._colorbar_info['location']
anchor = cbax._colorbar_info['anchor']
fraction = cbax._colorbar_info['fraction']
aspect = cbax._colorbar_info['aspect']
shrink = cbax._colorbar_info['shrink']
cbpos, cbbbox = _get_pos_and_bbox(cbax, renderer)
# Colorbar gets put at extreme edge of outer bbox of the subplotspec
# It needs to be moved in by: 1) a pad 2) its "margin" 3) by
# any colorbars already added at this location:
cbpad = _colorbar_get_pad(cbax)
if location in ('left', 'right'):
# fraction and shrink are fractions of parent
pbcb = pb.shrunk(fraction, shrink).anchored(anchor, pb)
# The colorbar is at the left side of the parent. Need
# to translate to right (or left)
if location == 'right':
lmargin = cbpos.x0 - cbbbox.x0
dx = bboxparent.x1 - pbcb.x0 + offset['right']
dx += cbpad + lmargin
offset['right'] += cbbbox.width + cbpad
pbcb = pbcb.translated(dx, 0)
else:
lmargin = cbpos.x0 - cbbbox.x0
dx = bboxparent.x0 - pbcb.x0 # edge of parent
dx += -cbbbox.width - cbpad + lmargin - offset['left']
offset['left'] += cbbbox.width + cbpad
pbcb = pbcb.translated(dx, 0)
else: # horizontal axes:
pbcb = pb.shrunk(shrink, fraction).anchored(anchor, pb)
if location == 'top':
bmargin = cbpos.y0 - cbbbox.y0
dy = bboxparent.y1 - pbcb.y0 + offset['top']
dy += cbpad + bmargin
offset['top'] += cbbbox.height + cbpad
pbcb = pbcb.translated(0, dy)
else:
bmargin = cbpos.y0 - cbbbox.y0
dy = bboxparent.y0 - pbcb.y0
dy += -cbbbox.height - cbpad + bmargin - offset['bottom']
offset['bottom'] += cbbbox.height + cbpad
pbcb = pbcb.translated(0, dy)
pbcb = trans_fig_to_subfig.transform_bbox(pbcb)
cbax.set_transform(fig.transSubfigure)
cbax._set_position(pbcb)
cbax.set_aspect(aspect, anchor=anchor, adjustable='box')
return offset
def _reset_margins(fig):
"""
Reset the margins in the layoutboxes of fig.
Margins are usually set as a minimum, so if the figure gets smaller
the minimum needs to be zero in order for it to grow again.
"""
for span in fig.subfigs:
_reset_margins(span)
for ax in fig.axes:
if hasattr(ax, 'get_subplotspec') and ax.get_in_layout():
ss = ax.get_subplotspec()
gs = ss.get_gridspec()
if gs._layoutgrid is not None:
gs._layoutgrid.reset_margins()
fig._layoutgrid.reset_margins()
def _colorbar_get_pad(cax):
parents = cax._colorbar_info['parents']
gs = parents[0].get_gridspec()
cb_rspans, cb_cspans = _get_cb_parent_spans(cax)
bboxouter = gs._layoutgrid.get_inner_bbox(rows=cb_rspans, cols=cb_cspans)
if cax._colorbar_info['location'] in ['right', 'left']:
size = bboxouter.width
else:
size = bboxouter.height
return cax._colorbar_info['pad'] * size

View File

@@ -0,0 +1,208 @@
"""
Enums representing sets of strings that Matplotlib uses as input parameters.
Matplotlib often uses simple data types like strings or tuples to define a
concept; e.g. the line capstyle can be specified as one of 'butt', 'round',
or 'projecting'. The classes in this module are used internally and serve to
document these concepts formally.
As an end-user you will not use these classes directly, but only the values
they define.
"""
from enum import Enum, auto
from matplotlib import cbook, docstring
class _AutoStringNameEnum(Enum):
"""Automate the ``name = 'name'`` part of making a (str, Enum)."""
def _generate_next_value_(name, start, count, last_values):
return name
def __hash__(self):
return str(self).__hash__()
def _deprecate_case_insensitive_join_cap(s):
s_low = s.lower()
if s != s_low:
if s_low in ['miter', 'round', 'bevel']:
cbook.warn_deprecated(
"3.3", message="Case-insensitive capstyles are deprecated "
"since %(since)s and support for them will be removed "
"%(removal)s; please pass them in lowercase.")
elif s_low in ['butt', 'round', 'projecting']:
cbook.warn_deprecated(
"3.3", message="Case-insensitive joinstyles are deprecated "
"since %(since)s and support for them will be removed "
"%(removal)s; please pass them in lowercase.")
# Else, error out at the check_in_list stage.
return s_low
class JoinStyle(str, _AutoStringNameEnum):
"""
Define how the connection between two line segments is drawn.
For a visual impression of each *JoinStyle*, `view these docs online
<JoinStyle>`, or run `JoinStyle.demo`.
Lines in Matplotlib are typically defined by a 1D `~.path.Path` and a
finite ``linewidth``, where the underlying 1D `~.path.Path` represents the
center of the stroked line.
By default, `~.backend_bases.GraphicsContextBase` defines the boundaries of
a stroked line to simply be every point within some radius,
``linewidth/2``, away from any point of the center line. However, this
results in corners appearing "rounded", which may not be the desired
behavior if you are drawing, for example, a polygon or pointed star.
**Supported values:**
.. rst-class:: value-list
'miter'
the "arrow-tip" style. Each boundary of the filled-in area will
extend in a straight line parallel to the tangent vector of the
centerline at the point it meets the corner, until they meet in a
sharp point.
'round'
stokes every point within a radius of ``linewidth/2`` of the center
lines.
'bevel'
the "squared-off" style. It can be thought of as a rounded corner
where the "circular" part of the corner has been cut off.
.. note::
Very long miter tips are cut off (to form a *bevel*) after a
backend-dependent limit called the "miter limit", which specifies the
maximum allowed ratio of miter length to line width. For example, the
PDF backend uses the default value of 10 specified by the PDF standard,
while the SVG backend does not even specify the miter limit, resulting
in a default value of 4 per the SVG specification. Matplotlib does not
currently allow the user to adjust this parameter.
A more detailed description of the effect of a miter limit can be found
in the `Mozilla Developer Docs
<https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-miterlimit>`_
.. plot::
:alt: Demo of possible JoinStyle's
from matplotlib._enums import JoinStyle
JoinStyle.demo()
"""
miter = auto()
round = auto()
bevel = auto()
def __init__(self, s):
s = _deprecate_case_insensitive_join_cap(s)
Enum.__init__(self)
@staticmethod
def demo():
"""Demonstrate how each JoinStyle looks for various join angles."""
import numpy as np
import matplotlib.pyplot as plt
def plot_angle(ax, x, y, angle, style):
phi = np.radians(angle)
xx = [x + .5, x, x + .5*np.cos(phi)]
yy = [y, y, y + .5*np.sin(phi)]
ax.plot(xx, yy, lw=12, color='tab:blue', solid_joinstyle=style)
ax.plot(xx, yy, lw=1, color='black')
ax.plot(xx[1], yy[1], 'o', color='tab:red', markersize=3)
fig, ax = plt.subplots(figsize=(5, 4), constrained_layout=True)
ax.set_title('Join style')
for x, style in enumerate(['miter', 'round', 'bevel']):
ax.text(x, 5, style)
for y, angle in enumerate([20, 45, 60, 90, 120]):
plot_angle(ax, x, y, angle, style)
if x == 0:
ax.text(-1.3, y, f'{angle} degrees')
ax.set_xlim(-1.5, 2.75)
ax.set_ylim(-.5, 5.5)
ax.set_axis_off()
fig.show()
JoinStyle.input_description = "{" \
+ ", ".join([f"'{js.name}'" for js in JoinStyle]) \
+ "}"
class CapStyle(str, _AutoStringNameEnum):
r"""
Define how the two endpoints (caps) of an unclosed line are drawn.
How to draw the start and end points of lines that represent a closed curve
(i.e. that end in a `~.path.Path.CLOSEPOLY`) is controlled by the line's
`JoinStyle`. For all other lines, how the start and end points are drawn is
controlled by the *CapStyle*.
For a visual impression of each *CapStyle*, `view these docs online
<CapStyle>` or run `CapStyle.demo`.
**Supported values:**
.. rst-class:: value-list
'butt'
the line is squared off at its endpoint.
'projecting'
the line is squared off as in *butt*, but the filled in area
extends beyond the endpoint a distance of ``linewidth/2``.
'round'
like *butt*, but a semicircular cap is added to the end of the
line, of radius ``linewidth/2``.
.. plot::
:alt: Demo of possible CapStyle's
from matplotlib._enums import CapStyle
CapStyle.demo()
"""
butt = 'butt'
projecting = 'projecting'
round = 'round'
def __init__(self, s):
s = _deprecate_case_insensitive_join_cap(s)
Enum.__init__(self)
@staticmethod
def demo():
"""Demonstrate how each CapStyle looks for a thick line segment."""
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(4, 1.2))
ax = fig.add_axes([0, 0, 1, 0.8])
ax.set_title('Cap style')
for x, style in enumerate(['butt', 'round', 'projecting']):
ax.text(x+0.25, 0.85, style, ha='center')
xx = [x, x+0.5]
yy = [0, 0]
ax.plot(xx, yy, lw=12, color='tab:blue', solid_capstyle=style)
ax.plot(xx, yy, lw=1, color='black')
ax.plot(xx, yy, 'o', color='tab:red', markersize=3)
ax.text(2.25, 0.55, '(default)', ha='center')
ax.set_ylim(-.5, 1.5)
ax.set_axis_off()
fig.show()
CapStyle.input_description = "{" \
+ ", ".join([f"'{cs.name}'" for cs in CapStyle]) \
+ "}"
docstring.interpd.update({'JoinStyle': JoinStyle.input_description,
'CapStyle': CapStyle.input_description})

View File

@@ -0,0 +1,64 @@
"""
Internal debugging utilities, that are not expected to be used in the rest of
the codebase.
WARNING: Code in this module may change without prior notice!
"""
from io import StringIO
from pathlib import Path
import subprocess
from matplotlib.transforms import TransformNode
def graphviz_dump_transform(transform, dest, *, highlight=None):
"""
Generate a graphical representation of the transform tree for *transform*
using the :program:`dot` program (which this function depends on). The
output format (png, dot, etc.) is determined from the suffix of *dest*.
Parameters
----------
transform : `~matplotlib.transform.Transform`
The represented transform.
dest : str
Output filename. The extension must be one of the formats supported
by :program:`dot`, e.g. png, svg, dot, ...
(see https://www.graphviz.org/doc/info/output.html).
highlight : list of `~matplotlib.transform.Transform` or None
The transforms in the tree to be drawn in bold.
If *None*, *transform* is highlighted.
"""
if highlight is None:
highlight = [transform]
seen = set()
def recurse(root, buf):
if id(root) in seen:
return
seen.add(id(root))
props = {}
label = type(root).__name__
if root._invalid:
label = f'[{label}]'
if root in highlight:
props['style'] = 'bold'
props['shape'] = 'box'
props['label'] = '"%s"' % label
props = ' '.join(map('{0[0]}={0[1]}'.format, props.items()))
buf.write(f'{id(root)} [{props}];\n')
for key, val in vars(root).items():
if isinstance(val, TransformNode) and id(root) in val._parents:
buf.write(f'"{id(root)}" -> "{id(val)}" '
f'[label="{key}", fontsize=10];\n')
recurse(val, buf)
buf = StringIO()
buf.write('digraph G {\n')
recurse(transform, buf)
buf.write('}\n')
subprocess.run(
['dot', '-T', Path(dest).suffix[1:], '-o', dest],
input=buf.getvalue().encode('utf-8'), check=True)

View File

@@ -0,0 +1,560 @@
"""
A layoutgrid is a nrows by ncols set of boxes, meant to be used by
`._constrained_layout`, each box is analogous to a subplotspec element of
a gridspec.
Each box is defined by left[ncols], right[ncols], bottom[nrows] and top[nrows],
and by two editable margins for each side. The main margin gets its value
set by the size of ticklabels, titles, etc on each axes that is in the figure.
The outer margin is the padding around the axes, and space for any
colorbars.
The "inner" widths and heights of these boxes are then constrained to be the
same (relative the values of `width_ratios[ncols]` and `height_ratios[nrows]`).
The layoutgrid is then constrained to be contained within a parent layoutgrid,
its column(s) and row(s) specified when it is created.
"""
import itertools
import kiwisolver as kiwi
import logging
import numpy as np
from matplotlib.transforms import Bbox
_log = logging.getLogger(__name__)
class LayoutGrid:
"""
Analogous to a gridspec, and contained in another LayoutGrid.
"""
def __init__(self, parent=None, parent_pos=(0, 0),
parent_inner=False, name='', ncols=1, nrows=1,
h_pad=None, w_pad=None, width_ratios=None,
height_ratios=None):
Variable = kiwi.Variable
self.parent = parent
self.parent_pos = parent_pos
self.parent_inner = parent_inner
self.name = name
self.nrows = nrows
self.ncols = ncols
self.height_ratios = np.atleast_1d(height_ratios)
if height_ratios is None:
self.height_ratios = np.ones(nrows)
self.width_ratios = np.atleast_1d(width_ratios)
if width_ratios is None:
self.width_ratios = np.ones(ncols)
sn = self.name + '_'
if parent is None:
self.parent = None
self.solver = kiwi.Solver()
else:
self.parent = parent
parent.add_child(self, *parent_pos)
self.solver = self.parent.solver
# keep track of artist associated w/ this layout. Can be none
self.artists = np.empty((nrows, ncols), dtype=object)
self.children = np.empty((nrows, ncols), dtype=object)
self.margins = {}
self.margin_vals = {}
# all the boxes in each column share the same left/right margins:
for todo in ['left', 'right', 'leftcb', 'rightcb']:
# track the value so we can change only if a margin is larger
# than the current value
self.margin_vals[todo] = np.zeros(ncols)
sol = self.solver
# These are redundant, but make life easier if
# we define them all. All that is really
# needed is left/right, margin['left'], and margin['right']
self.widths = [Variable(f'{sn}widths[{i}]') for i in range(ncols)]
self.lefts = [Variable(f'{sn}lefts[{i}]') for i in range(ncols)]
self.rights = [Variable(f'{sn}rights[{i}]') for i in range(ncols)]
self.inner_widths = [Variable(f'{sn}inner_widths[{i}]')
for i in range(ncols)]
for todo in ['left', 'right', 'leftcb', 'rightcb']:
self.margins[todo] = [Variable(f'{sn}margins[{todo}][{i}]')
for i in range(ncols)]
for i in range(ncols):
sol.addEditVariable(self.margins[todo][i], 'strong')
for todo in ['bottom', 'top', 'bottomcb', 'topcb']:
self.margins[todo] = np.empty((nrows), dtype=object)
self.margin_vals[todo] = np.zeros(nrows)
self.heights = [Variable(f'{sn}heights[{i}]') for i in range(nrows)]
self.inner_heights = [Variable(f'{sn}inner_heights[{i}]')
for i in range(nrows)]
self.bottoms = [Variable(f'{sn}bottoms[{i}]') for i in range(nrows)]
self.tops = [Variable(f'{sn}tops[{i}]') for i in range(nrows)]
for todo in ['bottom', 'top', 'bottomcb', 'topcb']:
self.margins[todo] = [Variable(f'{sn}margins[{todo}][{i}]')
for i in range(nrows)]
for i in range(nrows):
sol.addEditVariable(self.margins[todo][i], 'strong')
# set these margins to zero by default. They will be edited as
# children are filled.
self.reset_margins()
self.add_constraints()
self.h_pad = h_pad
self.w_pad = w_pad
def __repr__(self):
str = f'LayoutBox: {self.name:25s} {self.nrows}x{self.ncols},\n'
for i in range(self.nrows):
for j in range(self.ncols):
str += f'{i}, {j}: '\
f'L({self.lefts[j].value():1.3f}, ' \
f'B{self.bottoms[i].value():1.3f}, ' \
f'W{self.widths[j].value():1.3f}, ' \
f'H{self.heights[i].value():1.3f}, ' \
f'innerW{self.inner_widths[j].value():1.3f}, ' \
f'innerH{self.inner_heights[i].value():1.3f}, ' \
f'ML{self.margins["left"][j].value():1.3f}, ' \
f'MR{self.margins["right"][j].value():1.3f}, \n'
return str
def reset_margins(self):
"""
Reset all the margins to zero. Must do this after changing
figure size, for instance, because the relative size of the
axes labels etc changes.
"""
for todo in ['left', 'right', 'bottom', 'top',
'leftcb', 'rightcb', 'bottomcb', 'topcb']:
self.edit_margins(todo, 0.0)
def add_constraints(self):
# define self-consistent constraints
self.hard_constraints()
# define relationship with parent layoutgrid:
self.parent_constraints()
# define relative widths of the grid cells to each other
# and stack horizontally and vertically.
self.grid_constraints()
def hard_constraints(self):
"""
These are the redundant constraints, plus ones that make the
rest of the code easier.
"""
for i in range(self.ncols):
hc = [self.rights[i] >= self.lefts[i],
(self.rights[i] - self.margins['right'][i] -
self.margins['rightcb'][i] >=
self.lefts[i] - self.margins['left'][i] -
self.margins['leftcb'][i])
]
for c in hc:
self.solver.addConstraint(c | 'required')
for i in range(self.nrows):
hc = [self.tops[i] >= self.bottoms[i],
(self.tops[i] - self.margins['top'][i] -
self.margins['topcb'][i] >=
self.bottoms[i] - self.margins['bottom'][i] -
self.margins['bottomcb'][i])
]
for c in hc:
self.solver.addConstraint(c | 'required')
def add_child(self, child, i=0, j=0):
self.children[i, j] = child
def parent_constraints(self):
# constraints that are due to the parent...
# i.e. the first column's left is equal to the
# parent's left, the last column right equal to the
# parent's right...
parent = self.parent
if parent is None:
hc = [self.lefts[0] == 0,
self.rights[-1] == 1,
# top and bottom reversed order...
self.tops[0] == 1,
self.bottoms[-1] == 0]
else:
rows, cols = self.parent_pos
rows = np.atleast_1d(rows)
cols = np.atleast_1d(cols)
left = parent.lefts[cols[0]]
right = parent.rights[cols[-1]]
top = parent.tops[rows[0]]
bottom = parent.bottoms[rows[-1]]
if self.parent_inner:
# the layout grid is contained inside the inner
# grid of the parent.
left += parent.margins['left'][cols[0]]
left += parent.margins['leftcb'][cols[0]]
right -= parent.margins['right'][cols[-1]]
right -= parent.margins['rightcb'][cols[-1]]
top -= parent.margins['top'][rows[0]]
top -= parent.margins['topcb'][rows[0]]
bottom += parent.margins['bottom'][rows[-1]]
bottom += parent.margins['bottomcb'][rows[-1]]
hc = [self.lefts[0] == left,
self.rights[-1] == right,
# from top to bottom
self.tops[0] == top,
self.bottoms[-1] == bottom]
for c in hc:
self.solver.addConstraint(c | 'required')
def grid_constraints(self):
# constrain the ratio of the inner part of the grids
# to be the same (relative to width_ratios)
# constrain widths:
w = (self.rights[0] - self.margins['right'][0] -
self.margins['rightcb'][0])
w = (w - self.lefts[0] - self.margins['left'][0] -
self.margins['leftcb'][0])
w0 = w / self.width_ratios[0]
# from left to right
for i in range(1, self.ncols):
w = (self.rights[i] - self.margins['right'][i] -
self.margins['rightcb'][i])
w = (w - self.lefts[i] - self.margins['left'][i] -
self.margins['leftcb'][i])
c = (w == w0 * self.width_ratios[i])
self.solver.addConstraint(c | 'strong')
# constrain the grid cells to be directly next to each other.
c = (self.rights[i - 1] == self.lefts[i])
self.solver.addConstraint(c | 'strong')
# constrain heights:
h = self.tops[0] - self.margins['top'][0] - self.margins['topcb'][0]
h = (h - self.bottoms[0] - self.margins['bottom'][0] -
self.margins['bottomcb'][0])
h0 = h / self.height_ratios[0]
# from top to bottom:
for i in range(1, self.nrows):
h = (self.tops[i] - self.margins['top'][i] -
self.margins['topcb'][i])
h = (h - self.bottoms[i] - self.margins['bottom'][i] -
self.margins['bottomcb'][i])
c = (h == h0 * self.height_ratios[i])
self.solver.addConstraint(c | 'strong')
# constrain the grid cells to be directly above each other.
c = (self.bottoms[i - 1] == self.tops[i])
self.solver.addConstraint(c | 'strong')
# Margin editing: The margins are variable and meant to
# contain things of a fixed size like axes labels, tick labels, titles
# etc
def edit_margin(self, todo, size, cell):
"""
Change the size of the margin for one cell.
Parameters
----------
todo : string (one of 'left', 'right', 'bottom', 'top')
margin to alter.
size : float
Size of the margin. If it is larger than the existing minimum it
updates the margin size. Fraction of figure size.
cell : int
Cell column or row to edit.
"""
self.solver.suggestValue(self.margins[todo][cell], size)
self.margin_vals[todo][cell] = size
def edit_margin_min(self, todo, size, cell=0):
"""
Change the minimum size of the margin for one cell.
Parameters
----------
todo : string (one of 'left', 'right', 'bottom', 'top')
margin to alter.
size : float
Minimum size of the margin . If it is larger than the
existing minimum it updates the margin size. Fraction of
figure size.
cell : int
Cell column or row to edit.
"""
if size > self.margin_vals[todo][cell]:
self.edit_margin(todo, size, cell)
def edit_margins(self, todo, size):
"""
Change the size of all the margin of all the cells in the layout grid.
Parameters
----------
todo : string (one of 'left', 'right', 'bottom', 'top')
margin to alter.
size : float
Size to set the margins. Fraction of figure size.
"""
for i in range(len(self.margin_vals[todo])):
self.edit_margin(todo, size, i)
def edit_all_margins_min(self, todo, size):
"""
Change the minimum size of all the margin of all
the cells in the layout grid.
Parameters
----------
todo : {'left', 'right', 'bottom', 'top'}
The margin to alter.
size : float
Minimum size of the margin. If it is larger than the
existing minimum it updates the margin size. Fraction of
figure size.
"""
for i in range(len(self.margin_vals[todo])):
self.edit_margin_min(todo, size, i)
def edit_outer_margin_mins(self, margin, ss):
"""
Edit all four margin minimums in one statement.
Parameters
----------
margin : dict
size of margins in a dict with keys 'left', 'right', 'bottom',
'top'
ss : SubplotSpec
defines the subplotspec these margins should be applied to
"""
self.edit_margin_min('left', margin['left'], ss.colspan.start)
self.edit_margin_min('leftcb', margin['leftcb'], ss.colspan.start)
self.edit_margin_min('right', margin['right'], ss.colspan.stop - 1)
self.edit_margin_min('rightcb', margin['rightcb'], ss.colspan.stop - 1)
# rows are from the top down:
self.edit_margin_min('top', margin['top'], ss.rowspan.start)
self.edit_margin_min('topcb', margin['topcb'], ss.rowspan.start)
self.edit_margin_min('bottom', margin['bottom'], ss.rowspan.stop - 1)
self.edit_margin_min('bottomcb', margin['bottomcb'],
ss.rowspan.stop - 1)
def get_margins(self, todo, col):
"""Return the margin at this position"""
return self.margin_vals[todo][col]
def get_outer_bbox(self, rows=0, cols=0):
"""
Return the outer bounding box of the subplot specs
given by rows and cols. rows and cols can be spans.
"""
rows = np.atleast_1d(rows)
cols = np.atleast_1d(cols)
bbox = Bbox.from_extents(
self.lefts[cols[0]].value(),
self.bottoms[rows[-1]].value(),
self.rights[cols[-1]].value(),
self.tops[rows[0]].value())
return bbox
def get_inner_bbox(self, rows=0, cols=0):
"""
Return the inner bounding box of the subplot specs
given by rows and cols. rows and cols can be spans.
"""
rows = np.atleast_1d(rows)
cols = np.atleast_1d(cols)
bbox = Bbox.from_extents(
(self.lefts[cols[0]].value() +
self.margins['left'][cols[0]].value() +
self.margins['leftcb'][cols[0]].value()),
(self.bottoms[rows[-1]].value() +
self.margins['bottom'][rows[-1]].value() +
self.margins['bottomcb'][rows[-1]].value()),
(self.rights[cols[-1]].value() -
self.margins['right'][cols[-1]].value() -
self.margins['rightcb'][cols[-1]].value()),
(self.tops[rows[0]].value() -
self.margins['top'][rows[0]].value() -
self.margins['topcb'][rows[0]].value())
)
return bbox
def get_bbox_for_cb(self, rows=0, cols=0):
"""
Return the bounding box that includes the
decorations but, *not* the colorbar...
"""
rows = np.atleast_1d(rows)
cols = np.atleast_1d(cols)
bbox = Bbox.from_extents(
(self.lefts[cols[0]].value() +
self.margins['leftcb'][cols[0]].value()),
(self.bottoms[rows[-1]].value() +
self.margins['bottomcb'][rows[-1]].value()),
(self.rights[cols[-1]].value() -
self.margins['rightcb'][cols[-1]].value()),
(self.tops[rows[0]].value() -
self.margins['topcb'][rows[0]].value())
)
return bbox
def get_left_margin_bbox(self, rows=0, cols=0):
"""
Return the left margin bounding box of the subplot specs
given by rows and cols. rows and cols can be spans.
"""
rows = np.atleast_1d(rows)
cols = np.atleast_1d(cols)
bbox = Bbox.from_extents(
(self.lefts[cols[0]].value() +
self.margins['leftcb'][cols[0]].value()),
(self.bottoms[rows[-1]].value()),
(self.lefts[cols[0]].value() +
self.margins['leftcb'][cols[0]].value() +
self.margins['left'][cols[0]].value()),
(self.tops[rows[0]].value()))
return bbox
def get_bottom_margin_bbox(self, rows=0, cols=0):
"""
Return the left margin bounding box of the subplot specs
given by rows and cols. rows and cols can be spans.
"""
rows = np.atleast_1d(rows)
cols = np.atleast_1d(cols)
bbox = Bbox.from_extents(
(self.lefts[cols[0]].value()),
(self.bottoms[rows[-1]].value() +
self.margins['bottomcb'][rows[-1]].value()),
(self.rights[cols[-1]].value()),
(self.bottoms[rows[-1]].value() +
self.margins['bottom'][rows[-1]].value() +
self.margins['bottomcb'][rows[-1]].value()
))
return bbox
def get_right_margin_bbox(self, rows=0, cols=0):
"""
Return the left margin bounding box of the subplot specs
given by rows and cols. rows and cols can be spans.
"""
rows = np.atleast_1d(rows)
cols = np.atleast_1d(cols)
bbox = Bbox.from_extents(
(self.rights[cols[-1]].value() -
self.margins['right'][cols[-1]].value() -
self.margins['rightcb'][cols[-1]].value()),
(self.bottoms[rows[-1]].value()),
(self.rights[cols[-1]].value() -
self.margins['rightcb'][cols[-1]].value()),
(self.tops[rows[0]].value()))
return bbox
def get_top_margin_bbox(self, rows=0, cols=0):
"""
Return the left margin bounding box of the subplot specs
given by rows and cols. rows and cols can be spans.
"""
rows = np.atleast_1d(rows)
cols = np.atleast_1d(cols)
bbox = Bbox.from_extents(
(self.lefts[cols[0]].value()),
(self.tops[rows[0]].value() -
self.margins['topcb'][rows[0]].value()),
(self.rights[cols[-1]].value()),
(self.tops[rows[0]].value() -
self.margins['topcb'][rows[0]].value() -
self.margins['top'][rows[0]].value()))
return bbox
def update_variables(self):
"""
Update the variables for the solver attached to this layoutgrid.
"""
self.solver.updateVariables()
_layoutboxobjnum = itertools.count()
def seq_id():
"""Generate a short sequential id for layoutbox objects."""
return '%06d' % next(_layoutboxobjnum)
def print_children(lb):
"""Print the children of the layoutbox."""
for child in lb.children:
print_children(child)
def plot_children(fig, lg, level=0, printit=False):
"""Simple plotting to show where boxes are."""
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
fig.canvas.draw()
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
col = colors[level]
for i in range(lg.nrows):
for j in range(lg.ncols):
bb = lg.get_outer_bbox(rows=i, cols=j)
fig.add_artist(
mpatches.Rectangle(bb.p0, bb.width, bb.height, linewidth=1,
edgecolor='0.7', facecolor='0.7',
alpha=0.2, transform=fig.transFigure,
zorder=-3))
bbi = lg.get_inner_bbox(rows=i, cols=j)
fig.add_artist(
mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=2,
edgecolor=col, facecolor='none',
transform=fig.transFigure, zorder=-2))
bbi = lg.get_left_margin_bbox(rows=i, cols=j)
fig.add_artist(
mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=0,
edgecolor='none', alpha=0.2,
facecolor=[0.5, 0.7, 0.5],
transform=fig.transFigure, zorder=-2))
bbi = lg.get_right_margin_bbox(rows=i, cols=j)
fig.add_artist(
mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=0,
edgecolor='none', alpha=0.2,
facecolor=[0.7, 0.5, 0.5],
transform=fig.transFigure, zorder=-2))
bbi = lg.get_bottom_margin_bbox(rows=i, cols=j)
fig.add_artist(
mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=0,
edgecolor='none', alpha=0.2,
facecolor=[0.5, 0.5, 0.7],
transform=fig.transFigure, zorder=-2))
bbi = lg.get_top_margin_bbox(rows=i, cols=j)
fig.add_artist(
mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=0,
edgecolor='none', alpha=0.2,
facecolor=[0.7, 0.2, 0.7],
transform=fig.transFigure, zorder=-2))
for ch in lg.children.flat:
if ch is not None:
plot_children(fig, ch, level=level+1)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,140 @@
"""
Manage figures for the pyplot interface.
"""
import atexit
from collections import OrderedDict
import gc
class Gcf:
"""
Singleton to maintain the relation between figures and their managers, and
keep track of and "active" figure and manager.
The canvas of a figure created through pyplot is associated with a figure
manager, which handles the interaction between the figure and the backend.
pyplot keeps track of figure managers using an identifier, the "figure
number" or "manager number" (which can actually be any hashable value);
this number is available as the :attr:`number` attribute of the manager.
This class is never instantiated; it consists of an `OrderedDict` mapping
figure/manager numbers to managers, and a set of class methods that
manipulate this `OrderedDict`.
Attributes
----------
figs : OrderedDict
`OrderedDict` mapping numbers to managers; the active manager is at the
end.
"""
figs = OrderedDict()
@classmethod
def get_fig_manager(cls, num):
"""
If manager number *num* exists, make it the active one and return it;
otherwise return *None*.
"""
manager = cls.figs.get(num, None)
if manager is not None:
cls.set_active(manager)
return manager
@classmethod
def destroy(cls, num):
"""
Destroy manager *num* -- either a manager instance or a manager number.
In the interactive backends, this is bound to the window "destroy" and
"delete" events.
It is recommended to pass a manager instance, to avoid confusion when
two managers share the same number.
"""
if all(hasattr(num, attr) for attr in ["num", "destroy"]):
manager = num
if cls.figs.get(manager.num) is manager:
cls.figs.pop(manager.num)
else:
try:
manager = cls.figs.pop(num)
except KeyError:
return
if hasattr(manager, "_cidgcf"):
manager.canvas.mpl_disconnect(manager._cidgcf)
manager.destroy()
gc.collect(1)
@classmethod
def destroy_fig(cls, fig):
"""Destroy figure *fig*."""
num = next((manager.num for manager in cls.figs.values()
if manager.canvas.figure == fig), None)
if num is not None:
cls.destroy(num)
@classmethod
def destroy_all(cls):
"""Destroy all figures."""
# Reimport gc in case the module globals have already been removed
# during interpreter shutdown.
import gc
for manager in list(cls.figs.values()):
manager.canvas.mpl_disconnect(manager._cidgcf)
manager.destroy()
cls.figs.clear()
gc.collect(1)
@classmethod
def has_fignum(cls, num):
"""Return whether figure number *num* exists."""
return num in cls.figs
@classmethod
def get_all_fig_managers(cls):
"""Return a list of figure managers."""
return list(cls.figs.values())
@classmethod
def get_num_fig_managers(cls):
"""Return the number of figures being managed."""
return len(cls.figs)
@classmethod
def get_active(cls):
"""Return the active manager, or *None* if there is no manager."""
return next(reversed(cls.figs.values())) if cls.figs else None
@classmethod
def _set_new_active_manager(cls, manager):
"""Adopt *manager* into pyplot and make it the active manager."""
if not hasattr(manager, "_cidgcf"):
manager._cidgcf = manager.canvas.mpl_connect(
"button_press_event", lambda event: cls.set_active(manager))
fig = manager.canvas.figure
fig.number = manager.num
label = fig.get_label()
if label:
manager.set_window_title(label)
cls.set_active(manager)
@classmethod
def set_active(cls, manager):
"""Make *manager* the active manager."""
cls.figs[manager.num] = manager
cls.figs.move_to_end(manager.num)
@classmethod
def draw_all(cls, force=False):
"""
Redraw all stale managed figures, or, if *force* is True, all managed
figures.
"""
for manager in cls.get_all_fig_managers():
if force or manager.canvas.figure.stale:
manager.canvas.draw_idle()
atexit.register(Gcf.destroy_all)

View File

@@ -0,0 +1,44 @@
"""
Text layouting utilities.
"""
import dataclasses
from .ft2font import KERNING_DEFAULT, LOAD_NO_HINTING
LayoutItem = dataclasses.make_dataclass(
"LayoutItem", ["char", "glyph_idx", "x", "prev_kern"])
def layout(string, font, *, kern_mode=KERNING_DEFAULT):
"""
Render *string* with *font*. For each character in *string*, yield a
(glyph-index, x-position) pair. When such a pair is yielded, the font's
glyph is set to the corresponding character.
Parameters
----------
string : str
The string to be rendered.
font : FT2Font
The font.
kern_mode : int
A FreeType kerning mode.
Yields
------
glyph_index : int
x_position : float
"""
x = 0
prev_glyph_idx = None
for char in string:
glyph_idx = font.get_char_index(ord(char))
kern = (font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64
if prev_glyph_idx is not None else 0.)
x += kern
glyph = font.load_glyph(glyph_idx, flags=LOAD_NO_HINTING)
yield LayoutItem(char, glyph_idx, x, kern)
x += glyph.linearHoriAdvance / 65536
prev_glyph_idx = glyph_idx

View File

@@ -0,0 +1,21 @@
# This file was generated by 'versioneer.py' (0.15) from
# revision-control system data, or from the parent directory name of an
# unpacked source archive. Distribution tarballs contain a pre-generated copy
# of this file.
import json
import sys
version_json = '''
{
"dirty": false,
"error": null,
"full-revisionid": "919145fe9849c999aa491457c6de6faede5959c3",
"version": "3.4.3"
}
''' # END VERSION_JSON
def get_versions():
return json.loads(version_json)

View File

@@ -0,0 +1,532 @@
"""
A python interface to Adobe Font Metrics Files.
Although a number of other python implementations exist, and may be more
complete than this, it was decided not to go with them because they were
either:
1) copyrighted or used a non-BSD compatible license
2) had too many dependencies and a free standing lib was needed
3) did more than needed and it was easier to write afresh rather than
figure out how to get just what was needed.
It is pretty easy to use, and has no external dependencies:
>>> import matplotlib as mpl
>>> from pathlib import Path
>>> afm_path = Path(mpl.get_data_path(), 'fonts', 'afm', 'ptmr8a.afm')
>>>
>>> from matplotlib.afm import AFM
>>> with afm_path.open('rb') as fh:
... afm = AFM(fh)
>>> afm.string_width_height('What the heck?')
(6220.0, 694)
>>> afm.get_fontname()
'Times-Roman'
>>> afm.get_kern_dist('A', 'f')
0
>>> afm.get_kern_dist('A', 'y')
-92.0
>>> afm.get_bbox_char('!')
[130, -9, 238, 676]
As in the Adobe Font Metrics File Format Specification, all dimensions
are given in units of 1/1000 of the scale factor (point size) of the font
being used.
"""
from collections import namedtuple
import logging
import re
from ._mathtext_data import uni2type1
_log = logging.getLogger(__name__)
def _to_int(x):
# Some AFM files have floats where we are expecting ints -- there is
# probably a better way to handle this (support floats, round rather than
# truncate). But I don't know what the best approach is now and this
# change to _to_int should at least prevent Matplotlib from crashing on
# these. JDH (2009-11-06)
return int(float(x))
def _to_float(x):
# Some AFM files use "," instead of "." as decimal separator -- this
# shouldn't be ambiguous (unless someone is wicked enough to use "," as
# thousands separator...).
if isinstance(x, bytes):
# Encoding doesn't really matter -- if we have codepoints >127 the call
# to float() will error anyways.
x = x.decode('latin-1')
return float(x.replace(',', '.'))
def _to_str(x):
return x.decode('utf8')
def _to_list_of_ints(s):
s = s.replace(b',', b' ')
return [_to_int(val) for val in s.split()]
def _to_list_of_floats(s):
return [_to_float(val) for val in s.split()]
def _to_bool(s):
if s.lower().strip() in (b'false', b'0', b'no'):
return False
else:
return True
def _parse_header(fh):
"""
Read the font metrics header (up to the char metrics) and returns
a dictionary mapping *key* to *val*. *val* will be converted to the
appropriate python type as necessary; e.g.:
* 'False'->False
* '0'->0
* '-168 -218 1000 898'-> [-168, -218, 1000, 898]
Dictionary keys are
StartFontMetrics, FontName, FullName, FamilyName, Weight,
ItalicAngle, IsFixedPitch, FontBBox, UnderlinePosition,
UnderlineThickness, Version, Notice, EncodingScheme, CapHeight,
XHeight, Ascender, Descender, StartCharMetrics
"""
header_converters = {
b'StartFontMetrics': _to_float,
b'FontName': _to_str,
b'FullName': _to_str,
b'FamilyName': _to_str,
b'Weight': _to_str,
b'ItalicAngle': _to_float,
b'IsFixedPitch': _to_bool,
b'FontBBox': _to_list_of_ints,
b'UnderlinePosition': _to_float,
b'UnderlineThickness': _to_float,
b'Version': _to_str,
# Some AFM files have non-ASCII characters (which are not allowed by
# the spec). Given that there is actually no public API to even access
# this field, just return it as straight bytes.
b'Notice': lambda x: x,
b'EncodingScheme': _to_str,
b'CapHeight': _to_float, # Is the second version a mistake, or
b'Capheight': _to_float, # do some AFM files contain 'Capheight'? -JKS
b'XHeight': _to_float,
b'Ascender': _to_float,
b'Descender': _to_float,
b'StdHW': _to_float,
b'StdVW': _to_float,
b'StartCharMetrics': _to_int,
b'CharacterSet': _to_str,
b'Characters': _to_int,
}
d = {}
first_line = True
for line in fh:
line = line.rstrip()
if line.startswith(b'Comment'):
continue
lst = line.split(b' ', 1)
key = lst[0]
if first_line:
# AFM spec, Section 4: The StartFontMetrics keyword
# [followed by a version number] must be the first line in
# the file, and the EndFontMetrics keyword must be the
# last non-empty line in the file. We just check the
# first header entry.
if key != b'StartFontMetrics':
raise RuntimeError('Not an AFM file')
first_line = False
if len(lst) == 2:
val = lst[1]
else:
val = b''
try:
converter = header_converters[key]
except KeyError:
_log.error('Found an unknown keyword in AFM header (was %r)' % key)
continue
try:
d[key] = converter(val)
except ValueError:
_log.error('Value error parsing header in AFM: %s, %s', key, val)
continue
if key == b'StartCharMetrics':
break
else:
raise RuntimeError('Bad parse')
return d
CharMetrics = namedtuple('CharMetrics', 'width, name, bbox')
CharMetrics.__doc__ = """
Represents the character metrics of a single character.
Notes
-----
The fields do currently only describe a subset of character metrics
information defined in the AFM standard.
"""
CharMetrics.width.__doc__ = """The character width (WX)."""
CharMetrics.name.__doc__ = """The character name (N)."""
CharMetrics.bbox.__doc__ = """
The bbox of the character (B) as a tuple (*llx*, *lly*, *urx*, *ury*)."""
def _parse_char_metrics(fh):
"""
Parse the given filehandle for character metrics information and return
the information as dicts.
It is assumed that the file cursor is on the line behind
'StartCharMetrics'.
Returns
-------
ascii_d : dict
A mapping "ASCII num of the character" to `.CharMetrics`.
name_d : dict
A mapping "character name" to `.CharMetrics`.
Notes
-----
This function is incomplete per the standard, but thus far parses
all the sample afm files tried.
"""
required_keys = {'C', 'WX', 'N', 'B'}
ascii_d = {}
name_d = {}
for line in fh:
# We are defensively letting values be utf8. The spec requires
# ascii, but there are non-compliant fonts in circulation
line = _to_str(line.rstrip()) # Convert from byte-literal
if line.startswith('EndCharMetrics'):
return ascii_d, name_d
# Split the metric line into a dictionary, keyed by metric identifiers
vals = dict(s.strip().split(' ', 1) for s in line.split(';') if s)
# There may be other metrics present, but only these are needed
if not required_keys.issubset(vals):
raise RuntimeError('Bad char metrics line: %s' % line)
num = _to_int(vals['C'])
wx = _to_float(vals['WX'])
name = vals['N']
bbox = _to_list_of_floats(vals['B'])
bbox = list(map(int, bbox))
metrics = CharMetrics(wx, name, bbox)
# Workaround: If the character name is 'Euro', give it the
# corresponding character code, according to WinAnsiEncoding (see PDF
# Reference).
if name == 'Euro':
num = 128
elif name == 'minus':
num = ord("\N{MINUS SIGN}") # 0x2212
if num != -1:
ascii_d[num] = metrics
name_d[name] = metrics
raise RuntimeError('Bad parse')
def _parse_kern_pairs(fh):
"""
Return a kern pairs dictionary; keys are (*char1*, *char2*) tuples and
values are the kern pair value. For example, a kern pairs line like
``KPX A y -50``
will be represented as::
d[ ('A', 'y') ] = -50
"""
line = next(fh)
if not line.startswith(b'StartKernPairs'):
raise RuntimeError('Bad start of kern pairs data: %s' % line)
d = {}
for line in fh:
line = line.rstrip()
if not line:
continue
if line.startswith(b'EndKernPairs'):
next(fh) # EndKernData
return d
vals = line.split()
if len(vals) != 4 or vals[0] != b'KPX':
raise RuntimeError('Bad kern pairs line: %s' % line)
c1, c2, val = _to_str(vals[1]), _to_str(vals[2]), _to_float(vals[3])
d[(c1, c2)] = val
raise RuntimeError('Bad kern pairs parse')
CompositePart = namedtuple('CompositePart', 'name, dx, dy')
CompositePart.__doc__ = """
Represents the information on a composite element of a composite char."""
CompositePart.name.__doc__ = """Name of the part, e.g. 'acute'."""
CompositePart.dx.__doc__ = """x-displacement of the part from the origin."""
CompositePart.dy.__doc__ = """y-displacement of the part from the origin."""
def _parse_composites(fh):
"""
Parse the given filehandle for composites information return them as a
dict.
It is assumed that the file cursor is on the line behind 'StartComposites'.
Returns
-------
dict
A dict mapping composite character names to a parts list. The parts
list is a list of `.CompositePart` entries describing the parts of
the composite.
Examples
--------
A composite definition line::
CC Aacute 2 ; PCC A 0 0 ; PCC acute 160 170 ;
will be represented as::
composites['Aacute'] = [CompositePart(name='A', dx=0, dy=0),
CompositePart(name='acute', dx=160, dy=170)]
"""
composites = {}
for line in fh:
line = line.rstrip()
if not line:
continue
if line.startswith(b'EndComposites'):
return composites
vals = line.split(b';')
cc = vals[0].split()
name, numParts = cc[1], _to_int(cc[2])
pccParts = []
for s in vals[1:-1]:
pcc = s.split()
part = CompositePart(pcc[1], _to_float(pcc[2]), _to_float(pcc[3]))
pccParts.append(part)
composites[name] = pccParts
raise RuntimeError('Bad composites parse')
def _parse_optional(fh):
"""
Parse the optional fields for kern pair data and composites.
Returns
-------
kern_data : dict
A dict containing kerning information. May be empty.
See `._parse_kern_pairs`.
composites : dict
A dict containing composite information. May be empty.
See `._parse_composites`.
"""
optional = {
b'StartKernData': _parse_kern_pairs,
b'StartComposites': _parse_composites,
}
d = {b'StartKernData': {},
b'StartComposites': {}}
for line in fh:
line = line.rstrip()
if not line:
continue
key = line.split()[0]
if key in optional:
d[key] = optional[key](fh)
return d[b'StartKernData'], d[b'StartComposites']
class AFM:
def __init__(self, fh):
"""Parse the AFM file in file object *fh*."""
self._header = _parse_header(fh)
self._metrics, self._metrics_by_name = _parse_char_metrics(fh)
self._kern, self._composite = _parse_optional(fh)
def get_bbox_char(self, c, isord=False):
if not isord:
c = ord(c)
return self._metrics[c].bbox
def string_width_height(self, s):
"""
Return the string width (including kerning) and string height
as a (*w*, *h*) tuple.
"""
if not len(s):
return 0, 0
total_width = 0
namelast = None
miny = 1e9
maxy = 0
for c in s:
if c == '\n':
continue
wx, name, bbox = self._metrics[ord(c)]
total_width += wx + self._kern.get((namelast, name), 0)
l, b, w, h = bbox
miny = min(miny, b)
maxy = max(maxy, b + h)
namelast = name
return total_width, maxy - miny
def get_str_bbox_and_descent(self, s):
"""Return the string bounding box and the maximal descent."""
if not len(s):
return 0, 0, 0, 0, 0
total_width = 0
namelast = None
miny = 1e9
maxy = 0
left = 0
if not isinstance(s, str):
s = _to_str(s)
for c in s:
if c == '\n':
continue
name = uni2type1.get(ord(c), f"uni{ord(c):04X}")
try:
wx, _, bbox = self._metrics_by_name[name]
except KeyError:
name = 'question'
wx, _, bbox = self._metrics_by_name[name]
total_width += wx + self._kern.get((namelast, name), 0)
l, b, w, h = bbox
left = min(left, l)
miny = min(miny, b)
maxy = max(maxy, b + h)
namelast = name
return left, miny, total_width, maxy - miny, -miny
def get_str_bbox(self, s):
"""Return the string bounding box."""
return self.get_str_bbox_and_descent(s)[:4]
def get_name_char(self, c, isord=False):
"""Get the name of the character, i.e., ';' is 'semicolon'."""
if not isord:
c = ord(c)
return self._metrics[c].name
def get_width_char(self, c, isord=False):
"""
Get the width of the character from the character metric WX field.
"""
if not isord:
c = ord(c)
return self._metrics[c].width
def get_width_from_char_name(self, name):
"""Get the width of the character from a type1 character name."""
return self._metrics_by_name[name].width
def get_height_char(self, c, isord=False):
"""Get the bounding box (ink) height of character *c* (space is 0)."""
if not isord:
c = ord(c)
return self._metrics[c].bbox[-1]
def get_kern_dist(self, c1, c2):
"""
Return the kerning pair distance (possibly 0) for chars *c1* and *c2*.
"""
name1, name2 = self.get_name_char(c1), self.get_name_char(c2)
return self.get_kern_dist_from_name(name1, name2)
def get_kern_dist_from_name(self, name1, name2):
"""
Return the kerning pair distance (possibly 0) for chars
*name1* and *name2*.
"""
return self._kern.get((name1, name2), 0)
def get_fontname(self):
"""Return the font name, e.g., 'Times-Roman'."""
return self._header[b'FontName']
@property
def postscript_name(self): # For consistency with FT2Font.
return self.get_fontname()
def get_fullname(self):
"""Return the font full name, e.g., 'Times-Roman'."""
name = self._header.get(b'FullName')
if name is None: # use FontName as a substitute
name = self._header[b'FontName']
return name
def get_familyname(self):
"""Return the font family name, e.g., 'Times'."""
name = self._header.get(b'FamilyName')
if name is not None:
return name
# FamilyName not specified so we'll make a guess
name = self.get_fullname()
extras = (r'(?i)([ -](regular|plain|italic|oblique|bold|semibold|'
r'light|ultralight|extra|condensed))+$')
return re.sub(extras, '', name)
@property
def family_name(self):
"""The font family name, e.g., 'Times'."""
return self.get_familyname()
def get_weight(self):
"""Return the font weight, e.g., 'Bold' or 'Roman'."""
return self._header[b'Weight']
def get_angle(self):
"""Return the fontangle as float."""
return self._header[b'ItalicAngle']
def get_capheight(self):
"""Return the cap height as float."""
return self._header[b'CapHeight']
def get_xheight(self):
"""Return the xheight as float."""
return self._header[b'XHeight']
def get_underline_thickness(self):
"""Return the underline thickness as float."""
return self._header[b'UnderlineThickness']
def get_horizontal_stem_width(self):
"""
Return the standard horizontal stem width as float, or *None* if
not specified in AFM file.
"""
return self._header.get(b'StdHW', None)
def get_vertical_stem_width(self):
"""
Return the standard vertical stem width as float, or *None* if
not specified in AFM file.
"""
return self._header.get(b'StdVW', None)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
from ._subplots import *
from ._axes import *

Some files were not shown because too many files have changed in this diff Show More