I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,115 @@
__all__ = [
"__bibtex__",
"__version__",
"__version_info__",
"set_loglevel",
"ExecutableNotFoundError",
"get_configdir",
"get_cachedir",
"get_data_path",
"matplotlib_fname",
"MatplotlibDeprecationWarning",
"RcParams",
"rc_params",
"rc_params_from_file",
"rcParamsDefault",
"rcParams",
"rcParamsOrig",
"defaultParams",
"rc",
"rcdefaults",
"rc_file_defaults",
"rc_file",
"rc_context",
"use",
"get_backend",
"interactive",
"is_interactive",
"colormaps",
"color_sequences",
]
import os
from pathlib import Path
from collections.abc import Callable, Generator
import contextlib
from packaging.version import Version
from matplotlib._api import MatplotlibDeprecationWarning
from typing import Any, NamedTuple
class _VersionInfo(NamedTuple):
major: int
minor: int
micro: int
releaselevel: str
serial: int
__bibtex__: str
__version__: str
__version_info__: _VersionInfo
def set_loglevel(level: str) -> None: ...
class _ExecInfo(NamedTuple):
executable: str
raw_version: str
version: Version
class ExecutableNotFoundError(FileNotFoundError): ...
def _get_executable_info(name: str) -> _ExecInfo: ...
def get_configdir() -> str: ...
def get_cachedir() -> str: ...
def get_data_path() -> str: ...
def matplotlib_fname() -> str: ...
class RcParams(dict[str, Any]):
validate: dict[str, Callable]
def __init__(self, *args, **kwargs) -> None: ...
def _set(self, key: str, val: Any) -> None: ...
def _get(self, key: str) -> Any: ...
def __setitem__(self, key: str, val: Any) -> None: ...
def __getitem__(self, key: str) -> Any: ...
def __iter__(self) -> Generator[str, None, None]: ...
def __len__(self) -> int: ...
def find_all(self, pattern: str) -> RcParams: ...
def copy(self) -> RcParams: ...
def rc_params(fail_on_error: bool = ...) -> RcParams: ...
def rc_params_from_file(
fname: str | Path | os.PathLike,
fail_on_error: bool = ...,
use_default_template: bool = ...,
) -> RcParams: ...
rcParamsDefault: RcParams
rcParams: RcParams
rcParamsOrig: RcParams
defaultParams: dict[str, Any]
def rc(group: str, **kwargs) -> None: ...
def rcdefaults() -> None: ...
def rc_file_defaults() -> None: ...
def rc_file(
fname: str | Path | os.PathLike, *, use_default_template: bool = ...
) -> None: ...
@contextlib.contextmanager
def rc_context(
rc: dict[str, Any] | None = ..., fname: str | Path | os.PathLike | None = ...
) -> Generator[None, None, None]: ...
def use(backend: str, *, force: bool = ...) -> None: ...
def get_backend() -> str: ...
def interactive(b: bool) -> None: ...
def is_interactive() -> bool: ...
def _preprocess_data(
func: Callable | None = ...,
*,
replace_names: list[str] | None = ...,
label_namer: str | None = ...
) -> Callable: ...
from matplotlib.cm import _colormaps as colormaps
from matplotlib.colors import _color_sequences as color_sequences

View File

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

View File

@ -0,0 +1,262 @@
# JavaScript template for HTMLWriter
JS_INCLUDE = """
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<script language="javascript">
function isInternetExplorer() {
ua = navigator.userAgent;
/* MSIE used to detect old browsers and Trident used to newer ones*/
return ua.indexOf("MSIE ") > -1 || ua.indexOf("Trident/") > -1;
}
/* Define the Animation class */
function Animation(frames, img_id, slider_id, interval, loop_select_id){
this.img_id = img_id;
this.slider_id = slider_id;
this.loop_select_id = loop_select_id;
this.interval = interval;
this.current_frame = 0;
this.direction = 0;
this.timer = null;
this.frames = new Array(frames.length);
for (var i=0; i<frames.length; i++)
{
this.frames[i] = new Image();
this.frames[i].src = frames[i];
}
var slider = document.getElementById(this.slider_id);
slider.max = this.frames.length - 1;
if (isInternetExplorer()) {
// switch from oninput to onchange because IE <= 11 does not conform
// with W3C specification. It ignores oninput and onchange behaves
// like oninput. In contrast, Microsoft Edge behaves correctly.
slider.setAttribute('onchange', slider.getAttribute('oninput'));
slider.setAttribute('oninput', null);
}
this.set_frame(this.current_frame);
}
Animation.prototype.get_loop_state = function(){
var button_group = document[this.loop_select_id].state;
for (var i = 0; i < button_group.length; i++) {
var button = button_group[i];
if (button.checked) {
return button.value;
}
}
return undefined;
}
Animation.prototype.set_frame = function(frame){
this.current_frame = frame;
document.getElementById(this.img_id).src =
this.frames[this.current_frame].src;
document.getElementById(this.slider_id).value = this.current_frame;
}
Animation.prototype.next_frame = function()
{
this.set_frame(Math.min(this.frames.length - 1, this.current_frame + 1));
}
Animation.prototype.previous_frame = function()
{
this.set_frame(Math.max(0, this.current_frame - 1));
}
Animation.prototype.first_frame = function()
{
this.set_frame(0);
}
Animation.prototype.last_frame = function()
{
this.set_frame(this.frames.length - 1);
}
Animation.prototype.slower = function()
{
this.interval /= 0.7;
if(this.direction > 0){this.play_animation();}
else if(this.direction < 0){this.reverse_animation();}
}
Animation.prototype.faster = function()
{
this.interval *= 0.7;
if(this.direction > 0){this.play_animation();}
else if(this.direction < 0){this.reverse_animation();}
}
Animation.prototype.anim_step_forward = function()
{
this.current_frame += 1;
if(this.current_frame < this.frames.length){
this.set_frame(this.current_frame);
}else{
var loop_state = this.get_loop_state();
if(loop_state == "loop"){
this.first_frame();
}else if(loop_state == "reflect"){
this.last_frame();
this.reverse_animation();
}else{
this.pause_animation();
this.last_frame();
}
}
}
Animation.prototype.anim_step_reverse = function()
{
this.current_frame -= 1;
if(this.current_frame >= 0){
this.set_frame(this.current_frame);
}else{
var loop_state = this.get_loop_state();
if(loop_state == "loop"){
this.last_frame();
}else if(loop_state == "reflect"){
this.first_frame();
this.play_animation();
}else{
this.pause_animation();
this.first_frame();
}
}
}
Animation.prototype.pause_animation = function()
{
this.direction = 0;
if (this.timer){
clearInterval(this.timer);
this.timer = null;
}
}
Animation.prototype.play_animation = function()
{
this.pause_animation();
this.direction = 1;
var t = this;
if (!this.timer) this.timer = setInterval(function() {
t.anim_step_forward();
}, this.interval);
}
Animation.prototype.reverse_animation = function()
{
this.pause_animation();
this.direction = -1;
var t = this;
if (!this.timer) this.timer = setInterval(function() {
t.anim_step_reverse();
}, this.interval);
}
</script>
"""
# Style definitions for the HTML template
STYLE_INCLUDE = """
<style>
.animation {
display: inline-block;
text-align: center;
}
input[type=range].anim-slider {
width: 374px;
margin-left: auto;
margin-right: auto;
}
.anim-buttons {
margin: 8px 0px;
}
.anim-buttons button {
padding: 0;
width: 36px;
}
.anim-state label {
margin-right: 8px;
}
.anim-state input {
margin: 0;
vertical-align: middle;
}
</style>
"""
# HTML template for HTMLWriter
DISPLAY_TEMPLATE = """
<div class="animation">
<img id="_anim_img{id}">
<div class="anim-controls">
<input id="_anim_slider{id}" type="range" class="anim-slider"
name="points" min="0" max="1" step="1" value="0"
oninput="anim{id}.set_frame(parseInt(this.value));">
<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>
""" # noqa: E501
INCLUDED_FRAMES = """
for (var i=0; i<{Nframes}; i++){{
frames[i] = "{frame_dir}/frame" + ("0000000" + i).slice(-7) +
".{frame_format}";
}}
"""

