Delete .venv directory
This commit is contained in:
committed by
GitHub
parent
7795984d81
commit
5a2693bd9f
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,262 +0,0 @@
|
||||
# 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}";
|
||||
}}
|
||||
"""
|
@@ -1,213 +0,0 @@
|
||||
"""
|
||||
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)
|
Binary file not shown.
Binary file not shown.
@@ -1,522 +0,0 @@
|
||||
"""
|
||||
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
|
Binary file not shown.
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
@@ -1,619 +0,0 @@
|
||||
"""
|
||||
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
|
Binary file not shown.
@@ -1,208 +0,0 @@
|
||||
"""
|
||||
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})
|
Binary file not shown.
@@ -1,64 +0,0 @@
|
||||
"""
|
||||
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)
|
@@ -1,560 +0,0 @@
|
||||
"""
|
||||
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
Binary file not shown.
@@ -1,140 +0,0 @@
|
||||
"""
|
||||
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)
|
Binary file not shown.
@@ -1,44 +0,0 @@
|
||||
"""
|
||||
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
|
Binary file not shown.
Binary file not shown.
@@ -1,21 +0,0 @@
|
||||
|
||||
# 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)
|
@@ -1,532 +0,0 @@
|
||||
"""
|
||||
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
@@ -1,2 +0,0 @@
|
||||
from ._subplots import *
|
||||
from ._axes import *
|
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user