View File

@ -0,0 +1,381 @@
"""
Helper functions for managing the Matplotlib API.
This documentation is only relevant for Matplotlib developers, not for users.
.. warning::
This module and its submodules are for internal use only. Do not use them
in your own code. We may change the API at any time with no warning.
"""
import functools
import itertools
import re
import sys
import warnings
from .deprecation import ( # noqa: F401
deprecated, warn_deprecated,
rename_parameter, delete_parameter, make_keyword_only,
deprecate_method_override, deprecate_privatize_attribute,
suppress_matplotlib_deprecation_warning,
MatplotlibDeprecationWarning)
class classproperty:
"""
Like `property`, but also triggers on access via the class, and it is the
*class* that's passed as argument.
Examples
--------
::
class C:
@classproperty
def foo(cls):
return cls.__name__
assert C.foo == "C"
"""
def __init__(self, fget, fset=None, fdel=None, doc=None):
self._fget = fget
if fset is not None or fdel is not None:
raise ValueError('classproperty only implements fget.')
self.fset = fset
self.fdel = fdel
# docs are ignored for now
self._doc = doc
def __get__(self, instance, owner):
return self._fget(owner)
@property
def fget(self):
return self._fget
# In the following check_foo() functions, the first parameter is positional-only to make
# e.g. `_api.check_isinstance([...], types=foo)` work.
def check_isinstance(types, /, **kwargs):
"""
For each *key, value* pair in *kwargs*, check that *value* is an instance
of one of *types*; if not, raise an appropriate TypeError.
As a special case, a ``None`` entry in *types* is treated as NoneType.
Examples
--------
>>> _api.check_isinstance((SomeClass, None), arg=arg)
"""
none_type = type(None)
types = ((types,) if isinstance(types, type) else
(none_type,) if types is None else
tuple(none_type if tp is None else tp for tp in types))
def type_name(tp):
return ("None" if tp is none_type
else tp.__qualname__ if tp.__module__ == "builtins"
else f"{tp.__module__}.{tp.__qualname__}")
for k, v in kwargs.items():
if not isinstance(v, types):
names = [*map(type_name, types)]
if "None" in names: # Move it to the end for better wording.
names.remove("None")
names.append("None")
raise TypeError(
"{!r} must be an instance of {}, not a {}".format(
k,
", ".join(names[:-1]) + " or " + names[-1]
if len(names) > 1 else names[0],
type_name(type(v))))
def check_in_list(values, /, *, _print_supported_values=True, **kwargs):
"""
For each *key, value* pair in *kwargs*, check that *value* is in *values*;
if not, raise an appropriate ValueError.
Parameters
----------
values : iterable
Sequence of values to check on.
_print_supported_values : bool, default: True
Whether to print *values* when raising ValueError.
**kwargs : dict
*key, value* pairs as keyword arguments to find in *values*.
Raises
------
ValueError
If any *value* in *kwargs* is not found in *values*.
Examples
--------
>>> _api.check_in_list(["foo", "bar"], arg=arg, other_arg=other_arg)
"""
if not kwargs:
raise TypeError("No argument to check!")
for key, val in kwargs.items():
if val not in values:
msg = f"{val!r} is not a valid value for {key}"
if _print_supported_values:
msg += f"; supported values are {', '.join(map(repr, values))}"
raise ValueError(msg)
def check_shape(shape, /, **kwargs):
"""
For each *key, value* pair in *kwargs*, check that *value* has the shape *shape*;
if not, raise an appropriate ValueError.
*None* in the shape is treated as a "free" size that can have any length.
e.g. (None, 2) -> (N, 2)
The values checked must be numpy arrays.
Examples
--------
To check for (N, 2) shaped arrays
>>> _api.check_shape((None, 2), arg=arg, other_arg=other_arg)
"""
for k, v in kwargs.items():
data_shape = v.shape
if (len(data_shape) != len(shape)
or any(s != t and t is not None for s, t in zip(data_shape, shape))):
dim_labels = iter(itertools.chain(
'NMLKJIH',
(f"D{i}" for i in itertools.count())))
text_shape = ", ".join([str(n) if n is not None else next(dim_labels)
for n in shape[::-1]][::-1])
if len(shape) == 1:
text_shape += ","
raise ValueError(
f"{k!r} must be {len(shape)}D with shape ({text_shape}), "
f"but your input has shape {v.shape}"
)
def check_getitem(mapping, /, **kwargs):
"""
*kwargs* must consist of a single *key, value* pair. If *key* is in
*mapping*, return ``mapping[value]``; else, raise an appropriate
ValueError.
Examples
--------
>>> _api.check_getitem({"foo": "bar"}, arg=arg)
"""
if len(kwargs) != 1:
raise ValueError("check_getitem takes a single keyword argument")
(k, v), = kwargs.items()
try:
return mapping[v]
except KeyError:
raise ValueError(
f"{v!r} is not a valid value for {k}; supported values are "
f"{', '.join(map(repr, mapping))}") from None
def caching_module_getattr(cls):
"""
Helper decorator for implementing module-level ``__getattr__`` as a class.
This decorator must be used at the module toplevel as follows::
@caching_module_getattr
class __getattr__: # The class *must* be named ``__getattr__``.
@property # Only properties are taken into account.
def name(self): ...
The ``__getattr__`` class will be replaced by a ``__getattr__``
function such that trying to access ``name`` on the module will
resolve the corresponding property (which may be decorated e.g. with
``_api.deprecated`` for deprecating module globals). The properties are
all implicitly cached. Moreover, a suitable AttributeError is generated
and raised if no property with the given name exists.
"""
assert cls.__name__ == "__getattr__"
# Don't accidentally export cls dunders.
props = {name: prop for name, prop in vars(cls).items()
if isinstance(prop, property)}
instance = cls()
@functools.cache
def __getattr__(name):
if name in props:
return props[name].__get__(instance)
raise AttributeError(
f"module {cls.__module__!r} has no attribute {name!r}")
return __getattr__
def define_aliases(alias_d, cls=None):
"""
Class decorator for defining property aliases.
Use as ::
@_api.define_aliases({"property": ["alias", ...], ...})
class C: ...
For each property, if the corresponding ``get_property`` is defined in the
class so far, an alias named ``get_alias`` will be defined; the same will
be done for setters. If neither the getter nor the setter exists, an
exception will be raised.
The alias map is stored as the ``_alias_map`` attribute on the class and
can be used by `.normalize_kwargs` (which assumes that higher priority
aliases come last).
"""
if cls is None: # Return the actual class decorator.
return functools.partial(define_aliases, alias_d)
def make_alias(name): # Enforce a closure over *name*.
@functools.wraps(getattr(cls, name))
def method(self, *args, **kwargs):
return getattr(self, name)(*args, **kwargs)
return method
for prop, aliases in alias_d.items():
exists = False
for prefix in ["get_", "set_"]:
if prefix + prop in vars(cls):
exists = True
for alias in aliases:
method = make_alias(prefix + prop)
method.__name__ = prefix + alias
method.__doc__ = f"Alias for `{prefix + prop}`."
setattr(cls, prefix + alias, method)
if not exists:
raise ValueError(
f"Neither getter nor setter exists for {prop!r}")
def get_aliased_and_aliases(d):
return {*d, *(alias for aliases in d.values() for alias in aliases)}
preexisting_aliases = getattr(cls, "_alias_map", {})
conflicting = (get_aliased_and_aliases(preexisting_aliases)
& get_aliased_and_aliases(alias_d))
if conflicting:
# Need to decide on conflict resolution policy.
raise NotImplementedError(
f"Parent class already defines conflicting aliases: {conflicting}")
cls._alias_map = {**preexisting_aliases, **alias_d}
return cls
def select_matching_signature(funcs, *args, **kwargs):
"""
Select and call the function that accepts ``*args, **kwargs``.
*funcs* is a list of functions which should not raise any exception (other
than `TypeError` if the arguments passed do not match their signature).
`select_matching_signature` tries to call each of the functions in *funcs*
with ``*args, **kwargs`` (in the order in which they are given). Calls
that fail with a `TypeError` are silently skipped. As soon as a call
succeeds, `select_matching_signature` returns its return value. If no
function accepts ``*args, **kwargs``, then the `TypeError` raised by the
last failing call is re-raised.
Callers should normally make sure that any ``*args, **kwargs`` can only
bind a single *func* (to avoid any ambiguity), although this is not checked
by `select_matching_signature`.
Notes
-----
`select_matching_signature` is intended to help implementing
signature-overloaded functions. In general, such functions should be
avoided, except for back-compatibility concerns. A typical use pattern is
::
def my_func(*args, **kwargs):
params = select_matching_signature(
[lambda old1, old2: locals(), lambda new: locals()],
*args, **kwargs)
if "old1" in params:
warn_deprecated(...)
old1, old2 = params.values() # note that locals() is ordered.
else:
new, = params.values()
# do things with params
which allows *my_func* to be called either with two parameters (*old1* and
*old2*) or a single one (*new*). Note that the new signature is given
last, so that callers get a `TypeError` corresponding to the new signature
if the arguments they passed in do not match any signature.
"""
# Rather than relying on locals() ordering, one could have just used func's
# signature (``bound = inspect.signature(func).bind(*args, **kwargs);
# bound.apply_defaults(); return bound``) but that is significantly slower.
for i, func in enumerate(funcs):
try:
return func(*args, **kwargs)
except TypeError:
if i == len(funcs) - 1:
raise
def nargs_error(name, takes, given):
"""Generate a TypeError to be raised by function calls with wrong arity."""
return TypeError(f"{name}() takes {takes} positional arguments but "
f"{given} were given")
def kwarg_error(name, kw):
"""
Generate a TypeError to be raised by function calls with wrong kwarg.
Parameters
----------
name : str
The name of the calling function.
kw : str or Iterable[str]
Either the invalid keyword argument name, or an iterable yielding
invalid keyword arguments (e.g., a ``kwargs`` dict).
"""
if not isinstance(kw, str):
kw = next(iter(kw))
return TypeError(f"{name}() got an unexpected keyword argument '{kw}'")
def recursive_subclasses(cls):
"""Yield *cls* and direct and indirect subclasses of *cls*."""
yield cls
for subcls in cls.__subclasses__():
yield from recursive_subclasses(subcls)
def warn_external(message, category=None):
"""
`warnings.warn` wrapper that sets *stacklevel* to "outside Matplotlib".
The original emitter of the warning can be obtained by patching this
function back to `warnings.warn`, i.e. ``_api.warn_external =
warnings.warn`` (or ``functools.partial(warnings.warn, stacklevel=2)``,
etc.).
"""
frame = sys._getframe()
for stacklevel in itertools.count(1):
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
# preemptively break reference cycle between locals and the frame
del frame
warnings.warn(message, category, stacklevel)

View File

@ -0,0 +1,59 @@
from collections.abc import Callable, Generator, Mapping, Sequence
from typing import Any, Iterable, TypeVar, overload
from numpy.typing import NDArray
from .deprecation import ( # noqa: re-exported API
deprecated as deprecated,
warn_deprecated as warn_deprecated,
rename_parameter as rename_parameter,
delete_parameter as delete_parameter,
make_keyword_only as make_keyword_only,
deprecate_method_override as deprecate_method_override,
deprecate_privatize_attribute as deprecate_privatize_attribute,
suppress_matplotlib_deprecation_warning as suppress_matplotlib_deprecation_warning,
MatplotlibDeprecationWarning as MatplotlibDeprecationWarning,
)
_T = TypeVar("_T")
class classproperty(Any):
def __init__(
self,
fget: Callable[[_T], Any],
fset: None = ...,
fdel: None = ...,
doc: str | None = None,
): ...
# Replace return with Self when py3.9 is dropped
@overload
def __get__(self, instance: None, owner: None) -> classproperty: ...
@overload
def __get__(self, instance: object, owner: type[object]) -> Any: ...
@property
def fget(self) -> Callable[[_T], Any]: ...
def check_isinstance(
types: type | tuple[type | None, ...], /, **kwargs: Any
) -> None: ...
def check_in_list(
values: Sequence[Any], /, *, _print_supported_values: bool = ..., **kwargs: Any
) -> None: ...
def check_shape(shape: tuple[int | None, ...], /, **kwargs: NDArray) -> None: ...
def check_getitem(mapping: Mapping[Any, Any], /, **kwargs: Any) -> Any: ...
def caching_module_getattr(cls: type) -> Callable[[str], Any]: ...
@overload
def define_aliases(
alias_d: dict[str, list[str]], cls: None = ...
) -> Callable[[type[_T]], type[_T]]: ...
@overload
def define_aliases(alias_d: dict[str, list[str]], cls: type[_T]) -> type[_T]: ...
def select_matching_signature(
funcs: list[Callable], *args: Any, **kwargs: Any
) -> Any: ...
def nargs_error(name: str, takes: int | str, given: int) -> TypeError: ...
def kwarg_error(name: str, kw: str | Iterable[str]) -> TypeError: ...
def recursive_subclasses(cls: type) -> Generator[type, None, None]: ...
def warn_external(
message: str | Warning, category: type[Warning] | None = ...
) -> None: ...

View File

@ -0,0 +1,513 @@
"""
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 math
import warnings
class MatplotlibDeprecationWarning(DeprecationWarning):
"""A class for issuing deprecation warnings for Matplotlib users."""
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:
if not removal:
macro, meso, *_ = since.split('.')
removal = f'{macro}.{int(meso) + 2}'
removal = f"in {removal}"
if not message:
message = (
("The %(name)s %(obj_type)s" if obj_type else "%(name)s")
+ (" will be deprecated in a future version"
if pending else
" was deprecated in Matplotlib %(since)s and will be removed %(removal)s"
)
+ "."
+ (" 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
--------
::
# 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 are the same as for `warn_deprecated`, except that *obj_type*
defaults to 'class' if decorating a class, 'attribute' if decorating a
property, and 'function' otherwise.
Examples
--------
::
@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)):
if obj_type is None:
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=None):
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-----'
second_arg = ' '.join([t.strip() for t in
(message, f"Use {alternative} instead."
if alternative else "", addendum) if t])
new_doc = (f"[*Deprecated*] {old_doc}\n"
f"{notes_header if notes_header not in old_doc else ''}\n"
f".. deprecated:: {since}\n"
f" {second_arg}")
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 (or method).
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 read and write 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*. This helper also works for deprecating methods.
"""
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}"),
lambda self, value: setattr(self, f"_{name}", value)),
name=name))
# Used by _copy_docstring_and_deprecators to redecorate pyplot wrappers and
# boilerplate.py to retrieve original signatures. It may seem natural to store
# this information as an attribute on the wrapper, but if the wrapper gets
# itself functools.wraps()ed, then such attributes are silently propagated to
# the outer wrapper, which is not desired.
DECORATORS = {}
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): ...
"""
decorator = functools.partial(rename_parameter, since, old, new)
if func is None:
return decorator
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.
DECORATORS[wrapper] = decorator
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): ...
"""
decorator = functools.partial(delete_parameter, since, name, **kwargs)
if func is None:
return decorator
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:
name_idx = (
# Deprecated parameter can't be passed positionally.
math.inf if kind is inspect.Parameter.KEYWORD_ONLY
# If call site has no more than this number of parameters, the
# deprecated parameter can't have been passed positionally.
else [*signature.parameters].index(name))
func.__signature__ = signature = signature.replace(parameters=[
param.replace(default=_deprecated_parameter)
if param.name == name else param
for param in signature.parameters.values()])
else:
name_idx = -1 # Deprecated parameter can always have been passed.
else:
is_varargs = is_varkwargs = False
# Deprecated parameter can't be passed positionally.
name_idx = math.inf
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):
if len(inner_args) <= name_idx and name not in inner_kwargs:
# Early return in the simple, non-deprecated case (much faster than
# calling bind()).
return func(*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)
DECORATORS[wrapper] = decorator
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.
When used on a method that has a pyplot wrapper, this should be the
outermost decorator, so that :file:`boilerplate.py` can access the original
signature.
"""
decorator = functools.partial(make_keyword_only, since, name)
if func is None:
return decorator
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]
name_idx = names.index(name)
kwonly = [name for name in names[name_idx:]
if signature.parameters[name].kind == POK]
@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).
if len(args) > name_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)
# Don't modify *func*'s signature, as boilerplate.py needs it.
wrapper.__signature__ = signature.replace(parameters=[
param.replace(kind=KWO) if param.name in kwonly else param
for param in signature.parameters.values()])
DECORATORS[wrapper] = decorator
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

View File

@ -0,0 +1,76 @@
from collections.abc import Callable
import contextlib
from typing import Any, TypedDict, TypeVar, overload
from typing_extensions import (
ParamSpec, # < Py 3.10
Unpack, # < Py 3.11
)
_P = ParamSpec("_P")
_R = TypeVar("_R")
_T = TypeVar("_T")
class MatplotlibDeprecationWarning(DeprecationWarning): ...
class DeprecationKwargs(TypedDict, total=False):
message: str
alternative: str
pending: bool
obj_type: str
addendum: str
removal: str
class NamedDeprecationKwargs(DeprecationKwargs, total=False):
name: str
def warn_deprecated(since: str, **kwargs: Unpack[NamedDeprecationKwargs]) -> None: ...
def deprecated(
since: str, **kwargs: Unpack[NamedDeprecationKwargs]
) -> Callable[[_T], _T]: ...
class deprecate_privatize_attribute(Any):
def __init__(self, since: str, **kwargs: Unpack[NamedDeprecationKwargs]): ...
def __set_name__(self, owner: type[object], name: str) -> None: ...
DECORATORS: dict[Callable, Callable] = ...
@overload
def rename_parameter(
since: str, old: str, new: str, func: None = ...
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ...
@overload
def rename_parameter(
since: str, old: str, new: str, func: Callable[_P, _R]
) -> Callable[_P, _R]: ...
class _deprecated_parameter_class: ...
_deprecated_parameter: _deprecated_parameter_class
@overload
def delete_parameter(
since: str, name: str, func: None = ..., **kwargs: Unpack[DeprecationKwargs]
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ...
@overload
def delete_parameter(
since: str, name: str, func: Callable[_P, _R], **kwargs: Unpack[DeprecationKwargs]
) -> Callable[_P, _R]: ...
@overload
def make_keyword_only(
since: str, name: str, func: None = ...
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ...
@overload
def make_keyword_only(
since: str, name: str, func: Callable[_P, _R]
) -> Callable[_P, _R]: ...
def deprecate_method_override(
method: Callable[_P, _R],
obj: object | type,
*,
allow_empty: bool = ...,
since: str,
**kwargs: Unpack[NamedDeprecationKwargs]
) -> Callable[_P, _R]: ...
def suppress_matplotlib_deprecation_warning() -> (
contextlib.AbstractContextManager[None]
): ...

View File

@ -0,0 +1,30 @@
def blocking_input_loop(figure, event_names, timeout, handler):
"""
Run *figure*'s event loop while listening to interactive events.
The events listed in *event_names* are passed to *handler*.
This function is used to implement `.Figure.waitforbuttonpress`,
`.Figure.ginput`, and `.Axes.clabel`.
Parameters
----------
figure : `~matplotlib.figure.Figure`
event_names : list of str
The names of the events passed to *handler*.
timeout : float
If positive, the event loop is stopped after *timeout* seconds.
handler : Callable[[Event], Any]
Function called for each event; it can force an early exit of the event
loop by calling ``canvas.stop_event_loop()``.
"""
if figure.canvas.manager:
figure.show() # Ensure that the figure is shown if we are managing it.
# Connect the events to the on_event function call.
cids = [figure.canvas.mpl_connect(name, handler) for name in event_names]
try:
figure.canvas.start_event_loop(timeout) # Start event loop.
finally: # Run even on exception like ctrl-c.
# Disconnect the callbacks.
for cid in cids:
figure.canvas.mpl_disconnect(cid)

View File

@ -0,0 +1,7 @@
def display_is_valid() -> bool: ...
def Win32_GetForegroundWindow() -> int | None: ...
def Win32_SetForegroundWindow(hwnd: int) -> None: ...
def Win32_SetProcessDpiAwareness_max() -> None: ...
def Win32_SetCurrentProcessExplicitAppUserModelID(appid: str) -> None: ...
def Win32_GetCurrentProcessExplicitAppUserModelID() -> str | None: ...

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
from .typing import ColorType
BASE_COLORS: dict[str, ColorType]
TABLEAU_COLORS: dict[str, ColorType]
XKCD_COLORS: dict[str, ColorType]
CSS4_COLORS: dict[str, ColorType]

View File

@ -0,0 +1,794 @@
"""
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: :ref:`constrainedlayout_guide`
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 :ref:`constrainedlayout_guide`
for more discussion of the algorithm with examples.
"""
import logging
import numpy as np
from matplotlib import _api, artist as martist
import matplotlib.transforms as mtransforms
import matplotlib._layoutgrid as mlayoutgrid
_log = logging.getLogger(__name__)
######################################################
def do_constrained_layout(fig, h_pad, w_pad,
hspace=None, wspace=None, rect=(0, 0, 1, 1),
compress=False):
"""
Do the constrained_layout. Called at draw time in
``figure.constrained_layout()``
Parameters
----------
fig : `~matplotlib.figure.Figure`
`.Figure` instance to do the layout in.
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.
rect : tuple of 4 floats
Rectangle in figure coordinates to perform constrained layout in
[left, bottom, width, height], each from 0-1.
compress : bool
Whether to shift Axes so that white space in between them is
removed. This is useful for simple grids of fixed-aspect Axes (e.g.
a grid of images).
Returns
-------
layoutgrid : private debugging structure
"""
renderer = fig._get_renderer()
# make layoutgrid tree...
layoutgrids = make_layoutgrids(fig, None, rect=rect)
if not layoutgrids['hasgrids']:
_api.warn_external('There are no gridspecs with layoutgrids. '
'Possibly did not call parent GridSpec with the'
' "figure" keyword')
return
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(layoutgrids, fig, renderer, h_pad=h_pad,
w_pad=w_pad, hspace=hspace, wspace=wspace)
make_margin_suptitles(layoutgrids, 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(layoutgrids, fig)
# update all the variables in the layout.
layoutgrids[fig].update_variables()
warn_collapsed = ('constrained_layout not applied because '
'axes sizes collapsed to zero. Try making '
'figure larger or Axes decorations smaller.')
if check_no_collapsed_axes(layoutgrids, fig):
reposition_axes(layoutgrids, fig, renderer, h_pad=h_pad,
w_pad=w_pad, hspace=hspace, wspace=wspace)
if compress:
layoutgrids = compress_fixed_aspect(layoutgrids, fig)
layoutgrids[fig].update_variables()
if check_no_collapsed_axes(layoutgrids, fig):
reposition_axes(layoutgrids, fig, renderer, h_pad=h_pad,
w_pad=w_pad, hspace=hspace, wspace=wspace)
else:
_api.warn_external(warn_collapsed)
else:
_api.warn_external(warn_collapsed)
reset_margins(layoutgrids, fig)
return layoutgrids
def make_layoutgrids(fig, layoutgrids, rect=(0, 0, 1, 1)):
"""
Make the layoutgrid tree.
(Sub)Figures get a layoutgrid so we can have figure margins.
Gridspecs that are attached to Axes get a layoutgrid so Axes
can have margins.
"""
if layoutgrids is None:
layoutgrids = dict()
layoutgrids['hasgrids'] = False
if not hasattr(fig, '_parent'):
# top figure; pass rect as parent to allow user-specified
# margins
layoutgrids[fig] = mlayoutgrid.LayoutGrid(parent=rect, name='figlb')
else:
# subfigure
gs = fig._subplotspec.get_gridspec()
# it is possible the gridspec containing this subfigure hasn't
# been added to the tree yet:
layoutgrids = make_layoutgrids_gs(layoutgrids, gs)
# add the layoutgrid for the subfigure:
parentlb = layoutgrids[gs]
layoutgrids[fig] = mlayoutgrid.LayoutGrid(
parent=parentlb,
name='panellb',
parent_inner=True,
nrows=1, ncols=1,
parent_pos=(fig._subplotspec.rowspan,
fig._subplotspec.colspan))
# recursively do all subfigures in this figure...
for sfig in fig.subfigs:
layoutgrids = make_layoutgrids(sfig, layoutgrids)
# for each Axes at the local level add its gridspec:
for ax in fig._localaxes:
gs = ax.get_gridspec()
if gs is not None:
layoutgrids = make_layoutgrids_gs(layoutgrids, gs)
return layoutgrids
def make_layoutgrids_gs(layoutgrids, gs):
"""
Make the layoutgrid for a gridspec (and anything nested in the gridspec)
"""
if gs in layoutgrids or gs.figure is None:
return layoutgrids
# in order to do constrained_layout there has to be at least *one*
# gridspec in the tree:
layoutgrids['hasgrids'] = True
if not hasattr(gs, '_subplot_spec'):
# normal gridspec
parent = layoutgrids[gs.figure]
layoutgrids[gs] = mlayoutgrid.LayoutGrid(
parent=parent,
parent_inner=True,
name='gridspec',
ncols=gs._ncols, nrows=gs._nrows,
width_ratios=gs.get_width_ratios(),
height_ratios=gs.get_height_ratios())
else:
# this is a gridspecfromsubplotspec:
subplot_spec = gs._subplot_spec
parentgs = subplot_spec.get_gridspec()
# if a nested gridspec it is possible the parent is not in there yet:
if parentgs not in layoutgrids:
layoutgrids = make_layoutgrids_gs(layoutgrids, parentgs)
subspeclb = layoutgrids[parentgs]
# gridspecfromsubplotspec need an outer container:
# get a unique representation:
rep = (gs, 'top')
if rep not in layoutgrids:
layoutgrids[rep] = mlayoutgrid.LayoutGrid(
parent=subspeclb,
name='top',
nrows=1, ncols=1,
parent_pos=(subplot_spec.rowspan, subplot_spec.colspan))
layoutgrids[gs] = mlayoutgrid.LayoutGrid(
parent=layoutgrids[rep],
name='gridspec',
nrows=gs._nrows, ncols=gs._ncols,
width_ratios=gs.get_width_ratios(),
height_ratios=gs.get_height_ratios())
return layoutgrids
def check_no_collapsed_axes(layoutgrids, fig):
"""
Check that no Axes have collapsed to zero size.
"""
for sfig in fig.subfigs:
ok = check_no_collapsed_axes(layoutgrids, sfig)
if not ok:
return False
for ax in fig.axes:
gs = ax.get_gridspec()
if gs in layoutgrids: # also implies gs is not None.
lg = layoutgrids[gs]
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 compress_fixed_aspect(layoutgrids, fig):
gs = None
for ax in fig.axes:
if ax.get_subplotspec() is None:
continue
ax.apply_aspect()
sub = ax.get_subplotspec()
_gs = sub.get_gridspec()
if gs is None:
gs = _gs
extraw = np.zeros(gs.ncols)
extrah = np.zeros(gs.nrows)
elif _gs != gs:
raise ValueError('Cannot do compressed layout if Axes are not'
'all from the same gridspec')
orig = ax.get_position(original=True)
actual = ax.get_position(original=False)
dw = orig.width - actual.width
if dw > 0:
extraw[sub.colspan] = np.maximum(extraw[sub.colspan], dw)
dh = orig.height - actual.height
if dh > 0:
extrah[sub.rowspan] = np.maximum(extrah[sub.rowspan], dh)
if gs is None:
raise ValueError('Cannot do compressed layout if no Axes '
'are part of a gridspec.')
w = np.sum(extraw) / 2
layoutgrids[fig].edit_margin_min('left', w)
layoutgrids[fig].edit_margin_min('right', w)
h = np.sum(extrah) / 2
layoutgrids[fig].edit_margin_min('top', h)
layoutgrids[fig].edit_margin_min('bottom', h)
return layoutgrids
def get_margin_from_padding(obj, *, w_pad=0, h_pad=0,
hspace=0, wspace=0):
ss = obj._subplotspec
gs = ss.get_gridspec()
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(layoutgrids, 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.
Parameters
----------
layoutgrids : dict
fig : `~matplotlib.figure.Figure`
`.Figure` instance to do the layout in.
renderer : `~matplotlib.backend_bases.RendererBase` subclass.
The renderer to use.
w_pad, h_pad : float, default: 0
Width and height padding (in fraction of figure).
hspace, wspace : float, default: 0
Width and height padding as fraction of figure size divided by
number of columns or rows.
"""
for sfig in fig.subfigs: # recursively make child panel margins
ss = sfig._subplotspec
gs = ss.get_gridspec()
make_layout_margins(layoutgrids, sfig, renderer,
w_pad=w_pad, h_pad=h_pad,
hspace=hspace, wspace=wspace)
margins = get_margin_from_padding(sfig, w_pad=0, h_pad=0,
hspace=hspace, wspace=wspace)
layoutgrids[gs].edit_outer_margin_mins(margins, ss)
for ax in fig._localaxes:
if not ax.get_subplotspec() or not ax.get_in_layout():
continue
ss = ax.get_subplotspec()
gs = ss.get_gridspec()
if gs not in layoutgrids:
return
margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
hspace=hspace, wspace=wspace)
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(layoutgrids, 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...
layoutgrids[gs].edit_outer_margin_mins(margin, ss)
# make margins for figure-level legends:
for leg in fig.legends:
inv_trans_fig = None
if leg._outside_loc and leg._bbox_to_anchor is None:
if inv_trans_fig is None:
inv_trans_fig = fig.transFigure.inverted().transform_bbox
bbox = inv_trans_fig(leg.get_tightbbox(renderer))
w = bbox.width + 2 * w_pad
h = bbox.height + 2 * h_pad
legendloc = leg._outside_loc
if legendloc == 'lower':
layoutgrids[fig].edit_margin_min('bottom', h)
elif legendloc == 'upper':
layoutgrids[fig].edit_margin_min('top', h)
if legendloc == 'right':
layoutgrids[fig].edit_margin_min('right', w)
elif legendloc == 'left':
layoutgrids[fig].edit_margin_min('left', w)
def make_margin_suptitles(layoutgrids, 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 sfig in fig.subfigs:
make_margin_suptitles(layoutgrids, sfig, 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))
layoutgrids[fig].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))
layoutgrids[fig].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))
layoutgrids[fig].edit_margin_min('left', bbox.width + 2 * w_pad)
def match_submerged_margins(layoutgrids, 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 sfig in fig.subfigs:
match_submerged_margins(layoutgrids, sfig)
axs = [a for a in fig.get_axes()
if a.get_subplotspec() is not None and a.get_in_layout()]
for ax1 in axs:
ss1 = ax1.get_subplotspec()
if ss1.get_gridspec() not in layoutgrids:
axs.remove(ax1)
continue
lg1 = layoutgrids[ss1.get_gridspec()]
# 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 = layoutgrids[ss2.get_gridspec()]
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 = layoutgrids[ss2.get_gridspec()]
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.
Parameters
----------
cbax : `~matplotlib.axes.Axes`
Axes for the colorbar.
"""
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 : `~matplotlib.axes.Axes`
renderer : `~matplotlib.backend_bases.RendererBase` subclass.
Returns
-------
pos : `~matplotlib.transforms.Bbox`
Position in figure coordinates.
bbox : `~matplotlib.transforms.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)
tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
if tightbbox is None:
bbox = pos
else:
bbox = tightbbox.transformed(fig.transFigure.inverted())
return pos, bbox
def reposition_axes(layoutgrids, 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 = layoutgrids[sfig].get_outer_bbox()
sfig._redo_transform_rel_fig(
bbox=bbox.transformed(trans_fig_to_subfig))
reposition_axes(layoutgrids, sfig, renderer,
w_pad=w_pad, h_pad=h_pad,
wspace=wspace, hspace=hspace)
for ax in fig._localaxes:
if ax.get_subplotspec() is None 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()
if gs not in layoutgrids:
return
bbox = layoutgrids[gs].get_inner_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]:
reposition_colorbar(layoutgrids, cbax, renderer,
offset=offset)
def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None):
"""
Place the colorbar in its new place.
Parameters
----------
layoutgrids : dict
cbax : `~matplotlib.axes.Axes`
Axes for the colorbar.
renderer : `~matplotlib.backend_bases.RendererBase` subclass.
The renderer to use.
offset : 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()
fig = cbax.figure
trans_fig_to_subfig = fig.transFigure - fig.transSubfigure
cb_rspans, cb_cspans = get_cb_parent_spans(cbax)
bboxparent = layoutgrids[gs].get_bbox_for_cb(rows=cb_rspans,
cols=cb_cspans)
pb = layoutgrids[gs].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(layoutgrids, 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_anchor(anchor)
if location in ['bottom', 'top']:
aspect = 1 / aspect
cbax.set_box_aspect(aspect)
cbax.set_aspect('auto')
return offset
def reset_margins(layoutgrids, 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 sfig in fig.subfigs:
reset_margins(layoutgrids, sfig)
for ax in fig.axes:
if ax.get_in_layout():
gs = ax.get_gridspec()
if gs in layoutgrids: # also implies gs is not None.
layoutgrids[gs].reset_margins()
layoutgrids[fig].reset_margins()
def colorbar_get_pad(layoutgrids, cax):
parents = cax._colorbar_info['parents']
gs = parents[0].get_gridspec()
cb_rspans, cb_cspans = get_cb_parent_spans(cax)
bboxouter = layoutgrids[gs].get_inner_bbox(rows=cb_rspans, cols=cb_cspans)
if cax._colorbar_info['location'] in ['right', 'left']:
size = bboxouter.width
else:
size = bboxouter.height
return cax._colorbar_info['pad'] * size

View File

@ -0,0 +1,125 @@
import inspect
from . import _api
def kwarg_doc(text):
"""
Decorator for defining the kwdoc documentation of artist properties.
This decorator can be applied to artist property setter methods.
The given text is stored in a private attribute ``_kwarg_doc`` on
the method. It is used to overwrite auto-generated documentation
in the *kwdoc list* for artists. The kwdoc list is used to document
``**kwargs`` when they are properties of an artist. See e.g. the
``**kwargs`` section in `.Axes.text`.
The text should contain the supported types, as well as the default
value if applicable, e.g.:
@_docstring.kwarg_doc("bool, default: :rc:`text.usetex`")
def set_usetex(self, usetex):
See Also
--------
matplotlib.artist.kwdoc
"""
def decorator(func):
func._kwarg_doc = text
return func
return decorator
class Substitution:
"""
A decorator that performs %-substitution on an object's docstring.
This decorator should be robust even if ``obj.__doc__`` is None (for
example, if -OO was passed to the interpreter).
Usage: construct a docstring.Substitution with a sequence or dictionary
suitable for performing substitution; then decorate a suitable function
with the constructed object, e.g.::
sub_author_name = Substitution(author='Jason')
@sub_author_name
def some_function(x):
"%(author)s wrote this function"
# note that some_function.__doc__ is now "Jason wrote this function"
One can also use positional arguments::
sub_first_last_names = Substitution('Edgar Allen', 'Poe')
@sub_first_last_names
def some_function(x):
"%s %s wrote the Raven"
"""
def __init__(self, *args, **kwargs):
if args and kwargs:
raise TypeError("Only positional or keyword args are allowed")
self.params = args or kwargs
def __call__(self, func):
if func.__doc__:
func.__doc__ = inspect.cleandoc(func.__doc__) % self.params
return func
def update(self, *args, **kwargs):
"""
Update ``self.params`` (which must be a dict) with the supplied args.
"""
self.params.update(*args, **kwargs)
class _ArtistKwdocLoader(dict):
def __missing__(self, key):
if not key.endswith(":kwdoc"):
raise KeyError(key)
name = key[:-len(":kwdoc")]
from matplotlib.artist import Artist, kwdoc
try:
cls, = [cls for cls in _api.recursive_subclasses(Artist)
if cls.__name__ == name]
except ValueError as e:
raise KeyError(key) from e
return self.setdefault(key, kwdoc(cls))
class _ArtistPropertiesSubstitution(Substitution):
"""
A `.Substitution` with two additional features:
- Substitutions of the form ``%(classname:kwdoc)s`` (ending with the
literal ":kwdoc" suffix) trigger lookup of an Artist subclass with the
given *classname*, and are substituted with the `.kwdoc` of that class.
- Decorating a class triggers substitution both on the class docstring and
on the class' ``__init__`` docstring (which is a commonly required
pattern for Artist subclasses).
"""
def __init__(self):
self.params = _ArtistKwdocLoader()
def __call__(self, obj):
super().__call__(obj)
if isinstance(obj, type) and obj.__init__ != object.__init__:
self(obj.__init__)
return obj
def copy(source):
"""Copy a docstring from another source function (if present)."""
def do_copy(target):
if source.__doc__:
target.__doc__ = source.__doc__
return target
return do_copy
# Create a decorator that will house the various docstring snippets reused
# throughout Matplotlib.
dedent_interpd = interpd = _ArtistPropertiesSubstitution()

View File

@ -0,0 +1,32 @@
from typing import Any, Callable, TypeVar, overload
_T = TypeVar('_T')
def kwarg_doc(text: str) -> Callable[[_T], _T]: ...
class Substitution:
@overload
def __init__(self, *args: str): ...
@overload
def __init__(self, **kwargs: str): ...
def __call__(self, func: _T) -> _T: ...
def update(self, *args, **kwargs): ... # type: ignore[no-untyped-def]
class _ArtistKwdocLoader(dict[str, str]):
def __missing__(self, key: str) -> str: ...
class _ArtistPropertiesSubstitution(Substitution):
def __init__(self) -> None: ...
def __call__(self, obj: _T) -> _T: ...
def copy(source: Any) -> Callable[[_T], _T]: ...
dedent_interpd: _ArtistPropertiesSubstitution
interpd: _ArtistPropertiesSubstitution

View File

@ -0,0 +1,185 @@
"""
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 _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__()
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()
@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`.
By default, `~.backend_bases.GraphicsContextBase` draws a stroked line as
squared off at its endpoints.
**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 = auto()
projecting = auto()
round = auto()
@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.set_ylim(-.5, 1.5)
ax.set_axis_off()
fig.show()
CapStyle.input_description = "{" \
+ ", ".join([f"'{cs.name}'" for cs in CapStyle]) \
+ "}"
_docstring.interpd.update({'JoinStyle': JoinStyle.input_description,
'CapStyle': CapStyle.input_description})

View File

@ -0,0 +1,18 @@
from enum import Enum
class _AutoStringNameEnum(Enum):
def __hash__(self) -> int: ...
class JoinStyle(str, _AutoStringNameEnum):
miter: str
round: str
bevel: str
@staticmethod
def demo() -> None: ...
class CapStyle(str, _AutoStringNameEnum):
butt: str
projecting: str
round: str
@staticmethod
def demo() -> None: ...

View File

@ -0,0 +1,111 @@
"""
A module for parsing and generating `fontconfig patterns`_.
.. _fontconfig patterns:
https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
"""
# This class logically belongs in `matplotlib.font_manager`, but placing it
# there would have created cyclical dependency problems, because it also needs
# to be available from `matplotlib.rcsetup` (for parsing matplotlibrc files).
from functools import lru_cache, partial
import re
from pyparsing import (
Group, Optional, ParseException, Regex, StringEnd, Suppress, ZeroOrMore, oneOf)
_family_punc = r'\\\-:,'
_family_unescape = partial(re.compile(r'\\(?=[%s])' % _family_punc).sub, '')
_family_escape = partial(re.compile(r'(?=[%s])' % _family_punc).sub, r'\\')
_value_punc = r'\\=_:,'
_value_unescape = partial(re.compile(r'\\(?=[%s])' % _value_punc).sub, '')
_value_escape = partial(re.compile(r'(?=[%s])' % _value_punc).sub, r'\\')
_CONSTANTS = {
'thin': ('weight', 'light'),
'extralight': ('weight', 'light'),
'ultralight': ('weight', 'light'),
'light': ('weight', 'light'),
'book': ('weight', 'book'),
'regular': ('weight', 'regular'),
'normal': ('weight', 'normal'),
'medium': ('weight', 'medium'),
'demibold': ('weight', 'demibold'),
'semibold': ('weight', 'semibold'),
'bold': ('weight', 'bold'),
'extrabold': ('weight', 'extra bold'),
'black': ('weight', 'black'),
'heavy': ('weight', 'heavy'),
'roman': ('slant', 'normal'),
'italic': ('slant', 'italic'),
'oblique': ('slant', 'oblique'),
'ultracondensed': ('width', 'ultra-condensed'),
'extracondensed': ('width', 'extra-condensed'),
'condensed': ('width', 'condensed'),
'semicondensed': ('width', 'semi-condensed'),
'expanded': ('width', 'expanded'),
'extraexpanded': ('width', 'extra-expanded'),
'ultraexpanded': ('width', 'ultra-expanded'),
}
@lru_cache # The parser instance is a singleton.
def _make_fontconfig_parser():
def comma_separated(elem):
return elem + ZeroOrMore(Suppress(",") + elem)
family = Regex(fr"([^{_family_punc}]|(\\[{_family_punc}]))*")
size = Regex(r"([0-9]+\.?[0-9]*|\.[0-9]+)")
name = Regex(r"[a-z]+")
value = Regex(fr"([^{_value_punc}]|(\\[{_value_punc}]))*")
prop = Group((name + Suppress("=") + comma_separated(value)) | oneOf(_CONSTANTS))
return (
Optional(comma_separated(family)("families"))
+ Optional("-" + comma_separated(size)("sizes"))
+ ZeroOrMore(":" + prop("properties*"))
+ StringEnd()
)
# `parse_fontconfig_pattern` is a bottleneck during the tests because it is
# repeatedly called when the rcParams are reset (to validate the default
# fonts). In practice, the cache size doesn't grow beyond a few dozen entries
# during the test suite.
@lru_cache
def parse_fontconfig_pattern(pattern):
"""
Parse a fontconfig *pattern* into a dict that can initialize a
`.font_manager.FontProperties` object.
"""
parser = _make_fontconfig_parser()
try:
parse = parser.parseString(pattern)
except ParseException as err:
# explain becomes a plain method on pyparsing 3 (err.explain(0)).
raise ValueError("\n" + ParseException.explain(err, 0)) from None
parser.resetCache()
props = {}
if "families" in parse:
props["family"] = [*map(_family_unescape, parse["families"])]
if "sizes" in parse:
props["size"] = [*parse["sizes"]]
for prop in parse.get("properties", []):
if len(prop) == 1:
prop = _CONSTANTS[prop[0]]
k, *v = prop
props.setdefault(k, []).extend(map(_value_unescape, v))
return props
def generate_fontconfig_pattern(d):
"""Convert a `.FontProperties` to a fontconfig pattern string."""
kvs = [(k, getattr(d, f"get_{k}")())
for k in ["style", "variant", "weight", "stretch", "file", "size"]]
# Families is given first without a leading keyword. Other entries (which
# are necessarily scalar) are given as key=value, skipping Nones.
return (",".join(_family_escape(f) for f in d.get_family())
+ "".join(f":{k}={_value_escape(str(v))}"
for k, v in kvs if v is not None))

View File

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

View File

@ -0,0 +1,547 @@
"""
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
import matplotlib as mpl
import matplotlib.patches as mpatches
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_pos = parent_pos
self.parent_inner = parent_inner
self.name = name + seq_id()
if isinstance(parent, LayoutGrid):
self.name = f'{parent.name}.{self.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 not isinstance(parent, LayoutGrid):
# parent can be a rect if not a LayoutGrid
# allows specifying a rectangle to contain the layout.
self.solver = kiwi.Solver()
else:
parent.add_child(self, *parent_pos)
self.solver = 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
self.lefts = [Variable(f'{sn}lefts[{i}]') for i in range(ncols)]
self.rights = [Variable(f'{sn}rights[{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.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(parent)
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'R{self.rights[j].value():1.3f}, ' \
f'T{self.tops[i].value():1.3f}, ' \
f'ML{self.margins["left"][j].value():1.3f}, ' \
f'MR{self.margins["right"][j].value():1.3f}, ' \
f'MB{self.margins["bottom"][i].value():1.3f}, ' \
f'MT{self.margins["top"][i].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, parent):
# define self-consistent constraints
self.hard_constraints()
# define relationship with parent layoutgrid:
self.parent_constraints(parent)
# 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):
# np.ix_ returns the cross product of i and j indices
self.children[np.ix_(np.atleast_1d(i), np.atleast_1d(j))] = child
def parent_constraints(self, parent):
# 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...
if not isinstance(parent, LayoutGrid):
# specify a rectangle in figure coordinates
hc = [self.lefts[0] == parent[0],
self.rights[-1] == parent[0] + parent[2],
# top and bottom reversed order...
self.tops[0] == parent[1] + parent[3],
self.bottoms[-1] == parent[1]]
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 plot_children(fig, lg=None, level=0):
"""Simple plotting to show where boxes are."""
if lg is None:
_layoutgrids = fig.get_layout_engine().execute(fig)
lg = _layoutgrids[fig]
colors = mpl.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

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