asd
This commit is contained in:
1516
venv/lib/python3.12/site-packages/matplotlib/__init__.py
Normal file
1516
venv/lib/python3.12/site-packages/matplotlib/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
115
venv/lib/python3.12/site-packages/matplotlib/__init__.pyi
Normal file
115
venv/lib/python3.12/site-packages/matplotlib/__init__.pyi
Normal 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
|
||||
532
venv/lib/python3.12/site-packages/matplotlib/_afm.py
Normal file
532
venv/lib/python3.12/site-packages/matplotlib/_afm.py
Normal 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)
|
||||
262
venv/lib/python3.12/site-packages/matplotlib/_animation_data.py
Normal file
262
venv/lib/python3.12/site-packages/matplotlib/_animation_data.py
Normal 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}";
|
||||
}}
|
||||
"""
|
||||
381
venv/lib/python3.12/site-packages/matplotlib/_api/__init__.py
Normal file
381
venv/lib/python3.12/site-packages/matplotlib/_api/__init__.py
Normal 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)
|
||||
@ -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: ...
|
||||
513
venv/lib/python3.12/site-packages/matplotlib/_api/deprecation.py
Normal file
513
venv/lib/python3.12/site-packages/matplotlib/_api/deprecation.py
Normal 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
|
||||
@ -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]
|
||||
): ...
|
||||
@ -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)
|
||||
Binary file not shown.
@ -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: ...
|
||||
1446
venv/lib/python3.12/site-packages/matplotlib/_cm.py
Normal file
1446
venv/lib/python3.12/site-packages/matplotlib/_cm.py
Normal file
File diff suppressed because it is too large
Load Diff
2071
venv/lib/python3.12/site-packages/matplotlib/_cm_listed.py
Normal file
2071
venv/lib/python3.12/site-packages/matplotlib/_cm_listed.py
Normal file
File diff suppressed because it is too large
Load Diff
1141
venv/lib/python3.12/site-packages/matplotlib/_color_data.py
Normal file
1141
venv/lib/python3.12/site-packages/matplotlib/_color_data.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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]
|
||||
@ -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
|
||||
125
venv/lib/python3.12/site-packages/matplotlib/_docstring.py
Normal file
125
venv/lib/python3.12/site-packages/matplotlib/_docstring.py
Normal 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()
|
||||
32
venv/lib/python3.12/site-packages/matplotlib/_docstring.pyi
Normal file
32
venv/lib/python3.12/site-packages/matplotlib/_docstring.pyi
Normal 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
|
||||
185
venv/lib/python3.12/site-packages/matplotlib/_enums.py
Normal file
185
venv/lib/python3.12/site-packages/matplotlib/_enums.py
Normal 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})
|
||||
18
venv/lib/python3.12/site-packages/matplotlib/_enums.pyi
Normal file
18
venv/lib/python3.12/site-packages/matplotlib/_enums.pyi
Normal 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: ...
|
||||
@ -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))
|
||||
Binary file not shown.
@ -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)
|
||||
547
venv/lib/python3.12/site-packages/matplotlib/_layoutgrid.py
Normal file
547
venv/lib/python3.12/site-packages/matplotlib/_layoutgrid.py
Normal 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)
|
||||
2861
venv/lib/python3.12/site-packages/matplotlib/_mathtext.py
Normal file
2861
venv/lib/python3.12/site-packages/matplotlib/_mathtext.py
Normal file
File diff suppressed because it is too large
Load Diff
1744
venv/lib/python3.12/site-packages/matplotlib/_mathtext_data.py
Normal file
1744
venv/lib/python3.12/site-packages/matplotlib/_mathtext_data.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
9
venv/lib/python3.12/site-packages/matplotlib/_path.pyi
Normal file
9
venv/lib/python3.12/site-packages/matplotlib/_path.pyi
Normal file
@ -0,0 +1,9 @@
|
||||
from collections.abc import Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .transforms import BboxBase
|
||||
|
||||
def affine_transform(points: np.ndarray, trans: np.ndarray) -> np.ndarray: ...
|
||||
def count_bboxes_overlapping_bbox(bbox: BboxBase, bboxes: Sequence[BboxBase]) -> int: ...
|
||||
def update_path_extents(path, trans, rect, minpos, ignore): ...
|
||||
134
venv/lib/python3.12/site-packages/matplotlib/_pylab_helpers.py
Normal file
134
venv/lib/python3.12/site-packages/matplotlib/_pylab_helpers.py
Normal file
@ -0,0 +1,134 @@
|
||||
"""
|
||||
Manage figures for the pyplot interface.
|
||||
"""
|
||||
|
||||
import atexit
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
class Gcf:
|
||||
"""
|
||||
Singleton to maintain the relation between figures and their managers, and
|
||||
keep track of and "active" figure and manager.
|
||||
|
||||
The canvas of a figure created through pyplot is associated with a figure
|
||||
manager, which handles the interaction between the figure and the backend.
|
||||
pyplot keeps track of figure managers using an identifier, the "figure
|
||||
number" or "manager number" (which can actually be any hashable value);
|
||||
this number is available as the :attr:`number` attribute of the manager.
|
||||
|
||||
This class is never instantiated; it consists of an `OrderedDict` mapping
|
||||
figure/manager numbers to managers, and a set of class methods that
|
||||
manipulate this `OrderedDict`.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
figs : OrderedDict
|
||||
`OrderedDict` mapping numbers to managers; the active manager is at the
|
||||
end.
|
||||
"""
|
||||
|
||||
figs = OrderedDict()
|
||||
|
||||
@classmethod
|
||||
def get_fig_manager(cls, num):
|
||||
"""
|
||||
If manager number *num* exists, make it the active one and return it;
|
||||
otherwise return *None*.
|
||||
"""
|
||||
manager = cls.figs.get(num, None)
|
||||
if manager is not None:
|
||||
cls.set_active(manager)
|
||||
return manager
|
||||
|
||||
@classmethod
|
||||
def destroy(cls, num):
|
||||
"""
|
||||
Destroy manager *num* -- either a manager instance or a manager number.
|
||||
|
||||
In the interactive backends, this is bound to the window "destroy" and
|
||||
"delete" events.
|
||||
|
||||
It is recommended to pass a manager instance, to avoid confusion when
|
||||
two managers share the same number.
|
||||
"""
|
||||
if all(hasattr(num, attr) for attr in ["num", "destroy"]):
|
||||
manager = num
|
||||
if cls.figs.get(manager.num) is manager:
|
||||
cls.figs.pop(manager.num)
|
||||
else:
|
||||
try:
|
||||
manager = cls.figs.pop(num)
|
||||
except KeyError:
|
||||
return
|
||||
if hasattr(manager, "_cidgcf"):
|
||||
manager.canvas.mpl_disconnect(manager._cidgcf)
|
||||
manager.destroy()
|
||||
|
||||
@classmethod
|
||||
def destroy_fig(cls, fig):
|
||||
"""Destroy figure *fig*."""
|
||||
num = next((manager.num for manager in cls.figs.values()
|
||||
if manager.canvas.figure == fig), None)
|
||||
if num is not None:
|
||||
cls.destroy(num)
|
||||
|
||||
@classmethod
|
||||
def destroy_all(cls):
|
||||
"""Destroy all figures."""
|
||||
for manager in list(cls.figs.values()):
|
||||
manager.canvas.mpl_disconnect(manager._cidgcf)
|
||||
manager.destroy()
|
||||
cls.figs.clear()
|
||||
|
||||
@classmethod
|
||||
def has_fignum(cls, num):
|
||||
"""Return whether figure number *num* exists."""
|
||||
return num in cls.figs
|
||||
|
||||
@classmethod
|
||||
def get_all_fig_managers(cls):
|
||||
"""Return a list of figure managers."""
|
||||
return list(cls.figs.values())
|
||||
|
||||
@classmethod
|
||||
def get_num_fig_managers(cls):
|
||||
"""Return the number of figures being managed."""
|
||||
return len(cls.figs)
|
||||
|
||||
@classmethod
|
||||
def get_active(cls):
|
||||
"""Return the active manager, or *None* if there is no manager."""
|
||||
return next(reversed(cls.figs.values())) if cls.figs else None
|
||||
|
||||
@classmethod
|
||||
def _set_new_active_manager(cls, manager):
|
||||
"""Adopt *manager* into pyplot and make it the active manager."""
|
||||
if not hasattr(manager, "_cidgcf"):
|
||||
manager._cidgcf = manager.canvas.mpl_connect(
|
||||
"button_press_event", lambda event: cls.set_active(manager))
|
||||
fig = manager.canvas.figure
|
||||
fig.number = manager.num
|
||||
label = fig.get_label()
|
||||
if label:
|
||||
manager.set_window_title(label)
|
||||
cls.set_active(manager)
|
||||
|
||||
@classmethod
|
||||
def set_active(cls, manager):
|
||||
"""Make *manager* the active manager."""
|
||||
cls.figs[manager.num] = manager
|
||||
cls.figs.move_to_end(manager.num)
|
||||
|
||||
@classmethod
|
||||
def draw_all(cls, force=False):
|
||||
"""
|
||||
Redraw all stale managed figures, or, if *force* is True, all managed
|
||||
figures.
|
||||
"""
|
||||
for manager in cls.get_all_fig_managers():
|
||||
if force or manager.canvas.figure.stale:
|
||||
manager.canvas.draw_idle()
|
||||
|
||||
|
||||
atexit.register(Gcf.destroy_all)
|
||||
@ -0,0 +1,29 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from matplotlib.backend_bases import FigureManagerBase
|
||||
from matplotlib.figure import Figure
|
||||
|
||||
class Gcf:
|
||||
figs: OrderedDict[int, FigureManagerBase]
|
||||
@classmethod
|
||||
def get_fig_manager(cls, num: int) -> FigureManagerBase | None: ...
|
||||
@classmethod
|
||||
def destroy(cls, num: int | FigureManagerBase) -> None: ...
|
||||
@classmethod
|
||||
def destroy_fig(cls, fig: Figure) -> None: ...
|
||||
@classmethod
|
||||
def destroy_all(cls) -> None: ...
|
||||
@classmethod
|
||||
def has_fignum(cls, num: int) -> bool: ...
|
||||
@classmethod
|
||||
def get_all_fig_managers(cls) -> list[FigureManagerBase]: ...
|
||||
@classmethod
|
||||
def get_num_fig_managers(cls) -> int: ...
|
||||
@classmethod
|
||||
def get_active(cls) -> FigureManagerBase | None: ...
|
||||
@classmethod
|
||||
def _set_new_active_manager(cls, manager: FigureManagerBase) -> None: ...
|
||||
@classmethod
|
||||
def set_active(cls, manager: FigureManagerBase) -> None: ...
|
||||
@classmethod
|
||||
def draw_all(cls, force: bool = ...) -> None: ...
|
||||
Binary file not shown.
@ -0,0 +1,82 @@
|
||||
"""
|
||||
Low-level text helper utilities.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
|
||||
from . import _api
|
||||
from .ft2font import KERNING_DEFAULT, LOAD_NO_HINTING, FT2Font
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class LayoutItem:
|
||||
ft_object: FT2Font
|
||||
char: str
|
||||
glyph_idx: int
|
||||
x: float
|
||||
prev_kern: float
|
||||
|
||||
|
||||
def warn_on_missing_glyph(codepoint, fontnames):
|
||||
_api.warn_external(
|
||||
f"Glyph {codepoint} "
|
||||
f"({chr(codepoint).encode('ascii', 'namereplace').decode('ascii')}) "
|
||||
f"missing from font(s) {fontnames}.")
|
||||
|
||||
block = ("Hebrew" if 0x0590 <= codepoint <= 0x05ff else
|
||||
"Arabic" if 0x0600 <= codepoint <= 0x06ff else
|
||||
"Devanagari" if 0x0900 <= codepoint <= 0x097f else
|
||||
"Bengali" if 0x0980 <= codepoint <= 0x09ff else
|
||||
"Gurmukhi" if 0x0a00 <= codepoint <= 0x0a7f else
|
||||
"Gujarati" if 0x0a80 <= codepoint <= 0x0aff else
|
||||
"Oriya" if 0x0b00 <= codepoint <= 0x0b7f else
|
||||
"Tamil" if 0x0b80 <= codepoint <= 0x0bff else
|
||||
"Telugu" if 0x0c00 <= codepoint <= 0x0c7f else
|
||||
"Kannada" if 0x0c80 <= codepoint <= 0x0cff else
|
||||
"Malayalam" if 0x0d00 <= codepoint <= 0x0d7f else
|
||||
"Sinhala" if 0x0d80 <= codepoint <= 0x0dff else
|
||||
None)
|
||||
if block:
|
||||
_api.warn_external(
|
||||
f"Matplotlib currently does not support {block} natively.")
|
||||
|
||||
|
||||
def layout(string, font, *, kern_mode=KERNING_DEFAULT):
|
||||
"""
|
||||
Render *string* with *font*.
|
||||
|
||||
For each character in *string*, yield a LayoutItem instance. When such an instance
|
||||
is yielded, the font's glyph is set to the corresponding character.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
string : str
|
||||
The string to be rendered.
|
||||
font : FT2Font
|
||||
The font.
|
||||
kern_mode : int
|
||||
A FreeType kerning mode.
|
||||
|
||||
Yields
|
||||
------
|
||||
LayoutItem
|
||||
"""
|
||||
x = 0
|
||||
prev_glyph_idx = None
|
||||
char_to_font = font._get_fontmap(string)
|
||||
base_font = font
|
||||
for char in string:
|
||||
# This has done the fallback logic
|
||||
font = char_to_font.get(char, base_font)
|
||||
glyph_idx = font.get_char_index(ord(char))
|
||||
kern = (
|
||||
base_font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64
|
||||
if prev_glyph_idx is not None else 0.
|
||||
)
|
||||
x += kern
|
||||
glyph = font.load_glyph(glyph_idx, flags=LOAD_NO_HINTING)
|
||||
yield LayoutItem(font, char, glyph_idx, x, kern)
|
||||
x += glyph.linearHoriAdvance / 65536
|
||||
prev_glyph_idx = glyph_idx
|
||||
84
venv/lib/python3.12/site-packages/matplotlib/_tight_bbox.py
Normal file
84
venv/lib/python3.12/site-packages/matplotlib/_tight_bbox.py
Normal file
@ -0,0 +1,84 @@
|
||||
"""
|
||||
Helper module for the *bbox_inches* parameter in `.Figure.savefig`.
|
||||
"""
|
||||
|
||||
from matplotlib.transforms import Bbox, TransformedBbox, Affine2D
|
||||
|
||||
|
||||
def adjust_bbox(fig, bbox_inches, fixed_dpi=None):
|
||||
"""
|
||||
Temporarily adjust the figure so that only the specified area
|
||||
(bbox_inches) is saved.
|
||||
|
||||
It modifies fig.bbox, fig.bbox_inches,
|
||||
fig.transFigure._boxout, and fig.patch. While the figure size
|
||||
changes, the scale of the original figure is conserved. A
|
||||
function which restores the original values are returned.
|
||||
"""
|
||||
origBbox = fig.bbox
|
||||
origBboxInches = fig.bbox_inches
|
||||
_boxout = fig.transFigure._boxout
|
||||
|
||||
old_aspect = []
|
||||
locator_list = []
|
||||
sentinel = object()
|
||||
for ax in fig.axes:
|
||||
locator = ax.get_axes_locator()
|
||||
if locator is not None:
|
||||
ax.apply_aspect(locator(ax, None))
|
||||
locator_list.append(locator)
|
||||
current_pos = ax.get_position(original=False).frozen()
|
||||
ax.set_axes_locator(lambda a, r, _pos=current_pos: _pos)
|
||||
# override the method that enforces the aspect ratio on the Axes
|
||||
if 'apply_aspect' in ax.__dict__:
|
||||
old_aspect.append(ax.apply_aspect)
|
||||
else:
|
||||
old_aspect.append(sentinel)
|
||||
ax.apply_aspect = lambda pos=None: None
|
||||
|
||||
def restore_bbox():
|
||||
for ax, loc, aspect in zip(fig.axes, locator_list, old_aspect):
|
||||
ax.set_axes_locator(loc)
|
||||
if aspect is sentinel:
|
||||
# delete our no-op function which un-hides the original method
|
||||
del ax.apply_aspect
|
||||
else:
|
||||
ax.apply_aspect = aspect
|
||||
|
||||
fig.bbox = origBbox
|
||||
fig.bbox_inches = origBboxInches
|
||||
fig.transFigure._boxout = _boxout
|
||||
fig.transFigure.invalidate()
|
||||
fig.patch.set_bounds(0, 0, 1, 1)
|
||||
|
||||
if fixed_dpi is None:
|
||||
fixed_dpi = fig.dpi
|
||||
tr = Affine2D().scale(fixed_dpi)
|
||||
dpi_scale = fixed_dpi / fig.dpi
|
||||
|
||||
fig.bbox_inches = Bbox.from_bounds(0, 0, *bbox_inches.size)
|
||||
x0, y0 = tr.transform(bbox_inches.p0)
|
||||
w1, h1 = fig.bbox.size * dpi_scale
|
||||
fig.transFigure._boxout = Bbox.from_bounds(-x0, -y0, w1, h1)
|
||||
fig.transFigure.invalidate()
|
||||
|
||||
fig.bbox = TransformedBbox(fig.bbox_inches, tr)
|
||||
|
||||
fig.patch.set_bounds(x0 / w1, y0 / h1,
|
||||
fig.bbox.width / w1, fig.bbox.height / h1)
|
||||
|
||||
return restore_bbox
|
||||
|
||||
|
||||
def process_figure_for_rasterizing(fig, bbox_inches_restore, fixed_dpi=None):
|
||||
"""
|
||||
A function that needs to be called when figure dpi changes during the
|
||||
drawing (e.g., rasterizing). It recovers the bbox and re-adjust it with
|
||||
the new dpi.
|
||||
"""
|
||||
|
||||
bbox_inches, restore_bbox = bbox_inches_restore
|
||||
restore_bbox()
|
||||
r = adjust_bbox(fig, bbox_inches, fixed_dpi)
|
||||
|
||||
return bbox_inches, r
|
||||
301
venv/lib/python3.12/site-packages/matplotlib/_tight_layout.py
Normal file
301
venv/lib/python3.12/site-packages/matplotlib/_tight_layout.py
Normal file
@ -0,0 +1,301 @@
|
||||
"""
|
||||
Routines to adjust subplot params so that subplots are
|
||||
nicely fit in the figure. In doing so, only axis labels, tick labels, Axes
|
||||
titles and offsetboxes that are anchored to Axes are currently considered.
|
||||
|
||||
Internally, this module assumes that the margins (left margin, etc.) which are
|
||||
differences between ``Axes.get_tightbbox`` and ``Axes.bbox`` are independent of
|
||||
Axes position. This may fail if ``Axes.adjustable`` is ``datalim`` as well as
|
||||
such cases as when left or right margin are affected by xlabel.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
import matplotlib as mpl
|
||||
from matplotlib import _api, artist as martist
|
||||
from matplotlib.font_manager import FontProperties
|
||||
from matplotlib.transforms import Bbox
|
||||
|
||||
|
||||
def _auto_adjust_subplotpars(
|
||||
fig, renderer, shape, span_pairs, subplot_list,
|
||||
ax_bbox_list=None, pad=1.08, h_pad=None, w_pad=None, rect=None):
|
||||
"""
|
||||
Return a dict of subplot parameters to adjust spacing between subplots
|
||||
or ``None`` if resulting Axes would have zero height or width.
|
||||
|
||||
Note that this function ignores geometry information of subplot itself, but
|
||||
uses what is given by the *shape* and *subplot_list* parameters. Also, the
|
||||
results could be incorrect if some subplots have ``adjustable=datalim``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
shape : tuple[int, int]
|
||||
Number of rows and columns of the grid.
|
||||
span_pairs : list[tuple[slice, slice]]
|
||||
List of rowspans and colspans occupied by each subplot.
|
||||
subplot_list : list of subplots
|
||||
List of subplots that will be used to calculate optimal subplot_params.
|
||||
pad : float
|
||||
Padding between the figure edge and the edges of subplots, as a
|
||||
fraction of the font size.
|
||||
h_pad, w_pad : float
|
||||
Padding (height/width) between edges of adjacent subplots, as a
|
||||
fraction of the font size. Defaults to *pad*.
|
||||
rect : tuple
|
||||
(left, bottom, right, top), default: None.
|
||||
"""
|
||||
rows, cols = shape
|
||||
|
||||
font_size_inch = (FontProperties(
|
||||
size=mpl.rcParams["font.size"]).get_size_in_points() / 72)
|
||||
pad_inch = pad * font_size_inch
|
||||
vpad_inch = h_pad * font_size_inch if h_pad is not None else pad_inch
|
||||
hpad_inch = w_pad * font_size_inch if w_pad is not None else pad_inch
|
||||
|
||||
if len(span_pairs) != len(subplot_list) or len(subplot_list) == 0:
|
||||
raise ValueError
|
||||
|
||||
if rect is None:
|
||||
margin_left = margin_bottom = margin_right = margin_top = None
|
||||
else:
|
||||
margin_left, margin_bottom, _right, _top = rect
|
||||
margin_right = 1 - _right if _right else None
|
||||
margin_top = 1 - _top if _top else None
|
||||
|
||||
vspaces = np.zeros((rows + 1, cols))
|
||||
hspaces = np.zeros((rows, cols + 1))
|
||||
|
||||
if ax_bbox_list is None:
|
||||
ax_bbox_list = [
|
||||
Bbox.union([ax.get_position(original=True) for ax in subplots])
|
||||
for subplots in subplot_list]
|
||||
|
||||
for subplots, ax_bbox, (rowspan, colspan) in zip(
|
||||
subplot_list, ax_bbox_list, span_pairs):
|
||||
if all(not ax.get_visible() for ax in subplots):
|
||||
continue
|
||||
|
||||
bb = []
|
||||
for ax in subplots:
|
||||
if ax.get_visible():
|
||||
bb += [martist._get_tightbbox_for_layout_only(ax, renderer)]
|
||||
|
||||
tight_bbox_raw = Bbox.union(bb)
|
||||
tight_bbox = fig.transFigure.inverted().transform_bbox(tight_bbox_raw)
|
||||
|
||||
hspaces[rowspan, colspan.start] += ax_bbox.xmin - tight_bbox.xmin # l
|
||||
hspaces[rowspan, colspan.stop] += tight_bbox.xmax - ax_bbox.xmax # r
|
||||
vspaces[rowspan.start, colspan] += tight_bbox.ymax - ax_bbox.ymax # t
|
||||
vspaces[rowspan.stop, colspan] += ax_bbox.ymin - tight_bbox.ymin # b
|
||||
|
||||
fig_width_inch, fig_height_inch = fig.get_size_inches()
|
||||
|
||||
# margins can be negative for Axes with aspect applied, so use max(, 0) to
|
||||
# make them nonnegative.
|
||||
if not margin_left:
|
||||
margin_left = max(hspaces[:, 0].max(), 0) + pad_inch/fig_width_inch
|
||||
suplabel = fig._supylabel
|
||||
if suplabel and suplabel.get_in_layout():
|
||||
rel_width = fig.transFigure.inverted().transform_bbox(
|
||||
suplabel.get_window_extent(renderer)).width
|
||||
margin_left += rel_width + pad_inch/fig_width_inch
|
||||
if not margin_right:
|
||||
margin_right = max(hspaces[:, -1].max(), 0) + pad_inch/fig_width_inch
|
||||
if not margin_top:
|
||||
margin_top = max(vspaces[0, :].max(), 0) + pad_inch/fig_height_inch
|
||||
if fig._suptitle and fig._suptitle.get_in_layout():
|
||||
rel_height = fig.transFigure.inverted().transform_bbox(
|
||||
fig._suptitle.get_window_extent(renderer)).height
|
||||
margin_top += rel_height + pad_inch/fig_height_inch
|
||||
if not margin_bottom:
|
||||
margin_bottom = max(vspaces[-1, :].max(), 0) + pad_inch/fig_height_inch
|
||||
suplabel = fig._supxlabel
|
||||
if suplabel and suplabel.get_in_layout():
|
||||
rel_height = fig.transFigure.inverted().transform_bbox(
|
||||
suplabel.get_window_extent(renderer)).height
|
||||
margin_bottom += rel_height + pad_inch/fig_height_inch
|
||||
|
||||
if margin_left + margin_right >= 1:
|
||||
_api.warn_external('Tight layout not applied. The left and right '
|
||||
'margins cannot be made large enough to '
|
||||
'accommodate all Axes decorations.')
|
||||
return None
|
||||
if margin_bottom + margin_top >= 1:
|
||||
_api.warn_external('Tight layout not applied. The bottom and top '
|
||||
'margins cannot be made large enough to '
|
||||
'accommodate all Axes decorations.')
|
||||
return None
|
||||
|
||||
kwargs = dict(left=margin_left,
|
||||
right=1 - margin_right,
|
||||
bottom=margin_bottom,
|
||||
top=1 - margin_top)
|
||||
|
||||
if cols > 1:
|
||||
hspace = hspaces[:, 1:-1].max() + hpad_inch / fig_width_inch
|
||||
# axes widths:
|
||||
h_axes = (1 - margin_right - margin_left - hspace * (cols - 1)) / cols
|
||||
if h_axes < 0:
|
||||
_api.warn_external('Tight layout not applied. tight_layout '
|
||||
'cannot make Axes width small enough to '
|
||||
'accommodate all Axes decorations')
|
||||
return None
|
||||
else:
|
||||
kwargs["wspace"] = hspace / h_axes
|
||||
if rows > 1:
|
||||
vspace = vspaces[1:-1, :].max() + vpad_inch / fig_height_inch
|
||||
v_axes = (1 - margin_top - margin_bottom - vspace * (rows - 1)) / rows
|
||||
if v_axes < 0:
|
||||
_api.warn_external('Tight layout not applied. tight_layout '
|
||||
'cannot make Axes height small enough to '
|
||||
'accommodate all Axes decorations.')
|
||||
return None
|
||||
else:
|
||||
kwargs["hspace"] = vspace / v_axes
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
def get_subplotspec_list(axes_list, grid_spec=None):
|
||||
"""
|
||||
Return a list of subplotspec from the given list of Axes.
|
||||
|
||||
For an instance of Axes that does not support subplotspec, None is inserted
|
||||
in the list.
|
||||
|
||||
If grid_spec is given, None is inserted for those not from the given
|
||||
grid_spec.
|
||||
"""
|
||||
subplotspec_list = []
|
||||
for ax in axes_list:
|
||||
axes_or_locator = ax.get_axes_locator()
|
||||
if axes_or_locator is None:
|
||||
axes_or_locator = ax
|
||||
|
||||
if hasattr(axes_or_locator, "get_subplotspec"):
|
||||
subplotspec = axes_or_locator.get_subplotspec()
|
||||
if subplotspec is not None:
|
||||
subplotspec = subplotspec.get_topmost_subplotspec()
|
||||
gs = subplotspec.get_gridspec()
|
||||
if grid_spec is not None:
|
||||
if gs != grid_spec:
|
||||
subplotspec = None
|
||||
elif gs.locally_modified_subplot_params():
|
||||
subplotspec = None
|
||||
else:
|
||||
subplotspec = None
|
||||
|
||||
subplotspec_list.append(subplotspec)
|
||||
|
||||
return subplotspec_list
|
||||
|
||||
|
||||
def get_tight_layout_figure(fig, axes_list, subplotspec_list, renderer,
|
||||
pad=1.08, h_pad=None, w_pad=None, rect=None):
|
||||
"""
|
||||
Return subplot parameters for tight-layouted-figure with specified padding.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fig : Figure
|
||||
axes_list : list of Axes
|
||||
subplotspec_list : list of `.SubplotSpec`
|
||||
The subplotspecs of each Axes.
|
||||
renderer : renderer
|
||||
pad : float
|
||||
Padding between the figure edge and the edges of subplots, as a
|
||||
fraction of the font size.
|
||||
h_pad, w_pad : float
|
||||
Padding (height/width) between edges of adjacent subplots. Defaults to
|
||||
*pad*.
|
||||
rect : tuple (left, bottom, right, top), default: None.
|
||||
rectangle in normalized figure coordinates
|
||||
that the whole subplots area (including labels) will fit into.
|
||||
Defaults to using the entire figure.
|
||||
|
||||
Returns
|
||||
-------
|
||||
subplotspec or None
|
||||
subplotspec kwargs to be passed to `.Figure.subplots_adjust` or
|
||||
None if tight_layout could not be accomplished.
|
||||
"""
|
||||
|
||||
# Multiple Axes can share same subplotspec (e.g., if using axes_grid1);
|
||||
# we need to group them together.
|
||||
ss_to_subplots = {ss: [] for ss in subplotspec_list}
|
||||
for ax, ss in zip(axes_list, subplotspec_list):
|
||||
ss_to_subplots[ss].append(ax)
|
||||
if ss_to_subplots.pop(None, None):
|
||||
_api.warn_external(
|
||||
"This figure includes Axes that are not compatible with "
|
||||
"tight_layout, so results might be incorrect.")
|
||||
if not ss_to_subplots:
|
||||
return {}
|
||||
subplot_list = list(ss_to_subplots.values())
|
||||
ax_bbox_list = [ss.get_position(fig) for ss in ss_to_subplots]
|
||||
|
||||
max_nrows = max(ss.get_gridspec().nrows for ss in ss_to_subplots)
|
||||
max_ncols = max(ss.get_gridspec().ncols for ss in ss_to_subplots)
|
||||
|
||||
span_pairs = []
|
||||
for ss in ss_to_subplots:
|
||||
# The intent here is to support Axes from different gridspecs where
|
||||
# one's nrows (or ncols) is a multiple of the other (e.g. 2 and 4),
|
||||
# but this doesn't actually work because the computed wspace, in
|
||||
# relative-axes-height, corresponds to different physical spacings for
|
||||
# the 2-row grid and the 4-row grid. Still, this code is left, mostly
|
||||
# for backcompat.
|
||||
rows, cols = ss.get_gridspec().get_geometry()
|
||||
div_row, mod_row = divmod(max_nrows, rows)
|
||||
div_col, mod_col = divmod(max_ncols, cols)
|
||||
if mod_row != 0:
|
||||
_api.warn_external('tight_layout not applied: number of rows '
|
||||
'in subplot specifications must be '
|
||||
'multiples of one another.')
|
||||
return {}
|
||||
if mod_col != 0:
|
||||
_api.warn_external('tight_layout not applied: number of '
|
||||
'columns in subplot specifications must be '
|
||||
'multiples of one another.')
|
||||
return {}
|
||||
span_pairs.append((
|
||||
slice(ss.rowspan.start * div_row, ss.rowspan.stop * div_row),
|
||||
slice(ss.colspan.start * div_col, ss.colspan.stop * div_col)))
|
||||
|
||||
kwargs = _auto_adjust_subplotpars(fig, renderer,
|
||||
shape=(max_nrows, max_ncols),
|
||||
span_pairs=span_pairs,
|
||||
subplot_list=subplot_list,
|
||||
ax_bbox_list=ax_bbox_list,
|
||||
pad=pad, h_pad=h_pad, w_pad=w_pad)
|
||||
|
||||
# kwargs can be none if tight_layout fails...
|
||||
if rect is not None and kwargs is not None:
|
||||
# if rect is given, the whole subplots area (including
|
||||
# labels) will fit into the rect instead of the
|
||||
# figure. Note that the rect argument of
|
||||
# *auto_adjust_subplotpars* specify the area that will be
|
||||
# covered by the total area of axes.bbox. Thus we call
|
||||
# auto_adjust_subplotpars twice, where the second run
|
||||
# with adjusted rect parameters.
|
||||
|
||||
left, bottom, right, top = rect
|
||||
if left is not None:
|
||||
left += kwargs["left"]
|
||||
if bottom is not None:
|
||||
bottom += kwargs["bottom"]
|
||||
if right is not None:
|
||||
right -= (1 - kwargs["right"])
|
||||
if top is not None:
|
||||
top -= (1 - kwargs["top"])
|
||||
|
||||
kwargs = _auto_adjust_subplotpars(fig, renderer,
|
||||
shape=(max_nrows, max_ncols),
|
||||
span_pairs=span_pairs,
|
||||
subplot_list=subplot_list,
|
||||
ax_bbox_list=ax_bbox_list,
|
||||
pad=pad, h_pad=h_pad, w_pad=w_pad,
|
||||
rect=(left, bottom, right, top))
|
||||
|
||||
return kwargs
|
||||
Binary file not shown.
26
venv/lib/python3.12/site-packages/matplotlib/_tri.pyi
Normal file
26
venv/lib/python3.12/site-packages/matplotlib/_tri.pyi
Normal file
@ -0,0 +1,26 @@
|
||||
# This is a private module implemented in C++
|
||||
# As such these type stubs are overly generic, but here to allow these types
|
||||
# as return types for public methods
|
||||
from typing import Any, final
|
||||
|
||||
@final
|
||||
class TrapezoidMapTriFinder:
|
||||
def __init__(self, *args, **kwargs) -> None: ...
|
||||
def find_many(self, *args, **kwargs) -> Any: ...
|
||||
def get_tree_stats(self, *args, **kwargs) -> Any: ...
|
||||
def initialize(self, *args, **kwargs) -> Any: ...
|
||||
def print_tree(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
@final
|
||||
class TriContourGenerator:
|
||||
def __init__(self, *args, **kwargs) -> None: ...
|
||||
def create_contour(self, *args, **kwargs) -> Any: ...
|
||||
def create_filled_contour(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
@final
|
||||
class Triangulation:
|
||||
def __init__(self, *args, **kwargs) -> None: ...
|
||||
def calculate_plane_coefficients(self, *args, **kwargs) -> Any: ...
|
||||
def get_edges(self, *args, **kwargs) -> Any: ...
|
||||
def get_neighbors(self, *args, **kwargs) -> Any: ...
|
||||
def set_mask(self, *args, **kwargs) -> Any: ...
|
||||
Binary file not shown.
879
venv/lib/python3.12/site-packages/matplotlib/_type1font.py
Normal file
879
venv/lib/python3.12/site-packages/matplotlib/_type1font.py
Normal file
@ -0,0 +1,879 @@
|
||||
"""
|
||||
A class representing a Type 1 font.
|
||||
|
||||
This version reads pfa and pfb files and splits them for embedding in
|
||||
pdf files. It also supports SlantFont and ExtendFont transformations,
|
||||
similarly to pdfTeX and friends. There is no support yet for subsetting.
|
||||
|
||||
Usage::
|
||||
|
||||
font = Type1Font(filename)
|
||||
clear_part, encrypted_part, finale = font.parts
|
||||
slanted_font = font.transform({'slant': 0.167})
|
||||
extended_font = font.transform({'extend': 1.2})
|
||||
|
||||
Sources:
|
||||
|
||||
* Adobe Technical Note #5040, Supporting Downloadable PostScript
|
||||
Language Fonts.
|
||||
|
||||
* Adobe Type 1 Font Format, Adobe Systems Incorporated, third printing,
|
||||
v1.1, 1993. ISBN 0-201-57044-0.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import binascii
|
||||
import functools
|
||||
import logging
|
||||
import re
|
||||
import string
|
||||
import struct
|
||||
import typing as T
|
||||
|
||||
import numpy as np
|
||||
|
||||
from matplotlib.cbook import _format_approx
|
||||
from . import _api
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _Token:
|
||||
"""
|
||||
A token in a PostScript stream.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
pos : int
|
||||
Position, i.e. offset from the beginning of the data.
|
||||
raw : str
|
||||
Raw text of the token.
|
||||
kind : str
|
||||
Description of the token (for debugging or testing).
|
||||
"""
|
||||
__slots__ = ('pos', 'raw')
|
||||
kind = '?'
|
||||
|
||||
def __init__(self, pos, raw):
|
||||
_log.debug('type1font._Token %s at %d: %r', self.kind, pos, raw)
|
||||
self.pos = pos
|
||||
self.raw = raw
|
||||
|
||||
def __str__(self):
|
||||
return f"<{self.kind} {self.raw} @{self.pos}>"
|
||||
|
||||
def endpos(self):
|
||||
"""Position one past the end of the token"""
|
||||
return self.pos + len(self.raw)
|
||||
|
||||
def is_keyword(self, *names):
|
||||
"""Is this a name token with one of the names?"""
|
||||
return False
|
||||
|
||||
def is_slash_name(self):
|
||||
"""Is this a name token that starts with a slash?"""
|
||||
return False
|
||||
|
||||
def is_delim(self):
|
||||
"""Is this a delimiter token?"""
|
||||
return False
|
||||
|
||||
def is_number(self):
|
||||
"""Is this a number token?"""
|
||||
return False
|
||||
|
||||
def value(self):
|
||||
return self.raw
|
||||
|
||||
|
||||
class _NameToken(_Token):
|
||||
kind = 'name'
|
||||
|
||||
def is_slash_name(self):
|
||||
return self.raw.startswith('/')
|
||||
|
||||
def value(self):
|
||||
return self.raw[1:]
|
||||
|
||||
|
||||
class _BooleanToken(_Token):
|
||||
kind = 'boolean'
|
||||
|
||||
def value(self):
|
||||
return self.raw == 'true'
|
||||
|
||||
|
||||
class _KeywordToken(_Token):
|
||||
kind = 'keyword'
|
||||
|
||||
def is_keyword(self, *names):
|
||||
return self.raw in names
|
||||
|
||||
|
||||
class _DelimiterToken(_Token):
|
||||
kind = 'delimiter'
|
||||
|
||||
def is_delim(self):
|
||||
return True
|
||||
|
||||
def opposite(self):
|
||||
return {'[': ']', ']': '[',
|
||||
'{': '}', '}': '{',
|
||||
'<<': '>>', '>>': '<<'
|
||||
}[self.raw]
|
||||
|
||||
|
||||
class _WhitespaceToken(_Token):
|
||||
kind = 'whitespace'
|
||||
|
||||
|
||||
class _StringToken(_Token):
|
||||
kind = 'string'
|
||||
_escapes_re = re.compile(r'\\([\\()nrtbf]|[0-7]{1,3})')
|
||||
_replacements = {'\\': '\\', '(': '(', ')': ')', 'n': '\n',
|
||||
'r': '\r', 't': '\t', 'b': '\b', 'f': '\f'}
|
||||
_ws_re = re.compile('[\0\t\r\f\n ]')
|
||||
|
||||
@classmethod
|
||||
def _escape(cls, match):
|
||||
group = match.group(1)
|
||||
try:
|
||||
return cls._replacements[group]
|
||||
except KeyError:
|
||||
return chr(int(group, 8))
|
||||
|
||||
@functools.lru_cache
|
||||
def value(self):
|
||||
if self.raw[0] == '(':
|
||||
return self._escapes_re.sub(self._escape, self.raw[1:-1])
|
||||
else:
|
||||
data = self._ws_re.sub('', self.raw[1:-1])
|
||||
if len(data) % 2 == 1:
|
||||
data += '0'
|
||||
return binascii.unhexlify(data)
|
||||
|
||||
|
||||
class _BinaryToken(_Token):
|
||||
kind = 'binary'
|
||||
|
||||
def value(self):
|
||||
return self.raw[1:]
|
||||
|
||||
|
||||
class _NumberToken(_Token):
|
||||
kind = 'number'
|
||||
|
||||
def is_number(self):
|
||||
return True
|
||||
|
||||
def value(self):
|
||||
if '.' not in self.raw:
|
||||
return int(self.raw)
|
||||
else:
|
||||
return float(self.raw)
|
||||
|
||||
|
||||
def _tokenize(data: bytes, skip_ws: bool) -> T.Generator[_Token, int, None]:
|
||||
"""
|
||||
A generator that produces _Token instances from Type-1 font code.
|
||||
|
||||
The consumer of the generator may send an integer to the tokenizer to
|
||||
indicate that the next token should be _BinaryToken of the given length.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : bytes
|
||||
The data of the font to tokenize.
|
||||
|
||||
skip_ws : bool
|
||||
If true, the generator will drop any _WhitespaceTokens from the output.
|
||||
"""
|
||||
|
||||
text = data.decode('ascii', 'replace')
|
||||
whitespace_or_comment_re = re.compile(r'[\0\t\r\f\n ]+|%[^\r\n]*')
|
||||
token_re = re.compile(r'/{0,2}[^]\0\t\r\f\n ()<>{}/%[]+')
|
||||
instring_re = re.compile(r'[()\\]')
|
||||
hex_re = re.compile(r'^<[0-9a-fA-F\0\t\r\f\n ]*>$')
|
||||
oct_re = re.compile(r'[0-7]{1,3}')
|
||||
pos = 0
|
||||
next_binary: int | None = None
|
||||
|
||||
while pos < len(text):
|
||||
if next_binary is not None:
|
||||
n = next_binary
|
||||
next_binary = (yield _BinaryToken(pos, data[pos:pos+n]))
|
||||
pos += n
|
||||
continue
|
||||
match = whitespace_or_comment_re.match(text, pos)
|
||||
if match:
|
||||
if not skip_ws:
|
||||
next_binary = (yield _WhitespaceToken(pos, match.group()))
|
||||
pos = match.end()
|
||||
elif text[pos] == '(':
|
||||
# PostScript string rules:
|
||||
# - parentheses must be balanced
|
||||
# - backslashes escape backslashes and parens
|
||||
# - also codes \n\r\t\b\f and octal escapes are recognized
|
||||
# - other backslashes do not escape anything
|
||||
start = pos
|
||||
pos += 1
|
||||
depth = 1
|
||||
while depth:
|
||||
match = instring_re.search(text, pos)
|
||||
if match is None:
|
||||
raise ValueError(
|
||||
f'Unterminated string starting at {start}')
|
||||
pos = match.end()
|
||||
if match.group() == '(':
|
||||
depth += 1
|
||||
elif match.group() == ')':
|
||||
depth -= 1
|
||||
else: # a backslash
|
||||
char = text[pos]
|
||||
if char in r'\()nrtbf':
|
||||
pos += 1
|
||||
else:
|
||||
octal = oct_re.match(text, pos)
|
||||
if octal:
|
||||
pos = octal.end()
|
||||
else:
|
||||
pass # non-escaping backslash
|
||||
next_binary = (yield _StringToken(start, text[start:pos]))
|
||||
elif text[pos:pos + 2] in ('<<', '>>'):
|
||||
next_binary = (yield _DelimiterToken(pos, text[pos:pos + 2]))
|
||||
pos += 2
|
||||
elif text[pos] == '<':
|
||||
start = pos
|
||||
try:
|
||||
pos = text.index('>', pos) + 1
|
||||
except ValueError as e:
|
||||
raise ValueError(f'Unterminated hex string starting at {start}'
|
||||
) from e
|
||||
if not hex_re.match(text[start:pos]):
|
||||
raise ValueError(f'Malformed hex string starting at {start}')
|
||||
next_binary = (yield _StringToken(pos, text[start:pos]))
|
||||
else:
|
||||
match = token_re.match(text, pos)
|
||||
if match:
|
||||
raw = match.group()
|
||||
if raw.startswith('/'):
|
||||
next_binary = (yield _NameToken(pos, raw))
|
||||
elif match.group() in ('true', 'false'):
|
||||
next_binary = (yield _BooleanToken(pos, raw))
|
||||
else:
|
||||
try:
|
||||
float(raw)
|
||||
next_binary = (yield _NumberToken(pos, raw))
|
||||
except ValueError:
|
||||
next_binary = (yield _KeywordToken(pos, raw))
|
||||
pos = match.end()
|
||||
else:
|
||||
next_binary = (yield _DelimiterToken(pos, text[pos]))
|
||||
pos += 1
|
||||
|
||||
|
||||
class _BalancedExpression(_Token):
|
||||
pass
|
||||
|
||||
|
||||
def _expression(initial, tokens, data):
|
||||
"""
|
||||
Consume some number of tokens and return a balanced PostScript expression.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
initial : _Token
|
||||
The token that triggered parsing a balanced expression.
|
||||
tokens : iterator of _Token
|
||||
Following tokens.
|
||||
data : bytes
|
||||
Underlying data that the token positions point to.
|
||||
|
||||
Returns
|
||||
-------
|
||||
_BalancedExpression
|
||||
"""
|
||||
delim_stack = []
|
||||
token = initial
|
||||
while True:
|
||||
if token.is_delim():
|
||||
if token.raw in ('[', '{'):
|
||||
delim_stack.append(token)
|
||||
elif token.raw in (']', '}'):
|
||||
if not delim_stack:
|
||||
raise RuntimeError(f"unmatched closing token {token}")
|
||||
match = delim_stack.pop()
|
||||
if match.raw != token.opposite():
|
||||
raise RuntimeError(
|
||||
f"opening token {match} closed by {token}"
|
||||
)
|
||||
if not delim_stack:
|
||||
break
|
||||
else:
|
||||
raise RuntimeError(f'unknown delimiter {token}')
|
||||
elif not delim_stack:
|
||||
break
|
||||
token = next(tokens)
|
||||
return _BalancedExpression(
|
||||
initial.pos,
|
||||
data[initial.pos:token.endpos()].decode('ascii', 'replace')
|
||||
)
|
||||
|
||||
|
||||
class Type1Font:
|
||||
"""
|
||||
A class representing a Type-1 font, for use by backends.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
parts : tuple
|
||||
A 3-tuple of the cleartext part, the encrypted part, and the finale of
|
||||
zeros.
|
||||
|
||||
decrypted : bytes
|
||||
The decrypted form of ``parts[1]``.
|
||||
|
||||
prop : dict[str, Any]
|
||||
A dictionary of font properties. Noteworthy keys include:
|
||||
|
||||
- FontName: PostScript name of the font
|
||||
- Encoding: dict from numeric codes to glyph names
|
||||
- FontMatrix: bytes object encoding a matrix
|
||||
- UniqueID: optional font identifier, dropped when modifying the font
|
||||
- CharStrings: dict from glyph names to byte code
|
||||
- Subrs: array of byte code subroutines
|
||||
- OtherSubrs: bytes object encoding some PostScript code
|
||||
"""
|
||||
__slots__ = ('parts', 'decrypted', 'prop', '_pos', '_abbr')
|
||||
# the _pos dict contains (begin, end) indices to parts[0] + decrypted
|
||||
# so that they can be replaced when transforming the font;
|
||||
# but since sometimes a definition appears in both parts[0] and decrypted,
|
||||
# _pos[name] is an array of such pairs
|
||||
#
|
||||
# _abbr maps three standard abbreviations to their particular names in
|
||||
# this font (e.g. 'RD' is named '-|' in some fonts)
|
||||
|
||||
def __init__(self, input):
|
||||
"""
|
||||
Initialize a Type-1 font.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input : str or 3-tuple
|
||||
Either a pfb file name, or a 3-tuple of already-decoded Type-1
|
||||
font `~.Type1Font.parts`.
|
||||
"""
|
||||
if isinstance(input, tuple) and len(input) == 3:
|
||||
self.parts = input
|
||||
else:
|
||||
with open(input, 'rb') as file:
|
||||
data = self._read(file)
|
||||
self.parts = self._split(data)
|
||||
|
||||
self.decrypted = self._decrypt(self.parts[1], 'eexec')
|
||||
self._abbr = {'RD': 'RD', 'ND': 'ND', 'NP': 'NP'}
|
||||
self._parse()
|
||||
|
||||
def _read(self, file):
|
||||
"""Read the font from a file, decoding into usable parts."""
|
||||
rawdata = file.read()
|
||||
if not rawdata.startswith(b'\x80'):
|
||||
return rawdata
|
||||
|
||||
data = b''
|
||||
while rawdata:
|
||||
if not rawdata.startswith(b'\x80'):
|
||||
raise RuntimeError('Broken pfb file (expected byte 128, '
|
||||
'got %d)' % rawdata[0])
|
||||
type = rawdata[1]
|
||||
if type in (1, 2):
|
||||
length, = struct.unpack('<i', rawdata[2:6])
|
||||
segment = rawdata[6:6 + length]
|
||||
rawdata = rawdata[6 + length:]
|
||||
|
||||
if type == 1: # ASCII text: include verbatim
|
||||
data += segment
|
||||
elif type == 2: # binary data: encode in hexadecimal
|
||||
data += binascii.hexlify(segment)
|
||||
elif type == 3: # end of file
|
||||
break
|
||||
else:
|
||||
raise RuntimeError('Unknown segment type %d in pfb file' % type)
|
||||
|
||||
return data
|
||||
|
||||
def _split(self, data):
|
||||
"""
|
||||
Split the Type 1 font into its three main parts.
|
||||
|
||||
The three parts are: (1) the cleartext part, which ends in a
|
||||
eexec operator; (2) the encrypted part; (3) the fixed part,
|
||||
which contains 512 ASCII zeros possibly divided on various
|
||||
lines, a cleartomark operator, and possibly something else.
|
||||
"""
|
||||
|
||||
# Cleartext part: just find the eexec and skip whitespace
|
||||
idx = data.index(b'eexec')
|
||||
idx += len(b'eexec')
|
||||
while data[idx] in b' \t\r\n':
|
||||
idx += 1
|
||||
len1 = idx
|
||||
|
||||
# Encrypted part: find the cleartomark operator and count
|
||||
# zeros backward
|
||||
idx = data.rindex(b'cleartomark') - 1
|
||||
zeros = 512
|
||||
while zeros and data[idx] in b'0' or data[idx] in b'\r\n':
|
||||
if data[idx] in b'0':
|
||||
zeros -= 1
|
||||
idx -= 1
|
||||
if zeros:
|
||||
# this may have been a problem on old implementations that
|
||||
# used the zeros as necessary padding
|
||||
_log.info('Insufficiently many zeros in Type 1 font')
|
||||
|
||||
# Convert encrypted part to binary (if we read a pfb file, we may end
|
||||
# up converting binary to hexadecimal to binary again; but if we read
|
||||
# a pfa file, this part is already in hex, and I am not quite sure if
|
||||
# even the pfb format guarantees that it will be in binary).
|
||||
idx1 = len1 + ((idx - len1 + 2) & ~1) # ensure an even number of bytes
|
||||
binary = binascii.unhexlify(data[len1:idx1])
|
||||
|
||||
return data[:len1], binary, data[idx+1:]
|
||||
|
||||
@staticmethod
|
||||
def _decrypt(ciphertext, key, ndiscard=4):
|
||||
"""
|
||||
Decrypt ciphertext using the Type-1 font algorithm.
|
||||
|
||||
The algorithm is described in Adobe's "Adobe Type 1 Font Format".
|
||||
The key argument can be an integer, or one of the strings
|
||||
'eexec' and 'charstring', which map to the key specified for the
|
||||
corresponding part of Type-1 fonts.
|
||||
|
||||
The ndiscard argument should be an integer, usually 4.
|
||||
That number of bytes is discarded from the beginning of plaintext.
|
||||
"""
|
||||
|
||||
key = _api.check_getitem({'eexec': 55665, 'charstring': 4330}, key=key)
|
||||
plaintext = []
|
||||
for byte in ciphertext:
|
||||
plaintext.append(byte ^ (key >> 8))
|
||||
key = ((key+byte) * 52845 + 22719) & 0xffff
|
||||
|
||||
return bytes(plaintext[ndiscard:])
|
||||
|
||||
@staticmethod
|
||||
def _encrypt(plaintext, key, ndiscard=4):
|
||||
"""
|
||||
Encrypt plaintext using the Type-1 font algorithm.
|
||||
|
||||
The algorithm is described in Adobe's "Adobe Type 1 Font Format".
|
||||
The key argument can be an integer, or one of the strings
|
||||
'eexec' and 'charstring', which map to the key specified for the
|
||||
corresponding part of Type-1 fonts.
|
||||
|
||||
The ndiscard argument should be an integer, usually 4. That
|
||||
number of bytes is prepended to the plaintext before encryption.
|
||||
This function prepends NUL bytes for reproducibility, even though
|
||||
the original algorithm uses random bytes, presumably to avoid
|
||||
cryptanalysis.
|
||||
"""
|
||||
|
||||
key = _api.check_getitem({'eexec': 55665, 'charstring': 4330}, key=key)
|
||||
ciphertext = []
|
||||
for byte in b'\0' * ndiscard + plaintext:
|
||||
c = byte ^ (key >> 8)
|
||||
ciphertext.append(c)
|
||||
key = ((key + c) * 52845 + 22719) & 0xffff
|
||||
|
||||
return bytes(ciphertext)
|
||||
|
||||
def _parse(self):
|
||||
"""
|
||||
Find the values of various font properties. This limited kind
|
||||
of parsing is described in Chapter 10 "Adobe Type Manager
|
||||
Compatibility" of the Type-1 spec.
|
||||
"""
|
||||
# Start with reasonable defaults
|
||||
prop = {'Weight': 'Regular', 'ItalicAngle': 0.0, 'isFixedPitch': False,
|
||||
'UnderlinePosition': -100, 'UnderlineThickness': 50}
|
||||
pos = {}
|
||||
data = self.parts[0] + self.decrypted
|
||||
|
||||
source = _tokenize(data, True)
|
||||
while True:
|
||||
# See if there is a key to be assigned a value
|
||||
# e.g. /FontName in /FontName /Helvetica def
|
||||
try:
|
||||
token = next(source)
|
||||
except StopIteration:
|
||||
break
|
||||
if token.is_delim():
|
||||
# skip over this - we want top-level keys only
|
||||
_expression(token, source, data)
|
||||
if token.is_slash_name():
|
||||
key = token.value()
|
||||
keypos = token.pos
|
||||
else:
|
||||
continue
|
||||
|
||||
# Some values need special parsing
|
||||
if key in ('Subrs', 'CharStrings', 'Encoding', 'OtherSubrs'):
|
||||
prop[key], endpos = {
|
||||
'Subrs': self._parse_subrs,
|
||||
'CharStrings': self._parse_charstrings,
|
||||
'Encoding': self._parse_encoding,
|
||||
'OtherSubrs': self._parse_othersubrs
|
||||
}[key](source, data)
|
||||
pos.setdefault(key, []).append((keypos, endpos))
|
||||
continue
|
||||
|
||||
try:
|
||||
token = next(source)
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
if isinstance(token, _KeywordToken):
|
||||
# constructs like
|
||||
# FontDirectory /Helvetica known {...} {...} ifelse
|
||||
# mean the key was not really a key
|
||||
continue
|
||||
|
||||
if token.is_delim():
|
||||
value = _expression(token, source, data).raw
|
||||
else:
|
||||
value = token.value()
|
||||
|
||||
# look for a 'def' possibly preceded by access modifiers
|
||||
try:
|
||||
kw = next(
|
||||
kw for kw in source
|
||||
if not kw.is_keyword('readonly', 'noaccess', 'executeonly')
|
||||
)
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
# sometimes noaccess def and readonly def are abbreviated
|
||||
if kw.is_keyword('def', self._abbr['ND'], self._abbr['NP']):
|
||||
prop[key] = value
|
||||
pos.setdefault(key, []).append((keypos, kw.endpos()))
|
||||
|
||||
# detect the standard abbreviations
|
||||
if value == '{noaccess def}':
|
||||
self._abbr['ND'] = key
|
||||
elif value == '{noaccess put}':
|
||||
self._abbr['NP'] = key
|
||||
elif value == '{string currentfile exch readstring pop}':
|
||||
self._abbr['RD'] = key
|
||||
|
||||
# Fill in the various *Name properties
|
||||
if 'FontName' not in prop:
|
||||
prop['FontName'] = (prop.get('FullName') or
|
||||
prop.get('FamilyName') or
|
||||
'Unknown')
|
||||
if 'FullName' not in prop:
|
||||
prop['FullName'] = prop['FontName']
|
||||
if 'FamilyName' not in prop:
|
||||
extras = ('(?i)([ -](regular|plain|italic|oblique|(semi)?bold|'
|
||||
'(ultra)?light|extra|condensed))+$')
|
||||
prop['FamilyName'] = re.sub(extras, '', prop['FullName'])
|
||||
# Decrypt the encrypted parts
|
||||
ndiscard = prop.get('lenIV', 4)
|
||||
cs = prop['CharStrings']
|
||||
for key, value in cs.items():
|
||||
cs[key] = self._decrypt(value, 'charstring', ndiscard)
|
||||
if 'Subrs' in prop:
|
||||
prop['Subrs'] = [
|
||||
self._decrypt(value, 'charstring', ndiscard)
|
||||
for value in prop['Subrs']
|
||||
]
|
||||
|
||||
self.prop = prop
|
||||
self._pos = pos
|
||||
|
||||
def _parse_subrs(self, tokens, _data):
|
||||
count_token = next(tokens)
|
||||
if not count_token.is_number():
|
||||
raise RuntimeError(
|
||||
f"Token following /Subrs must be a number, was {count_token}"
|
||||
)
|
||||
count = count_token.value()
|
||||
array = [None] * count
|
||||
next(t for t in tokens if t.is_keyword('array'))
|
||||
for _ in range(count):
|
||||
next(t for t in tokens if t.is_keyword('dup'))
|
||||
index_token = next(tokens)
|
||||
if not index_token.is_number():
|
||||
raise RuntimeError(
|
||||
"Token following dup in Subrs definition must be a "
|
||||
f"number, was {index_token}"
|
||||
)
|
||||
nbytes_token = next(tokens)
|
||||
if not nbytes_token.is_number():
|
||||
raise RuntimeError(
|
||||
"Second token following dup in Subrs definition must "
|
||||
f"be a number, was {nbytes_token}"
|
||||
)
|
||||
token = next(tokens)
|
||||
if not token.is_keyword(self._abbr['RD']):
|
||||
raise RuntimeError(
|
||||
f"Token preceding subr must be {self._abbr['RD']}, "
|
||||
f"was {token}"
|
||||
)
|
||||
binary_token = tokens.send(1+nbytes_token.value())
|
||||
array[index_token.value()] = binary_token.value()
|
||||
|
||||
return array, next(tokens).endpos()
|
||||
|
||||
@staticmethod
|
||||
def _parse_charstrings(tokens, _data):
|
||||
count_token = next(tokens)
|
||||
if not count_token.is_number():
|
||||
raise RuntimeError(
|
||||
"Token following /CharStrings must be a number, "
|
||||
f"was {count_token}"
|
||||
)
|
||||
count = count_token.value()
|
||||
charstrings = {}
|
||||
next(t for t in tokens if t.is_keyword('begin'))
|
||||
while True:
|
||||
token = next(t for t in tokens
|
||||
if t.is_keyword('end') or t.is_slash_name())
|
||||
if token.raw == 'end':
|
||||
return charstrings, token.endpos()
|
||||
glyphname = token.value()
|
||||
nbytes_token = next(tokens)
|
||||
if not nbytes_token.is_number():
|
||||
raise RuntimeError(
|
||||
f"Token following /{glyphname} in CharStrings definition "
|
||||
f"must be a number, was {nbytes_token}"
|
||||
)
|
||||
next(tokens) # usually RD or |-
|
||||
binary_token = tokens.send(1+nbytes_token.value())
|
||||
charstrings[glyphname] = binary_token.value()
|
||||
|
||||
@staticmethod
|
||||
def _parse_encoding(tokens, _data):
|
||||
# this only works for encodings that follow the Adobe manual
|
||||
# but some old fonts include non-compliant data - we log a warning
|
||||
# and return a possibly incomplete encoding
|
||||
encoding = {}
|
||||
while True:
|
||||
token = next(t for t in tokens
|
||||
if t.is_keyword('StandardEncoding', 'dup', 'def'))
|
||||
if token.is_keyword('StandardEncoding'):
|
||||
return _StandardEncoding, token.endpos()
|
||||
if token.is_keyword('def'):
|
||||
return encoding, token.endpos()
|
||||
index_token = next(tokens)
|
||||
if not index_token.is_number():
|
||||
_log.warning(
|
||||
f"Parsing encoding: expected number, got {index_token}"
|
||||
)
|
||||
continue
|
||||
name_token = next(tokens)
|
||||
if not name_token.is_slash_name():
|
||||
_log.warning(
|
||||
f"Parsing encoding: expected slash-name, got {name_token}"
|
||||
)
|
||||
continue
|
||||
encoding[index_token.value()] = name_token.value()
|
||||
|
||||
@staticmethod
|
||||
def _parse_othersubrs(tokens, data):
|
||||
init_pos = None
|
||||
while True:
|
||||
token = next(tokens)
|
||||
if init_pos is None:
|
||||
init_pos = token.pos
|
||||
if token.is_delim():
|
||||
_expression(token, tokens, data)
|
||||
elif token.is_keyword('def', 'ND', '|-'):
|
||||
return data[init_pos:token.endpos()], token.endpos()
|
||||
|
||||
def transform(self, effects):
|
||||
"""
|
||||
Return a new font that is slanted and/or extended.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
effects : dict
|
||||
A dict with optional entries:
|
||||
|
||||
- 'slant' : float, default: 0
|
||||
Tangent of the angle that the font is to be slanted to the
|
||||
right. Negative values slant to the left.
|
||||
- 'extend' : float, default: 1
|
||||
Scaling factor for the font width. Values less than 1 condense
|
||||
the glyphs.
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Type1Font`
|
||||
"""
|
||||
fontname = self.prop['FontName']
|
||||
italicangle = self.prop['ItalicAngle']
|
||||
|
||||
array = [
|
||||
float(x) for x in (self.prop['FontMatrix']
|
||||
.lstrip('[').rstrip(']').split())
|
||||
]
|
||||
oldmatrix = np.eye(3, 3)
|
||||
oldmatrix[0:3, 0] = array[::2]
|
||||
oldmatrix[0:3, 1] = array[1::2]
|
||||
modifier = np.eye(3, 3)
|
||||
|
||||
if 'slant' in effects:
|
||||
slant = effects['slant']
|
||||
fontname += f'_Slant_{int(1000 * slant)}'
|
||||
italicangle = round(
|
||||
float(italicangle) - np.arctan(slant) / np.pi * 180,
|
||||
5
|
||||
)
|
||||
modifier[1, 0] = slant
|
||||
|
||||
if 'extend' in effects:
|
||||
extend = effects['extend']
|
||||
fontname += f'_Extend_{int(1000 * extend)}'
|
||||
modifier[0, 0] = extend
|
||||
|
||||
newmatrix = np.dot(modifier, oldmatrix)
|
||||
array[::2] = newmatrix[0:3, 0]
|
||||
array[1::2] = newmatrix[0:3, 1]
|
||||
fontmatrix = (
|
||||
f"[{' '.join(_format_approx(x, 6) for x in array)}]"
|
||||
)
|
||||
replacements = (
|
||||
[(x, f'/FontName/{fontname} def')
|
||||
for x in self._pos['FontName']]
|
||||
+ [(x, f'/ItalicAngle {italicangle} def')
|
||||
for x in self._pos['ItalicAngle']]
|
||||
+ [(x, f'/FontMatrix {fontmatrix} readonly def')
|
||||
for x in self._pos['FontMatrix']]
|
||||
+ [(x, '') for x in self._pos.get('UniqueID', [])]
|
||||
)
|
||||
|
||||
data = bytearray(self.parts[0])
|
||||
data.extend(self.decrypted)
|
||||
len0 = len(self.parts[0])
|
||||
for (pos0, pos1), value in sorted(replacements, reverse=True):
|
||||
data[pos0:pos1] = value.encode('ascii', 'replace')
|
||||
if pos0 < len(self.parts[0]):
|
||||
if pos1 >= len(self.parts[0]):
|
||||
raise RuntimeError(
|
||||
f"text to be replaced with {value} spans "
|
||||
"the eexec boundary"
|
||||
)
|
||||
len0 += len(value) - pos1 + pos0
|
||||
|
||||
data = bytes(data)
|
||||
return Type1Font((
|
||||
data[:len0],
|
||||
self._encrypt(data[len0:], 'eexec'),
|
||||
self.parts[2]
|
||||
))
|
||||
|
||||
|
||||
_StandardEncoding = {
|
||||
**{ord(letter): letter for letter in string.ascii_letters},
|
||||
0: '.notdef',
|
||||
32: 'space',
|
||||
33: 'exclam',
|
||||
34: 'quotedbl',
|
||||
35: 'numbersign',
|
||||
36: 'dollar',
|
||||
37: 'percent',
|
||||
38: 'ampersand',
|
||||
39: 'quoteright',
|
||||
40: 'parenleft',
|
||||
41: 'parenright',
|
||||
42: 'asterisk',
|
||||
43: 'plus',
|
||||
44: 'comma',
|
||||
45: 'hyphen',
|
||||
46: 'period',
|
||||
47: 'slash',
|
||||
48: 'zero',
|
||||
49: 'one',
|
||||
50: 'two',
|
||||
51: 'three',
|
||||
52: 'four',
|
||||
53: 'five',
|
||||
54: 'six',
|
||||
55: 'seven',
|
||||
56: 'eight',
|
||||
57: 'nine',
|
||||
58: 'colon',
|
||||
59: 'semicolon',
|
||||
60: 'less',
|
||||
61: 'equal',
|
||||
62: 'greater',
|
||||
63: 'question',
|
||||
64: 'at',
|
||||
91: 'bracketleft',
|
||||
92: 'backslash',
|
||||
93: 'bracketright',
|
||||
94: 'asciicircum',
|
||||
95: 'underscore',
|
||||
96: 'quoteleft',
|
||||
123: 'braceleft',
|
||||
124: 'bar',
|
||||
125: 'braceright',
|
||||
126: 'asciitilde',
|
||||
161: 'exclamdown',
|
||||
162: 'cent',
|
||||
163: 'sterling',
|
||||
164: 'fraction',
|
||||
165: 'yen',
|
||||
166: 'florin',
|
||||
167: 'section',
|
||||
168: 'currency',
|
||||
169: 'quotesingle',
|
||||
170: 'quotedblleft',
|
||||
171: 'guillemotleft',
|
||||
172: 'guilsinglleft',
|
||||
173: 'guilsinglright',
|
||||
174: 'fi',
|
||||
175: 'fl',
|
||||
177: 'endash',
|
||||
178: 'dagger',
|
||||
179: 'daggerdbl',
|
||||
180: 'periodcentered',
|
||||
182: 'paragraph',
|
||||
183: 'bullet',
|
||||
184: 'quotesinglbase',
|
||||
185: 'quotedblbase',
|
||||
186: 'quotedblright',
|
||||
187: 'guillemotright',
|
||||
188: 'ellipsis',
|
||||
189: 'perthousand',
|
||||
191: 'questiondown',
|
||||
193: 'grave',
|
||||
194: 'acute',
|
||||
195: 'circumflex',
|
||||
196: 'tilde',
|
||||
197: 'macron',
|
||||
198: 'breve',
|
||||
199: 'dotaccent',
|
||||
200: 'dieresis',
|
||||
202: 'ring',
|
||||
203: 'cedilla',
|
||||
205: 'hungarumlaut',
|
||||
206: 'ogonek',
|
||||
207: 'caron',
|
||||
208: 'emdash',
|
||||
225: 'AE',
|
||||
227: 'ordfeminine',
|
||||
232: 'Lslash',
|
||||
233: 'Oslash',
|
||||
234: 'OE',
|
||||
235: 'ordmasculine',
|
||||
241: 'ae',
|
||||
245: 'dotlessi',
|
||||
248: 'lslash',
|
||||
249: 'oslash',
|
||||
250: 'oe',
|
||||
251: 'germandbls',
|
||||
}
|
||||
1
venv/lib/python3.12/site-packages/matplotlib/_version.py
Normal file
1
venv/lib/python3.12/site-packages/matplotlib/_version.py
Normal file
@ -0,0 +1 @@
|
||||
version = "3.9.2"
|
||||
1780
venv/lib/python3.12/site-packages/matplotlib/animation.py
Normal file
1780
venv/lib/python3.12/site-packages/matplotlib/animation.py
Normal file
File diff suppressed because it is too large
Load Diff
217
venv/lib/python3.12/site-packages/matplotlib/animation.pyi
Normal file
217
venv/lib/python3.12/site-packages/matplotlib/animation.pyi
Normal file
@ -0,0 +1,217 @@
|
||||
import abc
|
||||
from collections.abc import Callable, Collection, Iterable, Sequence, Generator
|
||||
import contextlib
|
||||
from pathlib import Path
|
||||
from matplotlib.artist import Artist
|
||||
from matplotlib.backend_bases import TimerBase
|
||||
from matplotlib.figure import Figure
|
||||
|
||||
from typing import Any
|
||||
|
||||
subprocess_creation_flags: int
|
||||
|
||||
def adjusted_figsize(w: float, h: float, dpi: float, n: int) -> tuple[float, float]: ...
|
||||
|
||||
class MovieWriterRegistry:
|
||||
def __init__(self) -> None: ...
|
||||
def register(
|
||||
self, name: str
|
||||
) -> Callable[[type[AbstractMovieWriter]], type[AbstractMovieWriter]]: ...
|
||||
def is_available(self, name: str) -> bool: ...
|
||||
def __iter__(self) -> Generator[str, None, None]: ...
|
||||
def list(self) -> list[str]: ...
|
||||
def __getitem__(self, name: str) -> type[AbstractMovieWriter]: ...
|
||||
|
||||
writers: MovieWriterRegistry
|
||||
|
||||
class AbstractMovieWriter(abc.ABC, metaclass=abc.ABCMeta):
|
||||
fps: int
|
||||
metadata: dict[str, str]
|
||||
codec: str
|
||||
bitrate: int
|
||||
def __init__(
|
||||
self,
|
||||
fps: int = ...,
|
||||
metadata: dict[str, str] | None = ...,
|
||||
codec: str | None = ...,
|
||||
bitrate: int | None = ...,
|
||||
) -> None: ...
|
||||
outfile: str | Path
|
||||
fig: Figure
|
||||
dpi: float
|
||||
|
||||
@abc.abstractmethod
|
||||
def setup(self, fig: Figure, outfile: str | Path, dpi: float | None = ...) -> None: ...
|
||||
@property
|
||||
def frame_size(self) -> tuple[int, int]: ...
|
||||
@abc.abstractmethod
|
||||
def grab_frame(self, **savefig_kwargs) -> None: ...
|
||||
@abc.abstractmethod
|
||||
def finish(self) -> None: ...
|
||||
@contextlib.contextmanager
|
||||
def saving(
|
||||
self, fig: Figure, outfile: str | Path, dpi: float | None, *args, **kwargs
|
||||
) -> Generator[AbstractMovieWriter, None, None]: ...
|
||||
|
||||
class MovieWriter(AbstractMovieWriter):
|
||||
supported_formats: list[str]
|
||||
frame_format: str
|
||||
extra_args: list[str] | None
|
||||
def __init__(
|
||||
self,
|
||||
fps: int = ...,
|
||||
codec: str | None = ...,
|
||||
bitrate: int | None = ...,
|
||||
extra_args: list[str] | None = ...,
|
||||
metadata: dict[str, str] | None = ...,
|
||||
) -> None: ...
|
||||
def setup(self, fig: Figure, outfile: str | Path, dpi: float | None = ...) -> None: ...
|
||||
def grab_frame(self, **savefig_kwargs) -> None: ...
|
||||
def finish(self) -> None: ...
|
||||
@classmethod
|
||||
def bin_path(cls) -> str: ...
|
||||
@classmethod
|
||||
def isAvailable(cls) -> bool: ...
|
||||
|
||||
class FileMovieWriter(MovieWriter):
|
||||
fig: Figure
|
||||
outfile: str | Path
|
||||
dpi: float
|
||||
temp_prefix: str
|
||||
fname_format_str: str
|
||||
def setup(
|
||||
self,
|
||||
fig: Figure,
|
||||
outfile: str | Path,
|
||||
dpi: float | None = ...,
|
||||
frame_prefix: str | Path | None = ...,
|
||||
) -> None: ...
|
||||
def __del__(self) -> None: ...
|
||||
@property
|
||||
def frame_format(self) -> str: ...
|
||||
@frame_format.setter
|
||||
def frame_format(self, frame_format: str) -> None: ...
|
||||
|
||||
class PillowWriter(AbstractMovieWriter):
|
||||
@classmethod
|
||||
def isAvailable(cls) -> bool: ...
|
||||
def setup(
|
||||
self, fig: Figure, outfile: str | Path, dpi: float | None = ...
|
||||
) -> None: ...
|
||||
def grab_frame(self, **savefig_kwargs) -> None: ...
|
||||
def finish(self) -> None: ...
|
||||
|
||||
class FFMpegBase:
|
||||
codec: str
|
||||
@property
|
||||
def output_args(self) -> list[str]: ...
|
||||
|
||||
class FFMpegWriter(FFMpegBase, MovieWriter): ...
|
||||
|
||||
class FFMpegFileWriter(FFMpegBase, FileMovieWriter):
|
||||
supported_formats: list[str]
|
||||
|
||||
class ImageMagickBase:
|
||||
@classmethod
|
||||
def bin_path(cls) -> str: ...
|
||||
@classmethod
|
||||
def isAvailable(cls) -> bool: ...
|
||||
|
||||
class ImageMagickWriter(ImageMagickBase, MovieWriter):
|
||||
input_names: str
|
||||
|
||||
class ImageMagickFileWriter(ImageMagickBase, FileMovieWriter):
|
||||
supported_formats: list[str]
|
||||
@property
|
||||
def input_names(self) -> str: ...
|
||||
|
||||
class HTMLWriter(FileMovieWriter):
|
||||
supported_formats: list[str]
|
||||
@classmethod
|
||||
def isAvailable(cls) -> bool: ...
|
||||
embed_frames: bool
|
||||
default_mode: str
|
||||
def __init__(
|
||||
self,
|
||||
fps: int = ...,
|
||||
codec: str | None = ...,
|
||||
bitrate: int | None = ...,
|
||||
extra_args: list[str] | None = ...,
|
||||
metadata: dict[str, str] | None = ...,
|
||||
embed_frames: bool = ...,
|
||||
default_mode: str = ...,
|
||||
embed_limit: float | None = ...,
|
||||
) -> None: ...
|
||||
def setup(
|
||||
self,
|
||||
fig: Figure,
|
||||
outfile: str | Path,
|
||||
dpi: float | None = ...,
|
||||
frame_dir: str | Path | None = ...,
|
||||
) -> None: ...
|
||||
def grab_frame(self, **savefig_kwargs): ...
|
||||
def finish(self) -> None: ...
|
||||
|
||||
class Animation:
|
||||
frame_seq: Iterable[Artist]
|
||||
event_source: Any
|
||||
def __init__(
|
||||
self, fig: Figure, event_source: Any | None = ..., blit: bool = ...
|
||||
) -> None: ...
|
||||
def __del__(self) -> None: ...
|
||||
def save(
|
||||
self,
|
||||
filename: str | Path,
|
||||
writer: AbstractMovieWriter | str | None = ...,
|
||||
fps: int | None = ...,
|
||||
dpi: float | None = ...,
|
||||
codec: str | None = ...,
|
||||
bitrate: int | None = ...,
|
||||
extra_args: list[str] | None = ...,
|
||||
metadata: dict[str, str] | None = ...,
|
||||
extra_anim: list[Animation] | None = ...,
|
||||
savefig_kwargs: dict[str, Any] | None = ...,
|
||||
*,
|
||||
progress_callback: Callable[[int, int], Any] | None = ...
|
||||
) -> None: ...
|
||||
def new_frame_seq(self) -> Iterable[Artist]: ...
|
||||
def new_saved_frame_seq(self) -> Iterable[Artist]: ...
|
||||
def to_html5_video(self, embed_limit: float | None = ...) -> str: ...
|
||||
def to_jshtml(
|
||||
self,
|
||||
fps: int | None = ...,
|
||||
embed_frames: bool = ...,
|
||||
default_mode: str | None = ...,
|
||||
) -> str: ...
|
||||
def _repr_html_(self) -> str: ...
|
||||
def pause(self) -> None: ...
|
||||
def resume(self) -> None: ...
|
||||
|
||||
class TimedAnimation(Animation):
|
||||
def __init__(
|
||||
self,
|
||||
fig: Figure,
|
||||
interval: int = ...,
|
||||
repeat_delay: int = ...,
|
||||
repeat: bool = ...,
|
||||
event_source: TimerBase | None = ...,
|
||||
*args,
|
||||
**kwargs
|
||||
) -> None: ...
|
||||
|
||||
class ArtistAnimation(TimedAnimation):
|
||||
def __init__(self, fig: Figure, artists: Sequence[Collection[Artist]], *args, **kwargs) -> None: ...
|
||||
|
||||
class FuncAnimation(TimedAnimation):
|
||||
def __init__(
|
||||
self,
|
||||
fig: Figure,
|
||||
func: Callable[..., Iterable[Artist]],
|
||||
frames: Iterable | int | Callable[[], Generator] | None = ...,
|
||||
init_func: Callable[[], Iterable[Artist]] | None = ...,
|
||||
fargs: tuple[Any, ...] | None = ...,
|
||||
save_count: int | None = ...,
|
||||
*,
|
||||
cache_frame_data: bool = ...,
|
||||
**kwargs
|
||||
) -> None: ...
|
||||
1862
venv/lib/python3.12/site-packages/matplotlib/artist.py
Normal file
1862
venv/lib/python3.12/site-packages/matplotlib/artist.py
Normal file
File diff suppressed because it is too large
Load Diff
181
venv/lib/python3.12/site-packages/matplotlib/artist.pyi
Normal file
181
venv/lib/python3.12/site-packages/matplotlib/artist.pyi
Normal file
@ -0,0 +1,181 @@
|
||||
from .axes._base import _AxesBase
|
||||
from .backend_bases import RendererBase, MouseEvent
|
||||
from .figure import Figure, SubFigure
|
||||
from .path import Path
|
||||
from .patches import Patch
|
||||
from .patheffects import AbstractPathEffect
|
||||
from .transforms import (
|
||||
BboxBase,
|
||||
Bbox,
|
||||
Transform,
|
||||
TransformedPatchPath,
|
||||
TransformedPath,
|
||||
)
|
||||
|
||||
import numpy as np
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, NamedTuple, TextIO, overload
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
def allow_rasterization(draw): ...
|
||||
|
||||
class _XYPair(NamedTuple):
|
||||
x: ArrayLike
|
||||
y: ArrayLike
|
||||
|
||||
class _Unset: ...
|
||||
|
||||
class Artist:
|
||||
zorder: float
|
||||
stale_callback: Callable[[Artist, bool], None] | None
|
||||
figure: Figure | SubFigure | None
|
||||
clipbox: BboxBase | None
|
||||
def __init__(self) -> None: ...
|
||||
def remove(self) -> None: ...
|
||||
def have_units(self) -> bool: ...
|
||||
# TODO units
|
||||
def convert_xunits(self, x): ...
|
||||
def convert_yunits(self, y): ...
|
||||
@property
|
||||
def axes(self) -> _AxesBase | None: ...
|
||||
@axes.setter
|
||||
def axes(self, new_axes: _AxesBase | None) -> None: ...
|
||||
@property
|
||||
def stale(self) -> bool: ...
|
||||
@stale.setter
|
||||
def stale(self, val: bool) -> None: ...
|
||||
def get_window_extent(self, renderer: RendererBase | None = ...) -> Bbox: ...
|
||||
def get_tightbbox(self, renderer: RendererBase | None = ...) -> Bbox | None: ...
|
||||
def add_callback(self, func: Callable[[Artist], Any]) -> int: ...
|
||||
def remove_callback(self, oid: int) -> None: ...
|
||||
def pchanged(self) -> None: ...
|
||||
def is_transform_set(self) -> bool: ...
|
||||
def set_transform(self, t: Transform | None) -> None: ...
|
||||
def get_transform(self) -> Transform: ...
|
||||
def get_children(self) -> list[Artist]: ...
|
||||
# TODO can these dicts be type narrowed? e.g. str keys
|
||||
def contains(self, mouseevent: MouseEvent) -> tuple[bool, dict[Any, Any]]: ...
|
||||
def pickable(self) -> bool: ...
|
||||
def pick(self, mouseevent: MouseEvent) -> None: ...
|
||||
def set_picker(
|
||||
self,
|
||||
picker: None
|
||||
| bool
|
||||
| float
|
||||
| Callable[[Artist, MouseEvent], tuple[bool, dict[Any, Any]]],
|
||||
) -> None: ...
|
||||
def get_picker(
|
||||
self,
|
||||
) -> None | bool | float | Callable[
|
||||
[Artist, MouseEvent], tuple[bool, dict[Any, Any]]
|
||||
]: ...
|
||||
def get_url(self) -> str | None: ...
|
||||
def set_url(self, url: str | None) -> None: ...
|
||||
def get_gid(self) -> str | None: ...
|
||||
def set_gid(self, gid: str | None) -> None: ...
|
||||
def get_snap(self) -> bool | None: ...
|
||||
def set_snap(self, snap: bool | None) -> None: ...
|
||||
def get_sketch_params(self) -> tuple[float, float, float] | None: ...
|
||||
def set_sketch_params(
|
||||
self,
|
||||
scale: float | None = ...,
|
||||
length: float | None = ...,
|
||||
randomness: float | None = ...,
|
||||
) -> None: ...
|
||||
def set_path_effects(self, path_effects: list[AbstractPathEffect]) -> None: ...
|
||||
def get_path_effects(self) -> list[AbstractPathEffect]: ...
|
||||
def get_figure(self) -> Figure | None: ...
|
||||
def set_figure(self, fig: Figure) -> None: ...
|
||||
def set_clip_box(self, clipbox: BboxBase | None) -> None: ...
|
||||
def set_clip_path(
|
||||
self,
|
||||
path: Patch | Path | TransformedPath | TransformedPatchPath | None,
|
||||
transform: Transform | None = ...,
|
||||
) -> None: ...
|
||||
def get_alpha(self) -> float | None: ...
|
||||
def get_visible(self) -> bool: ...
|
||||
def get_animated(self) -> bool: ...
|
||||
def get_in_layout(self) -> bool: ...
|
||||
def get_clip_on(self) -> bool: ...
|
||||
def get_clip_box(self) -> Bbox | None: ...
|
||||
def get_clip_path(
|
||||
self,
|
||||
) -> Patch | Path | TransformedPath | TransformedPatchPath | None: ...
|
||||
def get_transformed_clip_path_and_affine(
|
||||
self,
|
||||
) -> tuple[None, None] | tuple[Path, Transform]: ...
|
||||
def set_clip_on(self, b: bool) -> None: ...
|
||||
def get_rasterized(self) -> bool: ...
|
||||
def set_rasterized(self, rasterized: bool) -> None: ...
|
||||
def get_agg_filter(self) -> Callable[[ArrayLike, float], tuple[np.ndarray, float, float]] | None: ...
|
||||
def set_agg_filter(
|
||||
self, filter_func: Callable[[ArrayLike, float], tuple[np.ndarray, float, float]] | None
|
||||
) -> None: ...
|
||||
def draw(self, renderer: RendererBase) -> None: ...
|
||||
def set_alpha(self, alpha: float | None) -> None: ...
|
||||
def set_visible(self, b: bool) -> None: ...
|
||||
def set_animated(self, b: bool) -> None: ...
|
||||
def set_in_layout(self, in_layout: bool) -> None: ...
|
||||
def get_label(self) -> object: ...
|
||||
def set_label(self, s: object) -> None: ...
|
||||
def get_zorder(self) -> float: ...
|
||||
def set_zorder(self, level: float) -> None: ...
|
||||
@property
|
||||
def sticky_edges(self) -> _XYPair: ...
|
||||
def update_from(self, other: Artist) -> None: ...
|
||||
def properties(self) -> dict[str, Any]: ...
|
||||
def update(self, props: dict[str, Any]) -> list[Any]: ...
|
||||
def _internal_update(self, kwargs: Any) -> list[Any]: ...
|
||||
def set(self, **kwargs: Any) -> list[Any]: ...
|
||||
def findobj(
|
||||
self,
|
||||
match: None | Callable[[Artist], bool] | type[Artist] = ...,
|
||||
include_self: bool = ...,
|
||||
) -> list[Artist]: ...
|
||||
def get_cursor_data(self, event: MouseEvent) -> Any: ...
|
||||
def format_cursor_data(self, data: Any) -> str: ...
|
||||
def get_mouseover(self) -> bool: ...
|
||||
def set_mouseover(self, mouseover: bool) -> None: ...
|
||||
@property
|
||||
def mouseover(self) -> bool: ...
|
||||
@mouseover.setter
|
||||
def mouseover(self, mouseover: bool) -> None: ...
|
||||
|
||||
class ArtistInspector:
|
||||
oorig: Artist | type[Artist]
|
||||
o: type[Artist]
|
||||
aliasd: dict[str, set[str]]
|
||||
def __init__(
|
||||
self, o: Artist | type[Artist] | Iterable[Artist | type[Artist]]
|
||||
) -> None: ...
|
||||
def get_aliases(self) -> dict[str, set[str]]: ...
|
||||
def get_valid_values(self, attr: str) -> str | None: ...
|
||||
def get_setters(self) -> list[str]: ...
|
||||
@staticmethod
|
||||
def number_of_parameters(func: Callable) -> int: ...
|
||||
@staticmethod
|
||||
def is_alias(method: Callable) -> bool: ...
|
||||
def aliased_name(self, s: str) -> str: ...
|
||||
def aliased_name_rest(self, s: str, target: str) -> str: ...
|
||||
@overload
|
||||
def pprint_setters(
|
||||
self, prop: None = ..., leadingspace: int = ...
|
||||
) -> list[str]: ...
|
||||
@overload
|
||||
def pprint_setters(self, prop: str, leadingspace: int = ...) -> str: ...
|
||||
@overload
|
||||
def pprint_setters_rest(
|
||||
self, prop: None = ..., leadingspace: int = ...
|
||||
) -> list[str]: ...
|
||||
@overload
|
||||
def pprint_setters_rest(self, prop: str, leadingspace: int = ...) -> str: ...
|
||||
def properties(self) -> dict[str, Any]: ...
|
||||
def pprint_getters(self) -> list[str]: ...
|
||||
|
||||
def getp(obj: Artist, property: str | None = ...) -> Any: ...
|
||||
|
||||
get = getp
|
||||
|
||||
def setp(obj: Artist, *args, file: TextIO | None = ..., **kwargs) -> list[Any] | None: ...
|
||||
def kwdoc(artist: Artist | type[Artist] | Iterable[Artist | type[Artist]]) -> str: ...
|
||||
@ -0,0 +1,18 @@
|
||||
from . import _base
|
||||
from ._axes import Axes # noqa: F401
|
||||
|
||||
# Backcompat.
|
||||
Subplot = Axes
|
||||
|
||||
|
||||
class _SubplotBaseMeta(type):
|
||||
def __instancecheck__(self, obj):
|
||||
return (isinstance(obj, _base._AxesBase)
|
||||
and obj.get_subplotspec() is not None)
|
||||
|
||||
|
||||
class SubplotBase(metaclass=_SubplotBaseMeta):
|
||||
pass
|
||||
|
||||
|
||||
def subplot_class_factory(cls): return cls
|
||||
@ -0,0 +1,16 @@
|
||||
from typing import TypeVar
|
||||
|
||||
from ._axes import Axes as Axes
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
# Backcompat.
|
||||
Subplot = Axes
|
||||
|
||||
class _SubplotBaseMeta(type):
|
||||
def __instancecheck__(self, obj) -> bool: ...
|
||||
|
||||
class SubplotBase(metaclass=_SubplotBaseMeta): ...
|
||||
|
||||
def subplot_class_factory(cls: type[_T]) -> type[_T]: ...
|
||||
8634
venv/lib/python3.12/site-packages/matplotlib/axes/_axes.py
Normal file
8634
venv/lib/python3.12/site-packages/matplotlib/axes/_axes.py
Normal file
File diff suppressed because it is too large
Load Diff
772
venv/lib/python3.12/site-packages/matplotlib/axes/_axes.pyi
Normal file
772
venv/lib/python3.12/site-packages/matplotlib/axes/_axes.pyi
Normal file
@ -0,0 +1,772 @@
|
||||
from matplotlib.axes._base import _AxesBase
|
||||
from matplotlib.axes._secondary_axes import SecondaryAxis
|
||||
|
||||
from matplotlib.artist import Artist
|
||||
from matplotlib.backend_bases import RendererBase
|
||||
from matplotlib.collections import (
|
||||
Collection,
|
||||
LineCollection,
|
||||
PathCollection,
|
||||
PolyCollection,
|
||||
EventCollection,
|
||||
QuadMesh,
|
||||
)
|
||||
from matplotlib.colors import Colormap, Normalize
|
||||
from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer
|
||||
from matplotlib.contour import ContourSet, QuadContourSet
|
||||
from matplotlib.image import AxesImage, PcolorImage
|
||||
from matplotlib.legend import Legend
|
||||
from matplotlib.legend_handler import HandlerBase
|
||||
from matplotlib.lines import Line2D, AxLine
|
||||
from matplotlib.mlab import GaussianKDE
|
||||
from matplotlib.patches import Rectangle, FancyArrow, Polygon, StepPatch, Wedge
|
||||
from matplotlib.quiver import Quiver, QuiverKey, Barbs
|
||||
from matplotlib.text import Annotation, Text
|
||||
from matplotlib.transforms import Transform, Bbox
|
||||
import matplotlib.tri as mtri
|
||||
import matplotlib.table as mtable
|
||||
import matplotlib.stackplot as mstack
|
||||
import matplotlib.streamplot as mstream
|
||||
|
||||
import datetime
|
||||
import PIL.Image
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import Any, Literal, overload
|
||||
import numpy as np
|
||||
from numpy.typing import ArrayLike
|
||||
from matplotlib.typing import ColorType, MarkerType, LineStyleType
|
||||
|
||||
class Axes(_AxesBase):
|
||||
def get_title(self, loc: Literal["left", "center", "right"] = ...) -> str: ...
|
||||
def set_title(
|
||||
self,
|
||||
label: str,
|
||||
fontdict: dict[str, Any] | None = ...,
|
||||
loc: Literal["left", "center", "right"] | None = ...,
|
||||
pad: float | None = ...,
|
||||
*,
|
||||
y: float | None = ...,
|
||||
**kwargs
|
||||
) -> Text: ...
|
||||
def get_legend_handles_labels(
|
||||
self, legend_handler_map: dict[type, HandlerBase] | None = ...
|
||||
) -> tuple[list[Artist], list[Any]]: ...
|
||||
legend_: Legend | None
|
||||
|
||||
@overload
|
||||
def legend(self) -> Legend: ...
|
||||
@overload
|
||||
def legend(self, handles: Iterable[Artist | tuple[Artist, ...]], labels: Iterable[str], **kwargs) -> Legend: ...
|
||||
@overload
|
||||
def legend(self, *, handles: Iterable[Artist | tuple[Artist, ...]], **kwargs) -> Legend: ...
|
||||
@overload
|
||||
def legend(self, labels: Iterable[str], **kwargs) -> Legend: ...
|
||||
@overload
|
||||
def legend(self, **kwargs) -> Legend: ...
|
||||
|
||||
def inset_axes(
|
||||
self,
|
||||
bounds: tuple[float, float, float, float],
|
||||
*,
|
||||
transform: Transform | None = ...,
|
||||
zorder: float = ...,
|
||||
**kwargs
|
||||
) -> Axes: ...
|
||||
def indicate_inset(
|
||||
self,
|
||||
bounds: tuple[float, float, float, float],
|
||||
inset_ax: Axes | None = ...,
|
||||
*,
|
||||
transform: Transform | None = ...,
|
||||
facecolor: ColorType = ...,
|
||||
edgecolor: ColorType = ...,
|
||||
alpha: float = ...,
|
||||
zorder: float = ...,
|
||||
**kwargs
|
||||
) -> Rectangle: ...
|
||||
def indicate_inset_zoom(self, inset_ax: Axes, **kwargs) -> Rectangle: ...
|
||||
def secondary_xaxis(
|
||||
self,
|
||||
location: Literal["top", "bottom"] | float,
|
||||
*,
|
||||
functions: tuple[
|
||||
Callable[[ArrayLike], ArrayLike], Callable[[ArrayLike], ArrayLike]
|
||||
]
|
||||
| Transform
|
||||
| None = ...,
|
||||
transform: Transform | None = ...,
|
||||
**kwargs
|
||||
) -> SecondaryAxis: ...
|
||||
def secondary_yaxis(
|
||||
self,
|
||||
location: Literal["left", "right"] | float,
|
||||
*,
|
||||
functions: tuple[
|
||||
Callable[[ArrayLike], ArrayLike], Callable[[ArrayLike], ArrayLike]
|
||||
]
|
||||
| Transform
|
||||
| None = ...,
|
||||
transform: Transform | None = ...,
|
||||
**kwargs
|
||||
) -> SecondaryAxis: ...
|
||||
def text(
|
||||
self,
|
||||
x: float,
|
||||
y: float,
|
||||
s: str,
|
||||
fontdict: dict[str, Any] | None = ...,
|
||||
**kwargs
|
||||
) -> Text: ...
|
||||
def annotate(
|
||||
self,
|
||||
text: str,
|
||||
xy: tuple[float, float],
|
||||
xytext: tuple[float, float] | None = ...,
|
||||
xycoords: str
|
||||
| Artist
|
||||
| Transform
|
||||
| Callable[[RendererBase], Bbox | Transform]
|
||||
| tuple[float, float] = ...,
|
||||
textcoords: str
|
||||
| Artist
|
||||
| Transform
|
||||
| Callable[[RendererBase], Bbox | Transform]
|
||||
| tuple[float, float]
|
||||
| None = ...,
|
||||
arrowprops: dict[str, Any] | None = ...,
|
||||
annotation_clip: bool | None = ...,
|
||||
**kwargs
|
||||
) -> Annotation: ...
|
||||
def axhline(
|
||||
self, y: float = ..., xmin: float = ..., xmax: float = ..., **kwargs
|
||||
) -> Line2D: ...
|
||||
def axvline(
|
||||
self, x: float = ..., ymin: float = ..., ymax: float = ..., **kwargs
|
||||
) -> Line2D: ...
|
||||
|
||||
# TODO: Could separate the xy2 and slope signatures
|
||||
def axline(
|
||||
self,
|
||||
xy1: tuple[float, float],
|
||||
xy2: tuple[float, float] | None = ...,
|
||||
*,
|
||||
slope: float | None = ...,
|
||||
**kwargs
|
||||
) -> AxLine: ...
|
||||
def axhspan(
|
||||
self, ymin: float, ymax: float, xmin: float = ..., xmax: float = ..., **kwargs
|
||||
) -> Rectangle: ...
|
||||
def axvspan(
|
||||
self, xmin: float, xmax: float, ymin: float = ..., ymax: float = ..., **kwargs
|
||||
) -> Rectangle: ...
|
||||
def hlines(
|
||||
self,
|
||||
y: float | ArrayLike,
|
||||
xmin: float | ArrayLike,
|
||||
xmax: float | ArrayLike,
|
||||
colors: ColorType | Sequence[ColorType] | None = ...,
|
||||
linestyles: LineStyleType = ...,
|
||||
label: str = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> LineCollection: ...
|
||||
def vlines(
|
||||
self,
|
||||
x: float | ArrayLike,
|
||||
ymin: float | ArrayLike,
|
||||
ymax: float | ArrayLike,
|
||||
colors: ColorType | Sequence[ColorType] | None = ...,
|
||||
linestyles: LineStyleType = ...,
|
||||
label: str = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> LineCollection: ...
|
||||
def eventplot(
|
||||
self,
|
||||
positions: ArrayLike | Sequence[ArrayLike],
|
||||
orientation: Literal["horizontal", "vertical"] = ...,
|
||||
lineoffsets: float | Sequence[float] = ...,
|
||||
linelengths: float | Sequence[float] = ...,
|
||||
linewidths: float | Sequence[float] | None = ...,
|
||||
colors: ColorType | Sequence[ColorType] | None = ...,
|
||||
alpha: float | Sequence[float] | None = ...,
|
||||
linestyles: LineStyleType | Sequence[LineStyleType] = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> EventCollection: ...
|
||||
def plot(
|
||||
self,
|
||||
*args: float | ArrayLike | str,
|
||||
scalex: bool = ...,
|
||||
scaley: bool = ...,
|
||||
data = ...,
|
||||
**kwargs
|
||||
) -> list[Line2D]: ...
|
||||
def plot_date(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
y: ArrayLike,
|
||||
fmt: str = ...,
|
||||
tz: str | datetime.tzinfo | None = ...,
|
||||
xdate: bool = ...,
|
||||
ydate: bool = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> list[Line2D]: ...
|
||||
def loglog(self, *args, **kwargs) -> list[Line2D]: ...
|
||||
def semilogx(self, *args, **kwargs) -> list[Line2D]: ...
|
||||
def semilogy(self, *args, **kwargs) -> list[Line2D]: ...
|
||||
def acorr(
|
||||
self, x: ArrayLike, *, data=..., **kwargs
|
||||
) -> tuple[np.ndarray, np.ndarray, LineCollection | Line2D, Line2D | None]: ...
|
||||
def xcorr(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
y: ArrayLike,
|
||||
normed: bool = ...,
|
||||
detrend: Callable[[ArrayLike], ArrayLike] = ...,
|
||||
usevlines: bool = ...,
|
||||
maxlags: int = ...,
|
||||
*,
|
||||
data = ...,
|
||||
**kwargs
|
||||
) -> tuple[np.ndarray, np.ndarray, LineCollection | Line2D, Line2D | None]: ...
|
||||
def step(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
y: ArrayLike,
|
||||
*args,
|
||||
where: Literal["pre", "post", "mid"] = ...,
|
||||
data = ...,
|
||||
**kwargs
|
||||
) -> list[Line2D]: ...
|
||||
def bar(
|
||||
self,
|
||||
x: float | ArrayLike,
|
||||
height: float | ArrayLike,
|
||||
width: float | ArrayLike = ...,
|
||||
bottom: float | ArrayLike | None = ...,
|
||||
*,
|
||||
align: Literal["center", "edge"] = ...,
|
||||
data = ...,
|
||||
**kwargs
|
||||
) -> BarContainer: ...
|
||||
def barh(
|
||||
self,
|
||||
y: float | ArrayLike,
|
||||
width: float | ArrayLike,
|
||||
height: float | ArrayLike = ...,
|
||||
left: float | ArrayLike | None = ...,
|
||||
*,
|
||||
align: Literal["center", "edge"] = ...,
|
||||
data = ...,
|
||||
**kwargs
|
||||
) -> BarContainer: ...
|
||||
def bar_label(
|
||||
self,
|
||||
container: BarContainer,
|
||||
labels: ArrayLike | None = ...,
|
||||
*,
|
||||
fmt: str | Callable[[float], str] = ...,
|
||||
label_type: Literal["center", "edge"] = ...,
|
||||
padding: float = ...,
|
||||
**kwargs
|
||||
) -> list[Annotation]: ...
|
||||
def broken_barh(
|
||||
self,
|
||||
xranges: Sequence[tuple[float, float]],
|
||||
yrange: tuple[float, float],
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> PolyCollection: ...
|
||||
def stem(
|
||||
self,
|
||||
*args: ArrayLike | str,
|
||||
linefmt: str | None = ...,
|
||||
markerfmt: str | None = ...,
|
||||
basefmt: str | None = ...,
|
||||
bottom: float = ...,
|
||||
label: str | None = ...,
|
||||
orientation: Literal["vertical", "horizontal"] = ...,
|
||||
data=...,
|
||||
) -> StemContainer: ...
|
||||
|
||||
# TODO: data kwarg preprocessor?
|
||||
def pie(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
explode: ArrayLike | None = ...,
|
||||
labels: Sequence[str] | None = ...,
|
||||
colors: ColorType | Sequence[ColorType] | None = ...,
|
||||
autopct: str | Callable[[float], str] | None = ...,
|
||||
pctdistance: float = ...,
|
||||
shadow: bool = ...,
|
||||
labeldistance: float | None = ...,
|
||||
startangle: float = ...,
|
||||
radius: float = ...,
|
||||
counterclock: bool = ...,
|
||||
wedgeprops: dict[str, Any] | None = ...,
|
||||
textprops: dict[str, Any] | None = ...,
|
||||
center: tuple[float, float] = ...,
|
||||
frame: bool = ...,
|
||||
rotatelabels: bool = ...,
|
||||
*,
|
||||
normalize: bool = ...,
|
||||
hatch: str | Sequence[str] | None = ...,
|
||||
data=...,
|
||||
) -> tuple[list[Wedge], list[Text]] | tuple[
|
||||
list[Wedge], list[Text], list[Text]
|
||||
]: ...
|
||||
def errorbar(
|
||||
self,
|
||||
x: float | ArrayLike,
|
||||
y: float | ArrayLike,
|
||||
yerr: float | ArrayLike | None = ...,
|
||||
xerr: float | ArrayLike | None = ...,
|
||||
fmt: str = ...,
|
||||
ecolor: ColorType | None = ...,
|
||||
elinewidth: float | None = ...,
|
||||
capsize: float | None = ...,
|
||||
barsabove: bool = ...,
|
||||
lolims: bool | ArrayLike = ...,
|
||||
uplims: bool | ArrayLike = ...,
|
||||
xlolims: bool | ArrayLike = ...,
|
||||
xuplims: bool | ArrayLike = ...,
|
||||
errorevery: int | tuple[int, int] = ...,
|
||||
capthick: float | None = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> ErrorbarContainer: ...
|
||||
def boxplot(
|
||||
self,
|
||||
x: ArrayLike | Sequence[ArrayLike],
|
||||
notch: bool | None = ...,
|
||||
sym: str | None = ...,
|
||||
vert: bool | None = ...,
|
||||
whis: float | tuple[float, float] | None = ...,
|
||||
positions: ArrayLike | None = ...,
|
||||
widths: float | ArrayLike | None = ...,
|
||||
patch_artist: bool | None = ...,
|
||||
bootstrap: int | None = ...,
|
||||
usermedians: ArrayLike | None = ...,
|
||||
conf_intervals: ArrayLike | None = ...,
|
||||
meanline: bool | None = ...,
|
||||
showmeans: bool | None = ...,
|
||||
showcaps: bool | None = ...,
|
||||
showbox: bool | None = ...,
|
||||
showfliers: bool | None = ...,
|
||||
boxprops: dict[str, Any] | None = ...,
|
||||
tick_labels: Sequence[str] | None = ...,
|
||||
flierprops: dict[str, Any] | None = ...,
|
||||
medianprops: dict[str, Any] | None = ...,
|
||||
meanprops: dict[str, Any] | None = ...,
|
||||
capprops: dict[str, Any] | None = ...,
|
||||
whiskerprops: dict[str, Any] | None = ...,
|
||||
manage_ticks: bool = ...,
|
||||
autorange: bool = ...,
|
||||
zorder: float | None = ...,
|
||||
capwidths: float | ArrayLike | None = ...,
|
||||
label: Sequence[str] | None = ...,
|
||||
*,
|
||||
data=...,
|
||||
) -> dict[str, Any]: ...
|
||||
def bxp(
|
||||
self,
|
||||
bxpstats: Sequence[dict[str, Any]],
|
||||
positions: ArrayLike | None = ...,
|
||||
widths: float | ArrayLike | None = ...,
|
||||
vert: bool = ...,
|
||||
patch_artist: bool = ...,
|
||||
shownotches: bool = ...,
|
||||
showmeans: bool = ...,
|
||||
showcaps: bool = ...,
|
||||
showbox: bool = ...,
|
||||
showfliers: bool = ...,
|
||||
boxprops: dict[str, Any] | None = ...,
|
||||
whiskerprops: dict[str, Any] | None = ...,
|
||||
flierprops: dict[str, Any] | None = ...,
|
||||
medianprops: dict[str, Any] | None = ...,
|
||||
capprops: dict[str, Any] | None = ...,
|
||||
meanprops: dict[str, Any] | None = ...,
|
||||
meanline: bool = ...,
|
||||
manage_ticks: bool = ...,
|
||||
zorder: float | None = ...,
|
||||
capwidths: float | ArrayLike | None = ...,
|
||||
label: Sequence[str] | None = ...,
|
||||
) -> dict[str, Any]: ...
|
||||
def scatter(
|
||||
self,
|
||||
x: float | ArrayLike,
|
||||
y: float | ArrayLike,
|
||||
s: float | ArrayLike | None = ...,
|
||||
c: ArrayLike | Sequence[ColorType] | ColorType | None = ...,
|
||||
marker: MarkerType | None = ...,
|
||||
cmap: str | Colormap | None = ...,
|
||||
norm: str | Normalize | None = ...,
|
||||
vmin: float | None = ...,
|
||||
vmax: float | None = ...,
|
||||
alpha: float | None = ...,
|
||||
linewidths: float | Sequence[float] | None = ...,
|
||||
*,
|
||||
edgecolors: Literal["face", "none"] | ColorType | Sequence[ColorType] | None = ...,
|
||||
plotnonfinite: bool = ...,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> PathCollection: ...
|
||||
def hexbin(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
y: ArrayLike,
|
||||
C: ArrayLike | None = ...,
|
||||
gridsize: int | tuple[int, int] = ...,
|
||||
bins: Literal["log"] | int | Sequence[float] | None = ...,
|
||||
xscale: Literal["linear", "log"] = ...,
|
||||
yscale: Literal["linear", "log"] = ...,
|
||||
extent: tuple[float, float, float, float] | None = ...,
|
||||
cmap: str | Colormap | None = ...,
|
||||
norm: str | Normalize | None = ...,
|
||||
vmin: float | None = ...,
|
||||
vmax: float | None = ...,
|
||||
alpha: float | None = ...,
|
||||
linewidths: float | None = ...,
|
||||
edgecolors: Literal["face", "none"] | ColorType = ...,
|
||||
reduce_C_function: Callable[[np.ndarray | list[float]], float] = ...,
|
||||
mincnt: int | None = ...,
|
||||
marginals: bool = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> PolyCollection: ...
|
||||
def arrow(
|
||||
self, x: float, y: float, dx: float, dy: float, **kwargs
|
||||
) -> FancyArrow: ...
|
||||
def quiverkey(
|
||||
self, Q: Quiver, X: float, Y: float, U: float, label: str, **kwargs
|
||||
) -> QuiverKey: ...
|
||||
def quiver(self, *args, data=..., **kwargs) -> Quiver: ...
|
||||
def barbs(self, *args, data=..., **kwargs) -> Barbs: ...
|
||||
def fill(self, *args, data=..., **kwargs) -> list[Polygon]: ...
|
||||
def fill_between(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
y1: ArrayLike | float,
|
||||
y2: ArrayLike | float = ...,
|
||||
where: Sequence[bool] | None = ...,
|
||||
interpolate: bool = ...,
|
||||
step: Literal["pre", "post", "mid"] | None = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> PolyCollection: ...
|
||||
def fill_betweenx(
|
||||
self,
|
||||
y: ArrayLike,
|
||||
x1: ArrayLike | float,
|
||||
x2: ArrayLike | float = ...,
|
||||
where: Sequence[bool] | None = ...,
|
||||
step: Literal["pre", "post", "mid"] | None = ...,
|
||||
interpolate: bool = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> PolyCollection: ...
|
||||
def imshow(
|
||||
self,
|
||||
X: ArrayLike | PIL.Image.Image,
|
||||
cmap: str | Colormap | None = ...,
|
||||
norm: str | Normalize | None = ...,
|
||||
*,
|
||||
aspect: Literal["equal", "auto"] | float | None = ...,
|
||||
interpolation: str | None = ...,
|
||||
alpha: float | ArrayLike | None = ...,
|
||||
vmin: float | None = ...,
|
||||
vmax: float | None = ...,
|
||||
origin: Literal["upper", "lower"] | None = ...,
|
||||
extent: tuple[float, float, float, float] | None = ...,
|
||||
interpolation_stage: Literal["data", "rgba"] | None = ...,
|
||||
filternorm: bool = ...,
|
||||
filterrad: float = ...,
|
||||
resample: bool | None = ...,
|
||||
url: str | None = ...,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> AxesImage: ...
|
||||
def pcolor(
|
||||
self,
|
||||
*args: ArrayLike,
|
||||
shading: Literal["flat", "nearest", "auto"] | None = ...,
|
||||
alpha: float | None = ...,
|
||||
norm: str | Normalize | None = ...,
|
||||
cmap: str | Colormap | None = ...,
|
||||
vmin: float | None = ...,
|
||||
vmax: float | None = ...,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> Collection: ...
|
||||
def pcolormesh(
|
||||
self,
|
||||
*args: ArrayLike,
|
||||
alpha: float | None = ...,
|
||||
norm: str | Normalize | None = ...,
|
||||
cmap: str | Colormap | None = ...,
|
||||
vmin: float | None = ...,
|
||||
vmax: float | None = ...,
|
||||
shading: Literal["flat", "nearest", "gouraud", "auto"] | None = ...,
|
||||
antialiased: bool = ...,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> QuadMesh: ...
|
||||
def pcolorfast(
|
||||
self,
|
||||
*args: ArrayLike | tuple[float, float],
|
||||
alpha: float | None = ...,
|
||||
norm: str | Normalize | None = ...,
|
||||
cmap: str | Colormap | None = ...,
|
||||
vmin: float | None = ...,
|
||||
vmax: float | None = ...,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> AxesImage | PcolorImage | QuadMesh: ...
|
||||
def contour(self, *args, data=..., **kwargs) -> QuadContourSet: ...
|
||||
def contourf(self, *args, data=..., **kwargs) -> QuadContourSet: ...
|
||||
def clabel(
|
||||
self, CS: ContourSet, levels: ArrayLike | None = ..., **kwargs
|
||||
) -> list[Text]: ...
|
||||
def hist(
|
||||
self,
|
||||
x: ArrayLike | Sequence[ArrayLike],
|
||||
bins: int | Sequence[float] | str | None = ...,
|
||||
range: tuple[float, float] | None = ...,
|
||||
density: bool = ...,
|
||||
weights: ArrayLike | None = ...,
|
||||
cumulative: bool | float = ...,
|
||||
bottom: ArrayLike | float | None = ...,
|
||||
histtype: Literal["bar", "barstacked", "step", "stepfilled"] = ...,
|
||||
align: Literal["left", "mid", "right"] = ...,
|
||||
orientation: Literal["vertical", "horizontal"] = ...,
|
||||
rwidth: float | None = ...,
|
||||
log: bool = ...,
|
||||
color: ColorType | Sequence[ColorType] | None = ...,
|
||||
label: str | Sequence[str] | None = ...,
|
||||
stacked: bool = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> tuple[
|
||||
np.ndarray | list[np.ndarray],
|
||||
np.ndarray,
|
||||
BarContainer | Polygon | list[BarContainer | Polygon],
|
||||
]: ...
|
||||
def stairs(
|
||||
self,
|
||||
values: ArrayLike,
|
||||
edges: ArrayLike | None = ...,
|
||||
*,
|
||||
orientation: Literal["vertical", "horizontal"] = ...,
|
||||
baseline: float | ArrayLike | None = ...,
|
||||
fill: bool = ...,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> StepPatch: ...
|
||||
def hist2d(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
y: ArrayLike,
|
||||
bins: None
|
||||
| int
|
||||
| tuple[int, int]
|
||||
| ArrayLike
|
||||
| tuple[ArrayLike, ArrayLike] = ...,
|
||||
range: ArrayLike | None = ...,
|
||||
density: bool = ...,
|
||||
weights: ArrayLike | None = ...,
|
||||
cmin: float | None = ...,
|
||||
cmax: float | None = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray, QuadMesh]: ...
|
||||
def ecdf(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
weights: ArrayLike | None = ...,
|
||||
*,
|
||||
complementary: bool=...,
|
||||
orientation: Literal["vertical", "horizonatal"]=...,
|
||||
compress: bool=...,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> Line2D: ...
|
||||
def psd(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
NFFT: int | None = ...,
|
||||
Fs: float | None = ...,
|
||||
Fc: int | None = ...,
|
||||
detrend: Literal["none", "mean", "linear"]
|
||||
| Callable[[ArrayLike], ArrayLike]
|
||||
| None = ...,
|
||||
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ...,
|
||||
noverlap: int | None = ...,
|
||||
pad_to: int | None = ...,
|
||||
sides: Literal["default", "onesided", "twosided"] | None = ...,
|
||||
scale_by_freq: bool | None = ...,
|
||||
return_line: bool | None = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, Line2D]: ...
|
||||
def csd(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
y: ArrayLike,
|
||||
NFFT: int | None = ...,
|
||||
Fs: float | None = ...,
|
||||
Fc: int | None = ...,
|
||||
detrend: Literal["none", "mean", "linear"]
|
||||
| Callable[[ArrayLike], ArrayLike]
|
||||
| None = ...,
|
||||
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ...,
|
||||
noverlap: int | None = ...,
|
||||
pad_to: int | None = ...,
|
||||
sides: Literal["default", "onesided", "twosided"] | None = ...,
|
||||
scale_by_freq: bool | None = ...,
|
||||
return_line: bool | None = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, Line2D]: ...
|
||||
def magnitude_spectrum(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
Fs: float | None = ...,
|
||||
Fc: int | None = ...,
|
||||
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ...,
|
||||
pad_to: int | None = ...,
|
||||
sides: Literal["default", "onesided", "twosided"] | None = ...,
|
||||
scale: Literal["default", "linear", "dB"] | None = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> tuple[np.ndarray, np.ndarray, Line2D]: ...
|
||||
def angle_spectrum(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
Fs: float | None = ...,
|
||||
Fc: int | None = ...,
|
||||
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ...,
|
||||
pad_to: int | None = ...,
|
||||
sides: Literal["default", "onesided", "twosided"] | None = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> tuple[np.ndarray, np.ndarray, Line2D]: ...
|
||||
def phase_spectrum(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
Fs: float | None = ...,
|
||||
Fc: int | None = ...,
|
||||
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ...,
|
||||
pad_to: int | None = ...,
|
||||
sides: Literal["default", "onesided", "twosided"] | None = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> tuple[np.ndarray, np.ndarray, Line2D]: ...
|
||||
def cohere(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
y: ArrayLike,
|
||||
NFFT: int = ...,
|
||||
Fs: float = ...,
|
||||
Fc: int = ...,
|
||||
detrend: Literal["none", "mean", "linear"]
|
||||
| Callable[[ArrayLike], ArrayLike] = ...,
|
||||
window: Callable[[ArrayLike], ArrayLike] | ArrayLike = ...,
|
||||
noverlap: int = ...,
|
||||
pad_to: int | None = ...,
|
||||
sides: Literal["default", "onesided", "twosided"] = ...,
|
||||
scale_by_freq: bool | None = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> tuple[np.ndarray, np.ndarray]: ...
|
||||
def specgram(
|
||||
self,
|
||||
x: ArrayLike,
|
||||
NFFT: int | None = ...,
|
||||
Fs: float | None = ...,
|
||||
Fc: int | None = ...,
|
||||
detrend: Literal["none", "mean", "linear"]
|
||||
| Callable[[ArrayLike], ArrayLike]
|
||||
| None = ...,
|
||||
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ...,
|
||||
noverlap: int | None = ...,
|
||||
cmap: str | Colormap | None = ...,
|
||||
xextent: tuple[float, float] | None = ...,
|
||||
pad_to: int | None = ...,
|
||||
sides: Literal["default", "onesided", "twosided"] | None = ...,
|
||||
scale_by_freq: bool | None = ...,
|
||||
mode: Literal["default", "psd", "magnitude", "angle", "phase"] | None = ...,
|
||||
scale: Literal["default", "linear", "dB"] | None = ...,
|
||||
vmin: float | None = ...,
|
||||
vmax: float | None = ...,
|
||||
*,
|
||||
data=...,
|
||||
**kwargs
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray, AxesImage]: ...
|
||||
def spy(
|
||||
self,
|
||||
Z: ArrayLike,
|
||||
precision: float | Literal["present"] = ...,
|
||||
marker: str | None = ...,
|
||||
markersize: float | None = ...,
|
||||
aspect: Literal["equal", "auto"] | float | None = ...,
|
||||
origin: Literal["upper", "lower"] = ...,
|
||||
**kwargs
|
||||
) -> AxesImage: ...
|
||||
def matshow(self, Z: ArrayLike, **kwargs) -> AxesImage: ...
|
||||
def violinplot(
|
||||
self,
|
||||
dataset: ArrayLike | Sequence[ArrayLike],
|
||||
positions: ArrayLike | None = ...,
|
||||
vert: bool = ...,
|
||||
widths: float | ArrayLike = ...,
|
||||
showmeans: bool = ...,
|
||||
showextrema: bool = ...,
|
||||
showmedians: bool = ...,
|
||||
quantiles: Sequence[float | Sequence[float]] | None = ...,
|
||||
points: int = ...,
|
||||
bw_method: Literal["scott", "silverman"]
|
||||
| float
|
||||
| Callable[[GaussianKDE], float]
|
||||
| None = ...,
|
||||
side: Literal["both", "low", "high"] = ...,
|
||||
*,
|
||||
data=...,
|
||||
) -> dict[str, Collection]: ...
|
||||
def violin(
|
||||
self,
|
||||
vpstats: Sequence[dict[str, Any]],
|
||||
positions: ArrayLike | None = ...,
|
||||
vert: bool = ...,
|
||||
widths: float | ArrayLike = ...,
|
||||
showmeans: bool = ...,
|
||||
showextrema: bool = ...,
|
||||
showmedians: bool = ...,
|
||||
side: Literal["both", "low", "high"] = ...,
|
||||
) -> dict[str, Collection]: ...
|
||||
|
||||
table = mtable.table
|
||||
stackplot = mstack.stackplot
|
||||
streamplot = mstream.streamplot
|
||||
tricontour = mtri.tricontour
|
||||
tricontourf = mtri.tricontourf
|
||||
tripcolor = mtri.tripcolor
|
||||
triplot = mtri.triplot
|
||||
4752
venv/lib/python3.12/site-packages/matplotlib/axes/_base.py
Normal file
4752
venv/lib/python3.12/site-packages/matplotlib/axes/_base.py
Normal file
File diff suppressed because it is too large
Load Diff
459
venv/lib/python3.12/site-packages/matplotlib/axes/_base.pyi
Normal file
459
venv/lib/python3.12/site-packages/matplotlib/axes/_base.pyi
Normal file
@ -0,0 +1,459 @@
|
||||
import matplotlib.artist as martist
|
||||
|
||||
import datetime
|
||||
from collections.abc import Callable, Iterable, Iterator, Sequence
|
||||
from matplotlib import cbook
|
||||
from matplotlib.artist import Artist
|
||||
from matplotlib.axes import Axes
|
||||
from matplotlib.axis import XAxis, YAxis, Tick
|
||||
from matplotlib.backend_bases import RendererBase, MouseButton, MouseEvent
|
||||
from matplotlib.cbook import CallbackRegistry
|
||||
from matplotlib.container import Container
|
||||
from matplotlib.collections import Collection
|
||||
from matplotlib.cm import ScalarMappable
|
||||
from matplotlib.legend import Legend
|
||||
from matplotlib.lines import Line2D
|
||||
from matplotlib.gridspec import SubplotSpec, GridSpec
|
||||
from matplotlib.figure import Figure
|
||||
from matplotlib.image import AxesImage
|
||||
from matplotlib.patches import Patch
|
||||
from matplotlib.scale import ScaleBase
|
||||
from matplotlib.spines import Spines
|
||||
from matplotlib.table import Table
|
||||
from matplotlib.text import Text
|
||||
from matplotlib.transforms import Transform, Bbox
|
||||
|
||||
from cycler import Cycler
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import ArrayLike
|
||||
from typing import Any, Literal, TypeVar, overload
|
||||
from matplotlib.typing import ColorType
|
||||
|
||||
_T = TypeVar("_T", bound=Artist)
|
||||
|
||||
class _axis_method_wrapper:
|
||||
attr_name: str
|
||||
method_name: str
|
||||
__doc__: str
|
||||
def __init__(
|
||||
self, attr_name: str, method_name: str, *, doc_sub: dict[str, str] | None = ...
|
||||
) -> None: ...
|
||||
def __set_name__(self, owner: Any, name: str) -> None: ...
|
||||
|
||||
class _AxesBase(martist.Artist):
|
||||
name: str
|
||||
patch: Patch
|
||||
spines: Spines
|
||||
fmt_xdata: Callable[[float], str] | None
|
||||
fmt_ydata: Callable[[float], str] | None
|
||||
xaxis: XAxis
|
||||
yaxis: YAxis
|
||||
bbox: Bbox
|
||||
dataLim: Bbox
|
||||
transAxes: Transform
|
||||
transScale: Transform
|
||||
transLimits: Transform
|
||||
transData: Transform
|
||||
ignore_existing_data_limits: bool
|
||||
axison: bool
|
||||
containers: list[Container]
|
||||
callbacks: CallbackRegistry
|
||||
child_axes: list[_AxesBase]
|
||||
legend_: Legend | None
|
||||
title: Text
|
||||
_projection_init: Any
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fig: Figure,
|
||||
*args: tuple[float, float, float, float] | Bbox | int,
|
||||
facecolor: ColorType | None = ...,
|
||||
frameon: bool = ...,
|
||||
sharex: _AxesBase | None = ...,
|
||||
sharey: _AxesBase | None = ...,
|
||||
label: Any = ...,
|
||||
xscale: str | ScaleBase | None = ...,
|
||||
yscale: str | ScaleBase | None = ...,
|
||||
box_aspect: float | None = ...,
|
||||
forward_navigation_events: bool | Literal["auto"] = ...,
|
||||
**kwargs
|
||||
) -> None: ...
|
||||
def get_subplotspec(self) -> SubplotSpec | None: ...
|
||||
def set_subplotspec(self, subplotspec: SubplotSpec) -> None: ...
|
||||
def get_gridspec(self) -> GridSpec | None: ...
|
||||
def set_figure(self, fig: Figure) -> None: ...
|
||||
@property
|
||||
def viewLim(self) -> Bbox: ...
|
||||
def get_xaxis_transform(
|
||||
self, which: Literal["grid", "tick1", "tick2"] = ...
|
||||
) -> Transform: ...
|
||||
def get_xaxis_text1_transform(
|
||||
self, pad_points: float
|
||||
) -> tuple[
|
||||
Transform,
|
||||
Literal["center", "top", "bottom", "baseline", "center_baseline"],
|
||||
Literal["center", "left", "right"],
|
||||
]: ...
|
||||
def get_xaxis_text2_transform(
|
||||
self, pad_points
|
||||
) -> tuple[
|
||||
Transform,
|
||||
Literal["center", "top", "bottom", "baseline", "center_baseline"],
|
||||
Literal["center", "left", "right"],
|
||||
]: ...
|
||||
def get_yaxis_transform(
|
||||
self, which: Literal["grid", "tick1", "tick2"] = ...
|
||||
) -> Transform: ...
|
||||
def get_yaxis_text1_transform(
|
||||
self, pad_points
|
||||
) -> tuple[
|
||||
Transform,
|
||||
Literal["center", "top", "bottom", "baseline", "center_baseline"],
|
||||
Literal["center", "left", "right"],
|
||||
]: ...
|
||||
def get_yaxis_text2_transform(
|
||||
self, pad_points
|
||||
) -> tuple[
|
||||
Transform,
|
||||
Literal["center", "top", "bottom", "baseline", "center_baseline"],
|
||||
Literal["center", "left", "right"],
|
||||
]: ...
|
||||
def get_position(self, original: bool = ...) -> Bbox: ...
|
||||
def set_position(
|
||||
self,
|
||||
pos: Bbox | tuple[float, float, float, float],
|
||||
which: Literal["both", "active", "original"] = ...,
|
||||
) -> None: ...
|
||||
def reset_position(self) -> None: ...
|
||||
def set_axes_locator(
|
||||
self, locator: Callable[[_AxesBase, RendererBase], Bbox]
|
||||
) -> None: ...
|
||||
def get_axes_locator(self) -> Callable[[_AxesBase, RendererBase], Bbox]: ...
|
||||
def sharex(self, other: _AxesBase) -> None: ...
|
||||
def sharey(self, other: _AxesBase) -> None: ...
|
||||
def clear(self) -> None: ...
|
||||
def cla(self) -> None: ...
|
||||
|
||||
class ArtistList(Sequence[_T]):
|
||||
def __init__(
|
||||
self,
|
||||
axes: _AxesBase,
|
||||
prop_name: str,
|
||||
valid_types: type | Iterable[type] | None = ...,
|
||||
invalid_types: type | Iterable[type] | None = ...,
|
||||
) -> None: ...
|
||||
def __len__(self) -> int: ...
|
||||
def __iter__(self) -> Iterator[_T]: ...
|
||||
@overload
|
||||
def __getitem__(self, key: int) -> _T: ...
|
||||
@overload
|
||||
def __getitem__(self, key: slice) -> list[_T]: ...
|
||||
|
||||
@overload
|
||||
def __add__(self, other: _AxesBase.ArtistList[_T]) -> list[_T]: ...
|
||||
@overload
|
||||
def __add__(self, other: list[Any]) -> list[Any]: ...
|
||||
@overload
|
||||
def __add__(self, other: tuple[Any]) -> tuple[Any]: ...
|
||||
|
||||
@overload
|
||||
def __radd__(self, other: _AxesBase.ArtistList[_T]) -> list[_T]: ...
|
||||
@overload
|
||||
def __radd__(self, other: list[Any]) -> list[Any]: ...
|
||||
@overload
|
||||
def __radd__(self, other: tuple[Any]) -> tuple[Any]: ...
|
||||
|
||||
@property
|
||||
def artists(self) -> _AxesBase.ArtistList[Artist]: ...
|
||||
@property
|
||||
def collections(self) -> _AxesBase.ArtistList[Collection]: ...
|
||||
@property
|
||||
def images(self) -> _AxesBase.ArtistList[AxesImage]: ...
|
||||
@property
|
||||
def lines(self) -> _AxesBase.ArtistList[Line2D]: ...
|
||||
@property
|
||||
def patches(self) -> _AxesBase.ArtistList[Patch]: ...
|
||||
@property
|
||||
def tables(self) -> _AxesBase.ArtistList[Table]: ...
|
||||
@property
|
||||
def texts(self) -> _AxesBase.ArtistList[Text]: ...
|
||||
def get_facecolor(self) -> ColorType: ...
|
||||
def set_facecolor(self, color: ColorType | None) -> None: ...
|
||||
@overload
|
||||
def set_prop_cycle(self, cycler: Cycler) -> None: ...
|
||||
@overload
|
||||
def set_prop_cycle(self, label: str, values: Iterable[Any]) -> None: ...
|
||||
@overload
|
||||
def set_prop_cycle(self, **kwargs: Iterable[Any]) -> None: ...
|
||||
def get_aspect(self) -> float | Literal["auto"]: ...
|
||||
def set_aspect(
|
||||
self,
|
||||
aspect: float | Literal["auto", "equal"],
|
||||
adjustable: Literal["box", "datalim"] | None = ...,
|
||||
anchor: str | tuple[float, float] | None = ...,
|
||||
share: bool = ...,
|
||||
) -> None: ...
|
||||
def get_adjustable(self) -> Literal["box", "datalim"]: ...
|
||||
def set_adjustable(
|
||||
self, adjustable: Literal["box", "datalim"], share: bool = ...
|
||||
) -> None: ...
|
||||
def get_box_aspect(self) -> float | None: ...
|
||||
def set_box_aspect(self, aspect: float | None = ...) -> None: ...
|
||||
def get_anchor(self) -> str | tuple[float, float]: ...
|
||||
def set_anchor(
|
||||
self, anchor: str | tuple[float, float], share: bool = ...
|
||||
) -> None: ...
|
||||
def get_data_ratio(self) -> float: ...
|
||||
def apply_aspect(self, position: Bbox | None = ...) -> None: ...
|
||||
@overload
|
||||
def axis(
|
||||
self,
|
||||
arg: tuple[float, float, float, float] | bool | str | None = ...,
|
||||
/,
|
||||
*,
|
||||
emit: bool = ...
|
||||
) -> tuple[float, float, float, float]: ...
|
||||
@overload
|
||||
def axis(
|
||||
self,
|
||||
*,
|
||||
emit: bool = ...,
|
||||
xmin: float | None = ...,
|
||||
xmax: float | None = ...,
|
||||
ymin: float | None = ...,
|
||||
ymax: float | None = ...
|
||||
) -> tuple[float, float, float, float]: ...
|
||||
def get_legend(self) -> Legend: ...
|
||||
def get_images(self) -> list[AxesImage]: ...
|
||||
def get_lines(self) -> list[Line2D]: ...
|
||||
def get_xaxis(self) -> XAxis: ...
|
||||
def get_yaxis(self) -> YAxis: ...
|
||||
def has_data(self) -> bool: ...
|
||||
def add_artist(self, a: Artist) -> Artist: ...
|
||||
def add_child_axes(self, ax: _AxesBase) -> _AxesBase: ...
|
||||
def add_collection(
|
||||
self, collection: Collection, autolim: bool = ...
|
||||
) -> Collection: ...
|
||||
def add_image(self, image: AxesImage) -> AxesImage: ...
|
||||
def add_line(self, line: Line2D) -> Line2D: ...
|
||||
def add_patch(self, p: Patch) -> Patch: ...
|
||||
def add_table(self, tab: Table) -> Table: ...
|
||||
def add_container(self, container: Container) -> Container: ...
|
||||
def relim(self, visible_only: bool = ...) -> None: ...
|
||||
def update_datalim(
|
||||
self, xys: ArrayLike, updatex: bool = ..., updatey: bool = ...
|
||||
) -> None: ...
|
||||
def in_axes(self, mouseevent: MouseEvent) -> bool: ...
|
||||
def get_autoscale_on(self) -> bool: ...
|
||||
def set_autoscale_on(self, b: bool) -> None: ...
|
||||
@property
|
||||
def use_sticky_edges(self) -> bool: ...
|
||||
@use_sticky_edges.setter
|
||||
def use_sticky_edges(self, b: bool) -> None: ...
|
||||
def get_xmargin(self) -> float: ...
|
||||
def get_ymargin(self) -> float: ...
|
||||
def set_xmargin(self, m: float) -> None: ...
|
||||
def set_ymargin(self, m: float) -> None: ...
|
||||
|
||||
# Probably could be made better with overloads
|
||||
def margins(
|
||||
self,
|
||||
*margins: float,
|
||||
x: float | None = ...,
|
||||
y: float | None = ...,
|
||||
tight: bool | None = ...
|
||||
) -> tuple[float, float] | None: ...
|
||||
def set_rasterization_zorder(self, z: float | None) -> None: ...
|
||||
def get_rasterization_zorder(self) -> float | None: ...
|
||||
def autoscale(
|
||||
self,
|
||||
enable: bool = ...,
|
||||
axis: Literal["both", "x", "y"] = ...,
|
||||
tight: bool | None = ...,
|
||||
) -> None: ...
|
||||
def autoscale_view(
|
||||
self, tight: bool | None = ..., scalex: bool = ..., scaley: bool = ...
|
||||
) -> None: ...
|
||||
def draw_artist(self, a: Artist) -> None: ...
|
||||
def redraw_in_frame(self) -> None: ...
|
||||
def get_frame_on(self) -> bool: ...
|
||||
def set_frame_on(self, b: bool) -> None: ...
|
||||
def get_axisbelow(self) -> bool | Literal["line"]: ...
|
||||
def set_axisbelow(self, b: bool | Literal["line"]) -> None: ...
|
||||
def grid(
|
||||
self,
|
||||
visible: bool | None = ...,
|
||||
which: Literal["major", "minor", "both"] = ...,
|
||||
axis: Literal["both", "x", "y"] = ...,
|
||||
**kwargs
|
||||
) -> None: ...
|
||||
def ticklabel_format(
|
||||
self,
|
||||
*,
|
||||
axis: Literal["both", "x", "y"] = ...,
|
||||
style: Literal["", "sci", "scientific", "plain"] | None = ...,
|
||||
scilimits: tuple[int, int] | None = ...,
|
||||
useOffset: bool | float | None = ...,
|
||||
useLocale: bool | None = ...,
|
||||
useMathText: bool | None = ...
|
||||
) -> None: ...
|
||||
def locator_params(
|
||||
self, axis: Literal["both", "x", "y"] = ..., tight: bool | None = ..., **kwargs
|
||||
) -> None: ...
|
||||
def tick_params(self, axis: Literal["both", "x", "y"] = ..., **kwargs) -> None: ...
|
||||
def set_axis_off(self) -> None: ...
|
||||
def set_axis_on(self) -> None: ...
|
||||
def get_xlabel(self) -> str: ...
|
||||
def set_xlabel(
|
||||
self,
|
||||
xlabel: str,
|
||||
fontdict: dict[str, Any] | None = ...,
|
||||
labelpad: float | None = ...,
|
||||
*,
|
||||
loc: Literal["left", "center", "right"] | None = ...,
|
||||
**kwargs
|
||||
) -> Text: ...
|
||||
def invert_xaxis(self) -> None: ...
|
||||
def get_xbound(self) -> tuple[float, float]: ...
|
||||
def set_xbound(
|
||||
self, lower: float | None = ..., upper: float | None = ...
|
||||
) -> None: ...
|
||||
def get_xlim(self) -> tuple[float, float]: ...
|
||||
def set_xlim(
|
||||
self,
|
||||
left: float | tuple[float, float] | None = ...,
|
||||
right: float | None = ...,
|
||||
*,
|
||||
emit: bool = ...,
|
||||
auto: bool | None = ...,
|
||||
xmin: float | None = ...,
|
||||
xmax: float | None = ...
|
||||
) -> tuple[float, float]: ...
|
||||
def get_ylabel(self) -> str: ...
|
||||
def set_ylabel(
|
||||
self,
|
||||
ylabel: str,
|
||||
fontdict: dict[str, Any] | None = ...,
|
||||
labelpad: float | None = ...,
|
||||
*,
|
||||
loc: Literal["bottom", "center", "top"] | None = ...,
|
||||
**kwargs
|
||||
) -> Text: ...
|
||||
def invert_yaxis(self) -> None: ...
|
||||
def get_ybound(self) -> tuple[float, float]: ...
|
||||
def set_ybound(
|
||||
self, lower: float | None = ..., upper: float | None = ...
|
||||
) -> None: ...
|
||||
def get_ylim(self) -> tuple[float, float]: ...
|
||||
def set_ylim(
|
||||
self,
|
||||
bottom: float | tuple[float, float] | None = ...,
|
||||
top: float | None = ...,
|
||||
*,
|
||||
emit: bool = ...,
|
||||
auto: bool | None = ...,
|
||||
ymin: float | None = ...,
|
||||
ymax: float | None = ...
|
||||
) -> tuple[float, float]: ...
|
||||
def format_xdata(self, x: float) -> str: ...
|
||||
def format_ydata(self, y: float) -> str: ...
|
||||
def format_coord(self, x: float, y: float) -> str: ...
|
||||
def minorticks_on(self) -> None: ...
|
||||
def minorticks_off(self) -> None: ...
|
||||
def can_zoom(self) -> bool: ...
|
||||
def can_pan(self) -> bool: ...
|
||||
def get_navigate(self) -> bool: ...
|
||||
def set_navigate(self, b: bool) -> None: ...
|
||||
def get_forward_navigation_events(self) -> bool | Literal["auto"]: ...
|
||||
def set_forward_navigation_events(self, forward: bool | Literal["auto"]) -> None: ...
|
||||
def get_navigate_mode(self) -> Literal["PAN", "ZOOM"] | None: ...
|
||||
def set_navigate_mode(self, b: Literal["PAN", "ZOOM"] | None) -> None: ...
|
||||
def start_pan(self, x: float, y: float, button: MouseButton) -> None: ...
|
||||
def end_pan(self) -> None: ...
|
||||
def drag_pan(
|
||||
self, button: MouseButton, key: str | None, x: float, y: float
|
||||
) -> None: ...
|
||||
def get_children(self) -> list[Artist]: ...
|
||||
def contains_point(self, point: tuple[int, int]) -> bool: ...
|
||||
def get_default_bbox_extra_artists(self) -> list[Artist]: ...
|
||||
def get_tightbbox(
|
||||
self,
|
||||
renderer: RendererBase | None = ...,
|
||||
*,
|
||||
call_axes_locator: bool = ...,
|
||||
bbox_extra_artists: Sequence[Artist] | None = ...,
|
||||
for_layout_only: bool = ...
|
||||
) -> Bbox | None: ...
|
||||
def twinx(self) -> Axes: ...
|
||||
def twiny(self) -> Axes: ...
|
||||
def get_shared_x_axes(self) -> cbook.GrouperView: ...
|
||||
def get_shared_y_axes(self) -> cbook.GrouperView: ...
|
||||
def label_outer(self, remove_inner_ticks: bool = ...) -> None: ...
|
||||
|
||||
# The methods underneath this line are added via the `_axis_method_wrapper` class
|
||||
# Initially they are set to an object, but that object uses `__set_name__` to override
|
||||
# itself with a method modified from the Axis methods for the x or y Axis.
|
||||
# As such, they are typed according to the resultant method rather than as that object.
|
||||
|
||||
def get_xgridlines(self) -> list[Line2D]: ...
|
||||
def get_xticklines(self, minor: bool = ...) -> list[Line2D]: ...
|
||||
def get_ygridlines(self) -> list[Line2D]: ...
|
||||
def get_yticklines(self, minor: bool = ...) -> list[Line2D]: ...
|
||||
def _sci(self, im: ScalarMappable) -> None: ...
|
||||
def get_autoscalex_on(self) -> bool: ...
|
||||
def get_autoscaley_on(self) -> bool: ...
|
||||
def set_autoscalex_on(self, b: bool) -> None: ...
|
||||
def set_autoscaley_on(self, b: bool) -> None: ...
|
||||
def xaxis_inverted(self) -> bool: ...
|
||||
def get_xscale(self) -> str: ...
|
||||
def set_xscale(self, value: str | ScaleBase, **kwargs) -> None: ...
|
||||
def get_xticks(self, *, minor: bool = ...) -> np.ndarray: ...
|
||||
def set_xticks(
|
||||
self,
|
||||
ticks: ArrayLike,
|
||||
labels: Iterable[str] | None = ...,
|
||||
*,
|
||||
minor: bool = ...,
|
||||
**kwargs
|
||||
) -> list[Tick]: ...
|
||||
def get_xmajorticklabels(self) -> list[Text]: ...
|
||||
def get_xminorticklabels(self) -> list[Text]: ...
|
||||
def get_xticklabels(
|
||||
self, minor: bool = ..., which: Literal["major", "minor", "both"] | None = ...
|
||||
) -> list[Text]: ...
|
||||
def set_xticklabels(
|
||||
self,
|
||||
labels: Iterable[str | Text],
|
||||
*,
|
||||
minor: bool = ...,
|
||||
fontdict: dict[str, Any] | None = ...,
|
||||
**kwargs
|
||||
) -> list[Text]: ...
|
||||
def yaxis_inverted(self) -> bool: ...
|
||||
def get_yscale(self) -> str: ...
|
||||
def set_yscale(self, value: str | ScaleBase, **kwargs) -> None: ...
|
||||
def get_yticks(self, *, minor: bool = ...) -> np.ndarray: ...
|
||||
def set_yticks(
|
||||
self,
|
||||
ticks: ArrayLike,
|
||||
labels: Iterable[str] | None = ...,
|
||||
*,
|
||||
minor: bool = ...,
|
||||
**kwargs
|
||||
) -> list[Tick]: ...
|
||||
def get_ymajorticklabels(self) -> list[Text]: ...
|
||||
def get_yminorticklabels(self) -> list[Text]: ...
|
||||
def get_yticklabels(
|
||||
self, minor: bool = ..., which: Literal["major", "minor", "both"] | None = ...
|
||||
) -> list[Text]: ...
|
||||
def set_yticklabels(
|
||||
self,
|
||||
labels: Iterable[str | Text],
|
||||
*,
|
||||
minor: bool = ...,
|
||||
fontdict: dict[str, Any] | None = ...,
|
||||
**kwargs
|
||||
) -> list[Text]: ...
|
||||
def xaxis_date(self, tz: str | datetime.tzinfo | None = ...) -> None: ...
|
||||
def yaxis_date(self, tz: str | datetime.tzinfo | None = ...) -> None: ...
|
||||
@ -0,0 +1,321 @@
|
||||
import numbers
|
||||
|
||||
import numpy as np
|
||||
|
||||
from matplotlib import _api, _docstring, transforms
|
||||
import matplotlib.ticker as mticker
|
||||
from matplotlib.axes._base import _AxesBase, _TransformedBoundsLocator
|
||||
from matplotlib.axis import Axis
|
||||
from matplotlib.transforms import Transform
|
||||
|
||||
|
||||
class SecondaryAxis(_AxesBase):
|
||||
"""
|
||||
General class to hold a Secondary_X/Yaxis.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, orientation, location, functions, transform=None,
|
||||
**kwargs):
|
||||
"""
|
||||
See `.secondary_xaxis` and `.secondary_yaxis` for the doc string.
|
||||
While there is no need for this to be private, it should really be
|
||||
called by those higher level functions.
|
||||
"""
|
||||
_api.check_in_list(["x", "y"], orientation=orientation)
|
||||
self._functions = functions
|
||||
self._parent = parent
|
||||
self._orientation = orientation
|
||||
self._ticks_set = False
|
||||
|
||||
if self._orientation == 'x':
|
||||
super().__init__(self._parent.figure, [0, 1., 1, 0.0001], **kwargs)
|
||||
self._axis = self.xaxis
|
||||
self._locstrings = ['top', 'bottom']
|
||||
self._otherstrings = ['left', 'right']
|
||||
else: # 'y'
|
||||
super().__init__(self._parent.figure, [0, 1., 0.0001, 1], **kwargs)
|
||||
self._axis = self.yaxis
|
||||
self._locstrings = ['right', 'left']
|
||||
self._otherstrings = ['top', 'bottom']
|
||||
self._parentscale = None
|
||||
# this gets positioned w/o constrained_layout so exclude:
|
||||
|
||||
self.set_location(location, transform)
|
||||
self.set_functions(functions)
|
||||
|
||||
# styling:
|
||||
otheraxis = self.yaxis if self._orientation == 'x' else self.xaxis
|
||||
otheraxis.set_major_locator(mticker.NullLocator())
|
||||
otheraxis.set_ticks_position('none')
|
||||
|
||||
self.spines[self._otherstrings].set_visible(False)
|
||||
self.spines[self._locstrings].set_visible(True)
|
||||
|
||||
if self._pos < 0.5:
|
||||
# flip the location strings...
|
||||
self._locstrings = self._locstrings[::-1]
|
||||
self.set_alignment(self._locstrings[0])
|
||||
|
||||
def set_alignment(self, align):
|
||||
"""
|
||||
Set if axes spine and labels are drawn at top or bottom (or left/right)
|
||||
of the Axes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
align : {'top', 'bottom', 'left', 'right'}
|
||||
Either 'top' or 'bottom' for orientation='x' or
|
||||
'left' or 'right' for orientation='y' axis.
|
||||
"""
|
||||
_api.check_in_list(self._locstrings, align=align)
|
||||
if align == self._locstrings[1]: # Need to change the orientation.
|
||||
self._locstrings = self._locstrings[::-1]
|
||||
self.spines[self._locstrings[0]].set_visible(True)
|
||||
self.spines[self._locstrings[1]].set_visible(False)
|
||||
self._axis.set_ticks_position(align)
|
||||
self._axis.set_label_position(align)
|
||||
|
||||
def set_location(self, location, transform=None):
|
||||
"""
|
||||
Set the vertical or horizontal location of the axes in
|
||||
parent-normalized coordinates.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
location : {'top', 'bottom', 'left', 'right'} or float
|
||||
The position to put the secondary axis. Strings can be 'top' or
|
||||
'bottom' for orientation='x' and 'right' or 'left' for
|
||||
orientation='y'. A float indicates the relative position on the
|
||||
parent Axes to put the new Axes, 0.0 being the bottom (or left)
|
||||
and 1.0 being the top (or right).
|
||||
|
||||
transform : `.Transform`, optional
|
||||
Transform for the location to use. Defaults to
|
||||
the parent's ``transAxes``, so locations are normally relative to
|
||||
the parent axes.
|
||||
|
||||
.. versionadded:: 3.9
|
||||
"""
|
||||
|
||||
_api.check_isinstance((transforms.Transform, None), transform=transform)
|
||||
|
||||
# This puts the rectangle into figure-relative coordinates.
|
||||
if isinstance(location, str):
|
||||
_api.check_in_list(self._locstrings, location=location)
|
||||
self._pos = 1. if location in ('top', 'right') else 0.
|
||||
elif isinstance(location, numbers.Real):
|
||||
self._pos = location
|
||||
else:
|
||||
raise ValueError(
|
||||
f"location must be {self._locstrings[0]!r}, "
|
||||
f"{self._locstrings[1]!r}, or a float, not {location!r}")
|
||||
|
||||
self._loc = location
|
||||
|
||||
if self._orientation == 'x':
|
||||
# An x-secondary axes is like an inset axes from x = 0 to x = 1 and
|
||||
# from y = pos to y = pos + eps, in the parent's transAxes coords.
|
||||
bounds = [0, self._pos, 1., 1e-10]
|
||||
|
||||
# If a transformation is provided, use its y component rather than
|
||||
# the parent's transAxes. This can be used to place axes in the data
|
||||
# coords, for instance.
|
||||
if transform is not None:
|
||||
transform = transforms.blended_transform_factory(
|
||||
self._parent.transAxes, transform)
|
||||
else: # 'y'
|
||||
bounds = [self._pos, 0, 1e-10, 1]
|
||||
if transform is not None:
|
||||
transform = transforms.blended_transform_factory(
|
||||
transform, self._parent.transAxes) # Use provided x axis
|
||||
|
||||
# If no transform is provided, use the parent's transAxes
|
||||
if transform is None:
|
||||
transform = self._parent.transAxes
|
||||
|
||||
# this locator lets the axes move in the parent axes coordinates.
|
||||
# so it never needs to know where the parent is explicitly in
|
||||
# figure coordinates.
|
||||
# it gets called in ax.apply_aspect() (of all places)
|
||||
self.set_axes_locator(_TransformedBoundsLocator(bounds, transform))
|
||||
|
||||
def apply_aspect(self, position=None):
|
||||
# docstring inherited.
|
||||
self._set_lims()
|
||||
super().apply_aspect(position)
|
||||
|
||||
@_docstring.copy(Axis.set_ticks)
|
||||
def set_ticks(self, ticks, labels=None, *, minor=False, **kwargs):
|
||||
ret = self._axis.set_ticks(ticks, labels, minor=minor, **kwargs)
|
||||
self.stale = True
|
||||
self._ticks_set = True
|
||||
return ret
|
||||
|
||||
def set_functions(self, functions):
|
||||
"""
|
||||
Set how the secondary axis converts limits from the parent Axes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
functions : 2-tuple of func, or `Transform` with an inverse.
|
||||
Transform between the parent axis values and the secondary axis
|
||||
values.
|
||||
|
||||
If supplied as a 2-tuple of functions, the first function is
|
||||
the forward transform function and the second is the inverse
|
||||
transform.
|
||||
|
||||
If a transform is supplied, then the transform must have an
|
||||
inverse.
|
||||
"""
|
||||
|
||||
if (isinstance(functions, tuple) and len(functions) == 2 and
|
||||
callable(functions[0]) and callable(functions[1])):
|
||||
# make an arbitrary convert from a two-tuple of functions
|
||||
# forward and inverse.
|
||||
self._functions = functions
|
||||
elif isinstance(functions, Transform):
|
||||
self._functions = (
|
||||
functions.transform,
|
||||
lambda x: functions.inverted().transform(x)
|
||||
)
|
||||
elif functions is None:
|
||||
self._functions = (lambda x: x, lambda x: x)
|
||||
else:
|
||||
raise ValueError('functions argument of secondary Axes '
|
||||
'must be a two-tuple of callable functions '
|
||||
'with the first function being the transform '
|
||||
'and the second being the inverse')
|
||||
self._set_scale()
|
||||
|
||||
def draw(self, renderer):
|
||||
"""
|
||||
Draw the secondary Axes.
|
||||
|
||||
Consults the parent Axes for its limits and converts them
|
||||
using the converter specified by
|
||||
`~.axes._secondary_axes.set_functions` (or *functions*
|
||||
parameter when Axes initialized.)
|
||||
"""
|
||||
self._set_lims()
|
||||
# this sets the scale in case the parent has set its scale.
|
||||
self._set_scale()
|
||||
super().draw(renderer)
|
||||
|
||||
def _set_scale(self):
|
||||
"""
|
||||
Check if parent has set its scale
|
||||
"""
|
||||
|
||||
if self._orientation == 'x':
|
||||
pscale = self._parent.xaxis.get_scale()
|
||||
set_scale = self.set_xscale
|
||||
else: # 'y'
|
||||
pscale = self._parent.yaxis.get_scale()
|
||||
set_scale = self.set_yscale
|
||||
if pscale == self._parentscale:
|
||||
return
|
||||
|
||||
if self._ticks_set:
|
||||
ticks = self._axis.get_ticklocs()
|
||||
|
||||
# need to invert the roles here for the ticks to line up.
|
||||
set_scale('functionlog' if pscale == 'log' else 'function',
|
||||
functions=self._functions[::-1])
|
||||
|
||||
# OK, set_scale sets the locators, but if we've called
|
||||
# axsecond.set_ticks, we want to keep those.
|
||||
if self._ticks_set:
|
||||
self._axis.set_major_locator(mticker.FixedLocator(ticks))
|
||||
|
||||
# If the parent scale doesn't change, we can skip this next time.
|
||||
self._parentscale = pscale
|
||||
|
||||
def _set_lims(self):
|
||||
"""
|
||||
Set the limits based on parent limits and the convert method
|
||||
between the parent and this secondary Axes.
|
||||
"""
|
||||
if self._orientation == 'x':
|
||||
lims = self._parent.get_xlim()
|
||||
set_lim = self.set_xlim
|
||||
else: # 'y'
|
||||
lims = self._parent.get_ylim()
|
||||
set_lim = self.set_ylim
|
||||
order = lims[0] < lims[1]
|
||||
lims = self._functions[0](np.array(lims))
|
||||
neworder = lims[0] < lims[1]
|
||||
if neworder != order:
|
||||
# Flip because the transform will take care of the flipping.
|
||||
lims = lims[::-1]
|
||||
set_lim(lims)
|
||||
|
||||
def set_aspect(self, *args, **kwargs):
|
||||
"""
|
||||
Secondary Axes cannot set the aspect ratio, so calling this just
|
||||
sets a warning.
|
||||
"""
|
||||
_api.warn_external("Secondary Axes can't set the aspect ratio")
|
||||
|
||||
def set_color(self, color):
|
||||
"""
|
||||
Change the color of the secondary Axes and all decorators.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : :mpltype:`color`
|
||||
"""
|
||||
axis = self._axis_map[self._orientation]
|
||||
axis.set_tick_params(colors=color)
|
||||
for spine in self.spines.values():
|
||||
if spine.axis is axis:
|
||||
spine.set_color(color)
|
||||
axis.label.set_color(color)
|
||||
|
||||
|
||||
_secax_docstring = '''
|
||||
Warnings
|
||||
--------
|
||||
This method is experimental as of 3.1, and the API may change.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
location : {'top', 'bottom', 'left', 'right'} or float
|
||||
The position to put the secondary axis. Strings can be 'top' or
|
||||
'bottom' for orientation='x' and 'right' or 'left' for
|
||||
orientation='y'. A float indicates the relative position on the
|
||||
parent Axes to put the new Axes, 0.0 being the bottom (or left)
|
||||
and 1.0 being the top (or right).
|
||||
|
||||
functions : 2-tuple of func, or Transform with an inverse
|
||||
|
||||
If a 2-tuple of functions, the user specifies the transform
|
||||
function and its inverse. i.e.
|
||||
``functions=(lambda x: 2 / x, lambda x: 2 / x)`` would be an
|
||||
reciprocal transform with a factor of 2. Both functions must accept
|
||||
numpy arrays as input.
|
||||
|
||||
The user can also directly supply a subclass of
|
||||
`.transforms.Transform` so long as it has an inverse.
|
||||
|
||||
See :doc:`/gallery/subplots_axes_and_figures/secondary_axis`
|
||||
for examples of making these conversions.
|
||||
|
||||
transform : `.Transform`, optional
|
||||
If specified, *location* will be
|
||||
placed relative to this transform (in the direction of the axis)
|
||||
rather than the parent's axis. i.e. a secondary x-axis will
|
||||
use the provided y transform and the x transform of the parent.
|
||||
|
||||
.. versionadded:: 3.9
|
||||
|
||||
Returns
|
||||
-------
|
||||
ax : axes._secondary_axes.SecondaryAxis
|
||||
|
||||
Other Parameters
|
||||
----------------
|
||||
**kwargs : `~matplotlib.axes.Axes` properties.
|
||||
Other miscellaneous Axes parameters.
|
||||
'''
|
||||
_docstring.interpd.update(_secax_docstring=_secax_docstring)
|
||||
@ -0,0 +1,45 @@
|
||||
from matplotlib.axes._base import _AxesBase
|
||||
from matplotlib.axis import Tick
|
||||
|
||||
from matplotlib.transforms import Transform
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Literal
|
||||
from numpy.typing import ArrayLike
|
||||
from matplotlib.typing import ColorType
|
||||
|
||||
class SecondaryAxis(_AxesBase):
|
||||
def __init__(
|
||||
self,
|
||||
parent: _AxesBase,
|
||||
orientation: Literal["x", "y"],
|
||||
location: Literal["top", "bottom", "right", "left"] | float,
|
||||
functions: tuple[
|
||||
Callable[[ArrayLike], ArrayLike], Callable[[ArrayLike], ArrayLike]
|
||||
]
|
||||
| Transform,
|
||||
transform: Transform | None = ...,
|
||||
**kwargs
|
||||
) -> None: ...
|
||||
def set_alignment(
|
||||
self, align: Literal["top", "bottom", "right", "left"]
|
||||
) -> None: ...
|
||||
def set_location(
|
||||
self,
|
||||
location: Literal["top", "bottom", "right", "left"] | float,
|
||||
transform: Transform | None = ...
|
||||
) -> None: ...
|
||||
def set_ticks(
|
||||
self,
|
||||
ticks: ArrayLike,
|
||||
labels: Iterable[str] | None = ...,
|
||||
*,
|
||||
minor: bool = ...,
|
||||
**kwargs
|
||||
) -> list[Tick]: ...
|
||||
def set_functions(
|
||||
self,
|
||||
functions: tuple[Callable[[ArrayLike], ArrayLike], Callable[[ArrayLike], ArrayLike]] | Transform,
|
||||
) -> None: ...
|
||||
def set_aspect(self, *args, **kwargs) -> None: ...
|
||||
def set_color(self, color: ColorType) -> None: ...
|
||||
2811
venv/lib/python3.12/site-packages/matplotlib/axis.py
Normal file
2811
venv/lib/python3.12/site-packages/matplotlib/axis.py
Normal file
File diff suppressed because it is too large
Load Diff
280
venv/lib/python3.12/site-packages/matplotlib/axis.pyi
Normal file
280
venv/lib/python3.12/site-packages/matplotlib/axis.pyi
Normal file
@ -0,0 +1,280 @@
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
import datetime
|
||||
from typing import Any, Literal, overload
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
import matplotlib.artist as martist
|
||||
from matplotlib import cbook
|
||||
from matplotlib.axes import Axes
|
||||
from matplotlib.backend_bases import RendererBase
|
||||
from matplotlib.lines import Line2D
|
||||
from matplotlib.text import Text
|
||||
from matplotlib.ticker import Locator, Formatter
|
||||
from matplotlib.transforms import Transform, Bbox
|
||||
from matplotlib.typing import ColorType
|
||||
|
||||
|
||||
GRIDLINE_INTERPOLATION_STEPS: int
|
||||
|
||||
class Tick(martist.Artist):
|
||||
axes: Axes
|
||||
tick1line: Line2D
|
||||
tick2line: Line2D
|
||||
gridline: Line2D
|
||||
label1: Text
|
||||
label2: Text
|
||||
def __init__(
|
||||
self,
|
||||
axes: Axes,
|
||||
loc: float,
|
||||
*,
|
||||
size: float | None = ...,
|
||||
width: float | None = ...,
|
||||
color: ColorType | None = ...,
|
||||
tickdir: Literal["in", "inout", "out"] | None = ...,
|
||||
pad: float | None = ...,
|
||||
labelsize: float | None = ...,
|
||||
labelcolor: ColorType | None = ...,
|
||||
labelfontfamily: str | Sequence[str] | None = ...,
|
||||
zorder: float | None = ...,
|
||||
gridOn: bool | None = ...,
|
||||
tick1On: bool = ...,
|
||||
tick2On: bool = ...,
|
||||
label1On: bool = ...,
|
||||
label2On: bool = ...,
|
||||
major: bool = ...,
|
||||
labelrotation: float = ...,
|
||||
grid_color: ColorType | None = ...,
|
||||
grid_linestyle: str | None = ...,
|
||||
grid_linewidth: float | None = ...,
|
||||
grid_alpha: float | None = ...,
|
||||
**kwargs
|
||||
) -> None: ...
|
||||
def get_tickdir(self) -> Literal["in", "inout", "out"]: ...
|
||||
def get_tick_padding(self) -> float: ...
|
||||
def get_children(self) -> list[martist.Artist]: ...
|
||||
stale: bool
|
||||
def set_pad(self, val: float) -> None: ...
|
||||
def get_pad(self) -> None: ...
|
||||
def get_loc(self) -> float: ...
|
||||
def set_label1(self, s: object) -> None: ...
|
||||
def set_label(self, s: object) -> None: ...
|
||||
def set_label2(self, s: object) -> None: ...
|
||||
def set_url(self, url: str | None) -> None: ...
|
||||
def get_view_interval(self) -> ArrayLike: ...
|
||||
def update_position(self, loc: float) -> None: ...
|
||||
|
||||
class XTick(Tick):
|
||||
__name__: str
|
||||
def __init__(self, *args, **kwargs) -> None: ...
|
||||
stale: bool
|
||||
def update_position(self, loc: float) -> None: ...
|
||||
def get_view_interval(self) -> np.ndarray: ...
|
||||
|
||||
class YTick(Tick):
|
||||
__name__: str
|
||||
def __init__(self, *args, **kwargs) -> None: ...
|
||||
stale: bool
|
||||
def update_position(self, loc: float) -> None: ...
|
||||
def get_view_interval(self) -> np.ndarray: ...
|
||||
|
||||
class Ticker:
|
||||
def __init__(self) -> None: ...
|
||||
@property
|
||||
def locator(self) -> Locator | None: ...
|
||||
@locator.setter
|
||||
def locator(self, locator: Locator) -> None: ...
|
||||
@property
|
||||
def formatter(self) -> Formatter | None: ...
|
||||
@formatter.setter
|
||||
def formatter(self, formatter: Formatter) -> None: ...
|
||||
|
||||
class _LazyTickList:
|
||||
def __init__(self, major: bool) -> None: ...
|
||||
# Replace return with Self when py3.9 is dropped
|
||||
@overload
|
||||
def __get__(self, instance: None, owner: None) -> _LazyTickList: ...
|
||||
@overload
|
||||
def __get__(self, instance: Axis, owner: type[Axis]) -> list[Tick]: ...
|
||||
|
||||
class Axis(martist.Artist):
|
||||
OFFSETTEXTPAD: int
|
||||
isDefault_label: bool
|
||||
axes: Axes
|
||||
major: Ticker
|
||||
minor: Ticker
|
||||
callbacks: cbook.CallbackRegistry
|
||||
label: Text
|
||||
offsetText: Text
|
||||
labelpad: float
|
||||
pickradius: float
|
||||
def __init__(self, axes, *, pickradius: float = ...,
|
||||
clear: bool = ...) -> None: ...
|
||||
@property
|
||||
def isDefault_majloc(self) -> bool: ...
|
||||
@isDefault_majloc.setter
|
||||
def isDefault_majloc(self, value: bool) -> None: ...
|
||||
@property
|
||||
def isDefault_majfmt(self) -> bool: ...
|
||||
@isDefault_majfmt.setter
|
||||
def isDefault_majfmt(self, value: bool) -> None: ...
|
||||
@property
|
||||
def isDefault_minloc(self) -> bool: ...
|
||||
@isDefault_minloc.setter
|
||||
def isDefault_minloc(self, value: bool) -> None: ...
|
||||
@property
|
||||
def isDefault_minfmt(self) -> bool: ...
|
||||
@isDefault_minfmt.setter
|
||||
def isDefault_minfmt(self, value: bool) -> None: ...
|
||||
majorTicks: _LazyTickList
|
||||
minorTicks: _LazyTickList
|
||||
def get_remove_overlapping_locs(self) -> bool: ...
|
||||
def set_remove_overlapping_locs(self, val: bool) -> None: ...
|
||||
@property
|
||||
def remove_overlapping_locs(self) -> bool: ...
|
||||
@remove_overlapping_locs.setter
|
||||
def remove_overlapping_locs(self, val: bool) -> None: ...
|
||||
stale: bool
|
||||
def set_label_coords(
|
||||
self, x: float, y: float, transform: Transform | None = ...
|
||||
) -> None: ...
|
||||
def get_transform(self) -> Transform: ...
|
||||
def get_scale(self) -> str: ...
|
||||
def limit_range_for_scale(
|
||||
self, vmin: float, vmax: float
|
||||
) -> tuple[float, float]: ...
|
||||
def get_children(self) -> list[martist.Artist]: ...
|
||||
# TODO units
|
||||
converter: Any
|
||||
units: Any
|
||||
def clear(self) -> None: ...
|
||||
def reset_ticks(self) -> None: ...
|
||||
def minorticks_on(self) -> None: ...
|
||||
def minorticks_off(self) -> None: ...
|
||||
def set_tick_params(
|
||||
self,
|
||||
which: Literal["major", "minor", "both"] = ...,
|
||||
reset: bool = ...,
|
||||
**kwargs
|
||||
) -> None: ...
|
||||
def get_tick_params(
|
||||
self, which: Literal["major", "minor"] = ...
|
||||
) -> dict[str, Any]: ...
|
||||
def get_view_interval(self) -> tuple[float, float]: ...
|
||||
def set_view_interval(
|
||||
self, vmin: float, vmax: float, ignore: bool = ...
|
||||
) -> None: ...
|
||||
def get_data_interval(self) -> tuple[float, float]: ...
|
||||
def set_data_interval(
|
||||
self, vmin: float, vmax: float, ignore: bool = ...
|
||||
) -> None: ...
|
||||
def get_inverted(self) -> bool: ...
|
||||
def set_inverted(self, inverted: bool) -> None: ...
|
||||
def set_default_intervals(self) -> None: ...
|
||||
def get_tightbbox(
|
||||
self, renderer: RendererBase | None = ..., *, for_layout_only: bool = ...
|
||||
) -> Bbox | None: ...
|
||||
def get_tick_padding(self) -> float: ...
|
||||
def get_gridlines(self) -> list[Line2D]: ...
|
||||
def get_label(self) -> Text: ...
|
||||
def get_offset_text(self) -> Text: ...
|
||||
def get_pickradius(self) -> float: ...
|
||||
def get_majorticklabels(self) -> list[Text]: ...
|
||||
def get_minorticklabels(self) -> list[Text]: ...
|
||||
def get_ticklabels(
|
||||
self, minor: bool = ..., which: Literal["major", "minor", "both"] | None = ...
|
||||
) -> list[Text]: ...
|
||||
def get_majorticklines(self) -> list[Line2D]: ...
|
||||
def get_minorticklines(self) -> list[Line2D]: ...
|
||||
def get_ticklines(self, minor: bool = ...) -> list[Line2D]: ...
|
||||
def get_majorticklocs(self) -> np.ndarray: ...
|
||||
def get_minorticklocs(self) -> np.ndarray: ...
|
||||
def get_ticklocs(self, *, minor: bool = ...) -> np.ndarray: ...
|
||||
def get_ticks_direction(self, minor: bool = ...) -> np.ndarray: ...
|
||||
def get_label_text(self) -> str: ...
|
||||
def get_major_locator(self) -> Locator: ...
|
||||
def get_minor_locator(self) -> Locator: ...
|
||||
def get_major_formatter(self) -> Formatter: ...
|
||||
def get_minor_formatter(self) -> Formatter: ...
|
||||
def get_major_ticks(self, numticks: int | None = ...) -> list[Tick]: ...
|
||||
def get_minor_ticks(self, numticks: int | None = ...) -> list[Tick]: ...
|
||||
def grid(
|
||||
self,
|
||||
visible: bool | None = ...,
|
||||
which: Literal["major", "minor", "both"] = ...,
|
||||
**kwargs
|
||||
) -> None: ...
|
||||
# TODO units
|
||||
def update_units(self, data): ...
|
||||
def have_units(self) -> bool: ...
|
||||
def convert_units(self, x): ...
|
||||
def set_units(self, u) -> None: ...
|
||||
def get_units(self): ...
|
||||
def set_label_text(
|
||||
self, label: str, fontdict: dict[str, Any] | None = ..., **kwargs
|
||||
) -> Text: ...
|
||||
def set_major_formatter(
|
||||
self, formatter: Formatter | str | Callable[[float, float], str]
|
||||
) -> None: ...
|
||||
def set_minor_formatter(
|
||||
self, formatter: Formatter | str | Callable[[float, float], str]
|
||||
) -> None: ...
|
||||
def set_major_locator(self, locator: Locator) -> None: ...
|
||||
def set_minor_locator(self, locator: Locator) -> None: ...
|
||||
def set_pickradius(self, pickradius: float) -> None: ...
|
||||
def set_ticklabels(
|
||||
self,
|
||||
labels: Iterable[str | Text],
|
||||
*,
|
||||
minor: bool = ...,
|
||||
fontdict: dict[str, Any] | None = ...,
|
||||
**kwargs
|
||||
) -> list[Text]: ...
|
||||
def set_ticks(
|
||||
self,
|
||||
ticks: ArrayLike,
|
||||
labels: Iterable[str] | None = ...,
|
||||
*,
|
||||
minor: bool = ...,
|
||||
**kwargs
|
||||
) -> list[Tick]: ...
|
||||
def axis_date(self, tz: str | datetime.tzinfo | None = ...) -> None: ...
|
||||
def get_tick_space(self) -> int: ...
|
||||
def get_label_position(self) -> Literal["top", "bottom"]: ...
|
||||
def set_label_position(
|
||||
self, position: Literal["top", "bottom", "left", "right"]
|
||||
) -> None: ...
|
||||
def get_minpos(self) -> float: ...
|
||||
|
||||
class XAxis(Axis):
|
||||
__name__: str
|
||||
axis_name: str
|
||||
def __init__(self, *args, **kwargs) -> None: ...
|
||||
label_position: Literal["bottom", "top"]
|
||||
stale: bool
|
||||
def set_label_position(self, position: Literal["bottom", "top"]) -> None: ... # type: ignore[override]
|
||||
def set_ticks_position(
|
||||
self, position: Literal["top", "bottom", "both", "default", "none"]
|
||||
) -> None: ...
|
||||
def tick_top(self) -> None: ...
|
||||
def tick_bottom(self) -> None: ...
|
||||
def get_ticks_position(self) -> Literal["top", "bottom", "default", "unknown"]: ...
|
||||
def get_tick_space(self) -> int: ...
|
||||
|
||||
class YAxis(Axis):
|
||||
__name__: str
|
||||
axis_name: str
|
||||
def __init__(self, *args, **kwargs) -> None: ...
|
||||
label_position: Literal["left", "right"]
|
||||
stale: bool
|
||||
def set_label_position(self, position: Literal["left", "right"]) -> None: ... # type: ignore[override]
|
||||
def set_offset_position(self, position: Literal["left", "right"]) -> None: ...
|
||||
def set_ticks_position(
|
||||
self, position: Literal["left", "right", "both", "default", "none"]
|
||||
) -> None: ...
|
||||
def tick_right(self) -> None: ...
|
||||
def tick_left(self) -> None: ...
|
||||
def get_ticks_position(self) -> Literal["left", "right", "default", "unknown"]: ...
|
||||
def get_tick_space(self) -> int: ...
|
||||
3588
venv/lib/python3.12/site-packages/matplotlib/backend_bases.py
Normal file
3588
venv/lib/python3.12/site-packages/matplotlib/backend_bases.py
Normal file
File diff suppressed because it is too large
Load Diff
483
venv/lib/python3.12/site-packages/matplotlib/backend_bases.pyi
Normal file
483
venv/lib/python3.12/site-packages/matplotlib/backend_bases.pyi
Normal file
@ -0,0 +1,483 @@
|
||||
from enum import Enum, IntEnum
|
||||
import os
|
||||
from matplotlib import (
|
||||
cbook,
|
||||
transforms,
|
||||
widgets,
|
||||
_api,
|
||||
)
|
||||
from matplotlib.artist import Artist
|
||||
from matplotlib.axes import Axes
|
||||
from matplotlib.backend_managers import ToolManager
|
||||
from matplotlib.backend_tools import Cursors, ToolBase
|
||||
from matplotlib.colorbar import Colorbar
|
||||
from matplotlib.figure import Figure
|
||||
from matplotlib.font_manager import FontProperties
|
||||
from matplotlib.path import Path
|
||||
from matplotlib.texmanager import TexManager
|
||||
from matplotlib.text import Text
|
||||
from matplotlib.transforms import Bbox, BboxBase, Transform, TransformedPath
|
||||
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import Any, IO, Literal, NamedTuple, TypeVar
|
||||
from numpy.typing import ArrayLike
|
||||
from .typing import ColorType, LineStyleType, CapStyleType, JoinStyleType
|
||||
|
||||
def register_backend(
|
||||
format: str, backend: str | type[FigureCanvasBase], description: str | None = ...
|
||||
) -> None: ...
|
||||
def get_registered_canvas_class(format: str) -> type[FigureCanvasBase]: ...
|
||||
|
||||
class RendererBase:
|
||||
def __init__(self) -> None: ...
|
||||
def open_group(self, s: str, gid: str | None = ...) -> None: ...
|
||||
def close_group(self, s: str) -> None: ...
|
||||
def draw_path(
|
||||
self,
|
||||
gc: GraphicsContextBase,
|
||||
path: Path,
|
||||
transform: Transform,
|
||||
rgbFace: ColorType | None = ...,
|
||||
) -> None: ...
|
||||
def draw_markers(
|
||||
self,
|
||||
gc: GraphicsContextBase,
|
||||
marker_path: Path,
|
||||
marker_trans: Transform,
|
||||
path: Path,
|
||||
trans: Transform,
|
||||
rgbFace: ColorType | None = ...,
|
||||
) -> None: ...
|
||||
def draw_path_collection(
|
||||
self,
|
||||
gc: GraphicsContextBase,
|
||||
master_transform: Transform,
|
||||
paths: Sequence[Path],
|
||||
all_transforms: Sequence[ArrayLike],
|
||||
offsets: ArrayLike | Sequence[ArrayLike],
|
||||
offset_trans: Transform,
|
||||
facecolors: ColorType | Sequence[ColorType],
|
||||
edgecolors: ColorType | Sequence[ColorType],
|
||||
linewidths: float | Sequence[float],
|
||||
linestyles: LineStyleType | Sequence[LineStyleType],
|
||||
antialiaseds: bool | Sequence[bool],
|
||||
urls: str | Sequence[str],
|
||||
offset_position: Any,
|
||||
) -> None: ...
|
||||
def draw_quad_mesh(
|
||||
self,
|
||||
gc: GraphicsContextBase,
|
||||
master_transform: Transform,
|
||||
meshWidth,
|
||||
meshHeight,
|
||||
coordinates: ArrayLike,
|
||||
offsets: ArrayLike | Sequence[ArrayLike],
|
||||
offsetTrans: Transform,
|
||||
facecolors: Sequence[ColorType],
|
||||
antialiased: bool,
|
||||
edgecolors: Sequence[ColorType] | ColorType | None,
|
||||
) -> None: ...
|
||||
def draw_gouraud_triangles(
|
||||
self,
|
||||
gc: GraphicsContextBase,
|
||||
triangles_array: ArrayLike,
|
||||
colors_array: ArrayLike,
|
||||
transform: Transform,
|
||||
) -> None: ...
|
||||
def get_image_magnification(self) -> float: ...
|
||||
def draw_image(
|
||||
self,
|
||||
gc: GraphicsContextBase,
|
||||
x: float,
|
||||
y: float,
|
||||
im: ArrayLike,
|
||||
transform: transforms.Affine2DBase | None = ...,
|
||||
) -> None: ...
|
||||
def option_image_nocomposite(self) -> bool: ...
|
||||
def option_scale_image(self) -> bool: ...
|
||||
def draw_tex(
|
||||
self,
|
||||
gc: GraphicsContextBase,
|
||||
x: float,
|
||||
y: float,
|
||||
s: str,
|
||||
prop: FontProperties,
|
||||
angle: float,
|
||||
*,
|
||||
mtext: Text | None = ...
|
||||
) -> None: ...
|
||||
def draw_text(
|
||||
self,
|
||||
gc: GraphicsContextBase,
|
||||
x: float,
|
||||
y: float,
|
||||
s: str,
|
||||
prop: FontProperties,
|
||||
angle: float,
|
||||
ismath: bool | Literal["TeX"] = ...,
|
||||
mtext: Text | None = ...,
|
||||
) -> None: ...
|
||||
def get_text_width_height_descent(
|
||||
self, s: str, prop: FontProperties, ismath: bool | Literal["TeX"]
|
||||
) -> tuple[float, float, float]: ...
|
||||
def flipy(self) -> bool: ...
|
||||
def get_canvas_width_height(self) -> tuple[float, float]: ...
|
||||
def get_texmanager(self) -> TexManager: ...
|
||||
def new_gc(self) -> GraphicsContextBase: ...
|
||||
def points_to_pixels(self, points: ArrayLike) -> ArrayLike: ...
|
||||
def start_rasterizing(self) -> None: ...
|
||||
def stop_rasterizing(self) -> None: ...
|
||||
def start_filter(self) -> None: ...
|
||||
def stop_filter(self, filter_func) -> None: ...
|
||||
|
||||
class GraphicsContextBase:
|
||||
def __init__(self) -> None: ...
|
||||
def copy_properties(self, gc: GraphicsContextBase) -> None: ...
|
||||
def restore(self) -> None: ...
|
||||
def get_alpha(self) -> float: ...
|
||||
def get_antialiased(self) -> int: ...
|
||||
def get_capstyle(self) -> Literal["butt", "projecting", "round"]: ...
|
||||
def get_clip_rectangle(self) -> Bbox | None: ...
|
||||
def get_clip_path(
|
||||
self,
|
||||
) -> tuple[TransformedPath, Transform] | tuple[None, None]: ...
|
||||
def get_dashes(self) -> tuple[float, ArrayLike | None]: ...
|
||||
def get_forced_alpha(self) -> bool: ...
|
||||
def get_joinstyle(self) -> Literal["miter", "round", "bevel"]: ...
|
||||
def get_linewidth(self) -> float: ...
|
||||
def get_rgb(self) -> tuple[float, float, float, float]: ...
|
||||
def get_url(self) -> str | None: ...
|
||||
def get_gid(self) -> int | None: ...
|
||||
def get_snap(self) -> bool | None: ...
|
||||
def set_alpha(self, alpha: float) -> None: ...
|
||||
def set_antialiased(self, b: bool) -> None: ...
|
||||
def set_capstyle(self, cs: CapStyleType) -> None: ...
|
||||
def set_clip_rectangle(self, rectangle: Bbox | None) -> None: ...
|
||||
def set_clip_path(self, path: TransformedPath | None) -> None: ...
|
||||
def set_dashes(self, dash_offset: float, dash_list: ArrayLike | None) -> None: ...
|
||||
def set_foreground(self, fg: ColorType, isRGBA: bool = ...) -> None: ...
|
||||
def set_joinstyle(self, js: JoinStyleType) -> None: ...
|
||||
def set_linewidth(self, w: float) -> None: ...
|
||||
def set_url(self, url: str | None) -> None: ...
|
||||
def set_gid(self, id: int | None) -> None: ...
|
||||
def set_snap(self, snap: bool | None) -> None: ...
|
||||
def set_hatch(self, hatch: str | None) -> None: ...
|
||||
def get_hatch(self) -> str | None: ...
|
||||
def get_hatch_path(self, density: float = ...) -> Path: ...
|
||||
def get_hatch_color(self) -> ColorType: ...
|
||||
def set_hatch_color(self, hatch_color: ColorType) -> None: ...
|
||||
def get_hatch_linewidth(self) -> float: ...
|
||||
def get_sketch_params(self) -> tuple[float, float, float] | None: ...
|
||||
def set_sketch_params(
|
||||
self,
|
||||
scale: float | None = ...,
|
||||
length: float | None = ...,
|
||||
randomness: float | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
class TimerBase:
|
||||
callbacks: list[tuple[Callable, tuple, dict[str, Any]]]
|
||||
def __init__(
|
||||
self,
|
||||
interval: int | None = ...,
|
||||
callbacks: list[tuple[Callable, tuple, dict[str, Any]]] | None = ...,
|
||||
) -> None: ...
|
||||
def __del__(self) -> None: ...
|
||||
def start(self, interval: int | None = ...) -> None: ...
|
||||
def stop(self) -> None: ...
|
||||
@property
|
||||
def interval(self) -> int: ...
|
||||
@interval.setter
|
||||
def interval(self, interval: int) -> None: ...
|
||||
@property
|
||||
def single_shot(self) -> bool: ...
|
||||
@single_shot.setter
|
||||
def single_shot(self, ss: bool) -> None: ...
|
||||
def add_callback(self, func: Callable, *args, **kwargs) -> Callable: ...
|
||||
def remove_callback(self, func: Callable, *args, **kwargs) -> None: ...
|
||||
|
||||
class Event:
|
||||
name: str
|
||||
canvas: FigureCanvasBase
|
||||
def __init__(
|
||||
self, name: str, canvas: FigureCanvasBase, guiEvent: Any | None = ...
|
||||
) -> None: ...
|
||||
|
||||
@property
|
||||
def guiEvent(self) -> Any: ...
|
||||
|
||||
class DrawEvent(Event):
|
||||
renderer: RendererBase
|
||||
def __init__(
|
||||
self, name: str, canvas: FigureCanvasBase, renderer: RendererBase
|
||||
) -> None: ...
|
||||
|
||||
class ResizeEvent(Event):
|
||||
width: int
|
||||
height: int
|
||||
def __init__(self, name: str, canvas: FigureCanvasBase) -> None: ...
|
||||
|
||||
class CloseEvent(Event): ...
|
||||
|
||||
class LocationEvent(Event):
|
||||
lastevent: Event | None
|
||||
x: int
|
||||
y: int
|
||||
inaxes: Axes | None
|
||||
xdata: float | None
|
||||
ydata: float | None
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
canvas: FigureCanvasBase,
|
||||
x: int,
|
||||
y: int,
|
||||
guiEvent: Any | None = ...,
|
||||
*,
|
||||
modifiers: Iterable[str] | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
class MouseButton(IntEnum):
|
||||
LEFT: int
|
||||
MIDDLE: int
|
||||
RIGHT: int
|
||||
BACK: int
|
||||
FORWARD: int
|
||||
|
||||
class MouseEvent(LocationEvent):
|
||||
button: MouseButton | Literal["up", "down"] | None
|
||||
key: str | None
|
||||
step: float
|
||||
dblclick: bool
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
canvas: FigureCanvasBase,
|
||||
x: int,
|
||||
y: int,
|
||||
button: MouseButton | Literal["up", "down"] | None = ...,
|
||||
key: str | None = ...,
|
||||
step: float = ...,
|
||||
dblclick: bool = ...,
|
||||
guiEvent: Any | None = ...,
|
||||
*,
|
||||
modifiers: Iterable[str] | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
class PickEvent(Event):
|
||||
mouseevent: MouseEvent
|
||||
artist: Artist
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
canvas: FigureCanvasBase,
|
||||
mouseevent: MouseEvent,
|
||||
artist: Artist,
|
||||
guiEvent: Any | None = ...,
|
||||
**kwargs
|
||||
) -> None: ...
|
||||
|
||||
class KeyEvent(LocationEvent):
|
||||
key: str | None
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
canvas: FigureCanvasBase,
|
||||
key: str | None,
|
||||
x: int = ...,
|
||||
y: int = ...,
|
||||
guiEvent: Any | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
class FigureCanvasBase:
|
||||
required_interactive_framework: str | None
|
||||
|
||||
@_api.classproperty
|
||||
def manager_class(cls) -> type[FigureManagerBase]: ...
|
||||
events: list[str]
|
||||
fixed_dpi: None | float
|
||||
filetypes: dict[str, str]
|
||||
|
||||
@_api.classproperty
|
||||
def supports_blit(cls) -> bool: ...
|
||||
|
||||
figure: Figure
|
||||
manager: None | FigureManagerBase
|
||||
widgetlock: widgets.LockDraw
|
||||
mouse_grabber: None | Axes
|
||||
toolbar: None | NavigationToolbar2
|
||||
def __init__(self, figure: Figure | None = ...) -> None: ...
|
||||
@property
|
||||
def callbacks(self) -> cbook.CallbackRegistry: ...
|
||||
@property
|
||||
def button_pick_id(self) -> int: ...
|
||||
@property
|
||||
def scroll_pick_id(self) -> int: ...
|
||||
@classmethod
|
||||
def new_manager(cls, figure: Figure, num: int | str) -> FigureManagerBase: ...
|
||||
def is_saving(self) -> bool: ...
|
||||
def blit(self, bbox: BboxBase | None = ...) -> None: ...
|
||||
def inaxes(self, xy: tuple[float, float]) -> Axes | None: ...
|
||||
def grab_mouse(self, ax: Axes) -> None: ...
|
||||
def release_mouse(self, ax: Axes) -> None: ...
|
||||
def set_cursor(self, cursor: Cursors) -> None: ...
|
||||
def draw(self, *args, **kwargs) -> None: ...
|
||||
def draw_idle(self, *args, **kwargs) -> None: ...
|
||||
@property
|
||||
def device_pixel_ratio(self) -> float: ...
|
||||
def get_width_height(self, *, physical: bool = ...) -> tuple[int, int]: ...
|
||||
@classmethod
|
||||
def get_supported_filetypes(cls) -> dict[str, str]: ...
|
||||
@classmethod
|
||||
def get_supported_filetypes_grouped(cls) -> dict[str, list[str]]: ...
|
||||
def print_figure(
|
||||
self,
|
||||
filename: str | os.PathLike | IO,
|
||||
dpi: float | None = ...,
|
||||
facecolor: ColorType | Literal["auto"] | None = ...,
|
||||
edgecolor: ColorType | Literal["auto"] | None = ...,
|
||||
orientation: str = ...,
|
||||
format: str | None = ...,
|
||||
*,
|
||||
bbox_inches: Literal["tight"] | Bbox | None = ...,
|
||||
pad_inches: float | None = ...,
|
||||
bbox_extra_artists: list[Artist] | None = ...,
|
||||
backend: str | None = ...,
|
||||
**kwargs
|
||||
) -> Any: ...
|
||||
@classmethod
|
||||
def get_default_filetype(cls) -> str: ...
|
||||
def get_default_filename(self) -> str: ...
|
||||
_T = TypeVar("_T", bound=FigureCanvasBase)
|
||||
def switch_backends(self, FigureCanvasClass: type[_T]) -> _T: ...
|
||||
def mpl_connect(self, s: str, func: Callable[[Event], Any]) -> int: ...
|
||||
def mpl_disconnect(self, cid: int) -> None: ...
|
||||
def new_timer(
|
||||
self,
|
||||
interval: int | None = ...,
|
||||
callbacks: list[tuple[Callable, tuple, dict[str, Any]]] | None = ...,
|
||||
) -> TimerBase: ...
|
||||
def flush_events(self) -> None: ...
|
||||
def start_event_loop(self, timeout: float = ...) -> None: ...
|
||||
def stop_event_loop(self) -> None: ...
|
||||
|
||||
def key_press_handler(
|
||||
event: KeyEvent,
|
||||
canvas: FigureCanvasBase | None = ...,
|
||||
toolbar: NavigationToolbar2 | None = ...,
|
||||
) -> None: ...
|
||||
def button_press_handler(
|
||||
event: MouseEvent,
|
||||
canvas: FigureCanvasBase | None = ...,
|
||||
toolbar: NavigationToolbar2 | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
class NonGuiException(Exception): ...
|
||||
|
||||
class FigureManagerBase:
|
||||
canvas: FigureCanvasBase
|
||||
num: int | str
|
||||
key_press_handler_id: int | None
|
||||
button_press_handler_id: int | None
|
||||
toolmanager: ToolManager | None
|
||||
toolbar: NavigationToolbar2 | ToolContainerBase | None
|
||||
def __init__(self, canvas: FigureCanvasBase, num: int | str) -> None: ...
|
||||
@classmethod
|
||||
def create_with_canvas(
|
||||
cls, canvas_class: type[FigureCanvasBase], figure: Figure, num: int | str
|
||||
) -> FigureManagerBase: ...
|
||||
@classmethod
|
||||
def start_main_loop(cls) -> None: ...
|
||||
@classmethod
|
||||
def pyplot_show(cls, *, block: bool | None = ...) -> None: ...
|
||||
def show(self) -> None: ...
|
||||
def destroy(self) -> None: ...
|
||||
def full_screen_toggle(self) -> None: ...
|
||||
def resize(self, w: int, h: int) -> None: ...
|
||||
def get_window_title(self) -> str: ...
|
||||
def set_window_title(self, title: str) -> None: ...
|
||||
|
||||
cursors = Cursors
|
||||
|
||||
class _Mode(str, Enum):
|
||||
NONE: str
|
||||
PAN: str
|
||||
ZOOM: str
|
||||
|
||||
class NavigationToolbar2:
|
||||
toolitems: tuple[tuple[str, ...] | tuple[None, ...], ...]
|
||||
canvas: FigureCanvasBase
|
||||
mode: _Mode
|
||||
def __init__(self, canvas: FigureCanvasBase) -> None: ...
|
||||
def set_message(self, s: str) -> None: ...
|
||||
def draw_rubberband(
|
||||
self, event: Event, x0: float, y0: float, x1: float, y1: float
|
||||
) -> None: ...
|
||||
def remove_rubberband(self) -> None: ...
|
||||
def home(self, *args) -> None: ...
|
||||
def back(self, *args) -> None: ...
|
||||
def forward(self, *args) -> None: ...
|
||||
def mouse_move(self, event: MouseEvent) -> None: ...
|
||||
def pan(self, *args) -> None: ...
|
||||
|
||||
class _PanInfo(NamedTuple):
|
||||
button: MouseButton
|
||||
axes: list[Axes]
|
||||
cid: int
|
||||
def press_pan(self, event: Event) -> None: ...
|
||||
def drag_pan(self, event: Event) -> None: ...
|
||||
def release_pan(self, event: Event) -> None: ...
|
||||
def zoom(self, *args) -> None: ...
|
||||
|
||||
class _ZoomInfo(NamedTuple):
|
||||
direction: Literal["in", "out"]
|
||||
start_xy: tuple[float, float]
|
||||
axes: list[Axes]
|
||||
cid: int
|
||||
cbar: Colorbar
|
||||
def press_zoom(self, event: Event) -> None: ...
|
||||
def drag_zoom(self, event: Event) -> None: ...
|
||||
def release_zoom(self, event: Event) -> None: ...
|
||||
def push_current(self) -> None: ...
|
||||
subplot_tool: widgets.SubplotTool
|
||||
def configure_subplots(self, *args): ...
|
||||
def save_figure(self, *args) -> None: ...
|
||||
def update(self) -> None: ...
|
||||
def set_history_buttons(self) -> None: ...
|
||||
|
||||
class ToolContainerBase:
|
||||
toolmanager: ToolManager
|
||||
def __init__(self, toolmanager: ToolManager) -> None: ...
|
||||
def add_tool(self, tool: ToolBase, group: str, position: int = ...) -> None: ...
|
||||
def trigger_tool(self, name: str) -> None: ...
|
||||
def add_toolitem(
|
||||
self,
|
||||
name: str,
|
||||
group: str,
|
||||
position: int,
|
||||
image: str,
|
||||
description: str,
|
||||
toggle: bool,
|
||||
) -> None: ...
|
||||
def toggle_toolitem(self, name: str, toggled: bool) -> None: ...
|
||||
def remove_toolitem(self, name: str) -> None: ...
|
||||
def set_message(self, s: str) -> None: ...
|
||||
|
||||
class _Backend:
|
||||
backend_version: str
|
||||
FigureCanvas: type[FigureCanvasBase] | None
|
||||
FigureManager: type[FigureManagerBase]
|
||||
mainloop: None | Callable[[], Any]
|
||||
@classmethod
|
||||
def new_figure_manager(cls, num: int | str, *args, **kwargs) -> FigureManagerBase: ...
|
||||
@classmethod
|
||||
def new_figure_manager_given_figure(cls, num: int | str, figure: Figure) -> FigureManagerBase: ...
|
||||
@classmethod
|
||||
def draw_if_interactive(cls) -> None: ...
|
||||
@classmethod
|
||||
def show(cls, *, block: bool | None = ...) -> None: ...
|
||||
@staticmethod
|
||||
def export(cls) -> type[_Backend]: ...
|
||||
|
||||
class ShowBase(_Backend):
|
||||
def __call__(self, block: bool | None = ...) -> None: ...
|
||||
387
venv/lib/python3.12/site-packages/matplotlib/backend_managers.py
Normal file
387
venv/lib/python3.12/site-packages/matplotlib/backend_managers.py
Normal file
@ -0,0 +1,387 @@
|
||||
from matplotlib import _api, backend_tools, cbook, widgets
|
||||
|
||||
|
||||
class ToolEvent:
|
||||
"""Event for tool manipulation (add/remove)."""
|
||||
def __init__(self, name, sender, tool, data=None):
|
||||
self.name = name
|
||||
self.sender = sender
|
||||
self.tool = tool
|
||||
self.data = data
|
||||
|
||||
|
||||
class ToolTriggerEvent(ToolEvent):
|
||||
"""Event to inform that a tool has been triggered."""
|
||||
def __init__(self, name, sender, tool, canvasevent=None, data=None):
|
||||
super().__init__(name, sender, tool, data)
|
||||
self.canvasevent = canvasevent
|
||||
|
||||
|
||||
class ToolManagerMessageEvent:
|
||||
"""
|
||||
Event carrying messages from toolmanager.
|
||||
|
||||
Messages usually get displayed to the user by the toolbar.
|
||||
"""
|
||||
def __init__(self, name, sender, message):
|
||||
self.name = name
|
||||
self.sender = sender
|
||||
self.message = message
|
||||
|
||||
|
||||
class ToolManager:
|
||||
"""
|
||||
Manager for actions triggered by user interactions (key press, toolbar
|
||||
clicks, ...) on a Figure.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
figure : `.Figure`
|
||||
keypresslock : `~matplotlib.widgets.LockDraw`
|
||||
`.LockDraw` object to know if the `canvas` key_press_event is locked.
|
||||
messagelock : `~matplotlib.widgets.LockDraw`
|
||||
`.LockDraw` object to know if the message is available to write.
|
||||
"""
|
||||
|
||||
def __init__(self, figure=None):
|
||||
|
||||
self._key_press_handler_id = None
|
||||
|
||||
self._tools = {}
|
||||
self._keys = {}
|
||||
self._toggled = {}
|
||||
self._callbacks = cbook.CallbackRegistry()
|
||||
|
||||
# to process keypress event
|
||||
self.keypresslock = widgets.LockDraw()
|
||||
self.messagelock = widgets.LockDraw()
|
||||
|
||||
self._figure = None
|
||||
self.set_figure(figure)
|
||||
|
||||
@property
|
||||
def canvas(self):
|
||||
"""Canvas managed by FigureManager."""
|
||||
if not self._figure:
|
||||
return None
|
||||
return self._figure.canvas
|
||||
|
||||
@property
|
||||
def figure(self):
|
||||
"""Figure that holds the canvas."""
|
||||
return self._figure
|
||||
|
||||
@figure.setter
|
||||
def figure(self, figure):
|
||||
self.set_figure(figure)
|
||||
|
||||
def set_figure(self, figure, update_tools=True):
|
||||
"""
|
||||
Bind the given figure to the tools.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
figure : `.Figure`
|
||||
update_tools : bool, default: True
|
||||
Force tools to update figure.
|
||||
"""
|
||||
if self._key_press_handler_id:
|
||||
self.canvas.mpl_disconnect(self._key_press_handler_id)
|
||||
self._figure = figure
|
||||
if figure:
|
||||
self._key_press_handler_id = self.canvas.mpl_connect(
|
||||
'key_press_event', self._key_press)
|
||||
if update_tools:
|
||||
for tool in self._tools.values():
|
||||
tool.figure = figure
|
||||
|
||||
def toolmanager_connect(self, s, func):
|
||||
"""
|
||||
Connect event with string *s* to *func*.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
s : str
|
||||
The name of the event. The following events are recognized:
|
||||
|
||||
- 'tool_message_event'
|
||||
- 'tool_removed_event'
|
||||
- 'tool_added_event'
|
||||
|
||||
For every tool added a new event is created
|
||||
|
||||
- 'tool_trigger_TOOLNAME', where TOOLNAME is the id of the tool.
|
||||
|
||||
func : callable
|
||||
Callback function for the toolmanager event with signature::
|
||||
|
||||
def func(event: ToolEvent) -> Any
|
||||
|
||||
Returns
|
||||
-------
|
||||
cid
|
||||
The callback id for the connection. This can be used in
|
||||
`.toolmanager_disconnect`.
|
||||
"""
|
||||
return self._callbacks.connect(s, func)
|
||||
|
||||
def toolmanager_disconnect(self, cid):
|
||||
"""
|
||||
Disconnect callback id *cid*.
|
||||
|
||||
Example usage::
|
||||
|
||||
cid = toolmanager.toolmanager_connect('tool_trigger_zoom', onpress)
|
||||
#...later
|
||||
toolmanager.toolmanager_disconnect(cid)
|
||||
"""
|
||||
return self._callbacks.disconnect(cid)
|
||||
|
||||
def message_event(self, message, sender=None):
|
||||
"""Emit a `ToolManagerMessageEvent`."""
|
||||
if sender is None:
|
||||
sender = self
|
||||
|
||||
s = 'tool_message_event'
|
||||
event = ToolManagerMessageEvent(s, sender, message)
|
||||
self._callbacks.process(s, event)
|
||||
|
||||
@property
|
||||
def active_toggle(self):
|
||||
"""Currently toggled tools."""
|
||||
return self._toggled
|
||||
|
||||
def get_tool_keymap(self, name):
|
||||
"""
|
||||
Return the keymap associated with the specified tool.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
Name of the Tool.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of str
|
||||
List of keys associated with the tool.
|
||||
"""
|
||||
|
||||
keys = [k for k, i in self._keys.items() if i == name]
|
||||
return keys
|
||||
|
||||
def _remove_keys(self, name):
|
||||
for k in self.get_tool_keymap(name):
|
||||
del self._keys[k]
|
||||
|
||||
def update_keymap(self, name, key):
|
||||
"""
|
||||
Set the keymap to associate with the specified tool.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
Name of the Tool.
|
||||
key : str or list of str
|
||||
Keys to associate with the tool.
|
||||
"""
|
||||
if name not in self._tools:
|
||||
raise KeyError(f'{name!r} not in Tools')
|
||||
self._remove_keys(name)
|
||||
if isinstance(key, str):
|
||||
key = [key]
|
||||
for k in key:
|
||||
if k in self._keys:
|
||||
_api.warn_external(
|
||||
f'Key {k} changed from {self._keys[k]} to {name}')
|
||||
self._keys[k] = name
|
||||
|
||||
def remove_tool(self, name):
|
||||
"""
|
||||
Remove tool named *name*.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
Name of the tool.
|
||||
"""
|
||||
tool = self.get_tool(name)
|
||||
if getattr(tool, 'toggled', False): # If it's a toggled toggle tool, untoggle
|
||||
self.trigger_tool(tool, 'toolmanager')
|
||||
self._remove_keys(name)
|
||||
event = ToolEvent('tool_removed_event', self, tool)
|
||||
self._callbacks.process(event.name, event)
|
||||
del self._tools[name]
|
||||
|
||||
def add_tool(self, name, tool, *args, **kwargs):
|
||||
"""
|
||||
Add *tool* to `ToolManager`.
|
||||
|
||||
If successful, adds a new event ``tool_trigger_{name}`` where
|
||||
``{name}`` is the *name* of the tool; the event is fired every time the
|
||||
tool is triggered.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
Name of the tool, treated as the ID, has to be unique.
|
||||
tool : type
|
||||
Class of the tool to be added. A subclass will be used
|
||||
instead if one was registered for the current canvas class.
|
||||
*args, **kwargs
|
||||
Passed to the *tool*'s constructor.
|
||||
|
||||
See Also
|
||||
--------
|
||||
matplotlib.backend_tools.ToolBase : The base class for tools.
|
||||
"""
|
||||
|
||||
tool_cls = backend_tools._find_tool_class(type(self.canvas), tool)
|
||||
if not tool_cls:
|
||||
raise ValueError('Impossible to find class for %s' % str(tool))
|
||||
|
||||
if name in self._tools:
|
||||
_api.warn_external('A "Tool class" with the same name already '
|
||||
'exists, not added')
|
||||
return self._tools[name]
|
||||
|
||||
tool_obj = tool_cls(self, name, *args, **kwargs)
|
||||
self._tools[name] = tool_obj
|
||||
|
||||
if tool_obj.default_keymap is not None:
|
||||
self.update_keymap(name, tool_obj.default_keymap)
|
||||
|
||||
# For toggle tools init the radio_group in self._toggled
|
||||
if isinstance(tool_obj, backend_tools.ToolToggleBase):
|
||||
# None group is not mutually exclusive, a set is used to keep track
|
||||
# of all toggled tools in this group
|
||||
if tool_obj.radio_group is None:
|
||||
self._toggled.setdefault(None, set())
|
||||
else:
|
||||
self._toggled.setdefault(tool_obj.radio_group, None)
|
||||
|
||||
# If initially toggled
|
||||
if tool_obj.toggled:
|
||||
self._handle_toggle(tool_obj, None, None)
|
||||
tool_obj.set_figure(self.figure)
|
||||
|
||||
event = ToolEvent('tool_added_event', self, tool_obj)
|
||||
self._callbacks.process(event.name, event)
|
||||
|
||||
return tool_obj
|
||||
|
||||
def _handle_toggle(self, tool, canvasevent, data):
|
||||
"""
|
||||
Toggle tools, need to untoggle prior to using other Toggle tool.
|
||||
Called from trigger_tool.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tool : `.ToolBase`
|
||||
canvasevent : Event
|
||||
Original Canvas event or None.
|
||||
data : object
|
||||
Extra data to pass to the tool when triggering.
|
||||
"""
|
||||
|
||||
radio_group = tool.radio_group
|
||||
# radio_group None is not mutually exclusive
|
||||
# just keep track of toggled tools in this group
|
||||
if radio_group is None:
|
||||
if tool.name in self._toggled[None]:
|
||||
self._toggled[None].remove(tool.name)
|
||||
else:
|
||||
self._toggled[None].add(tool.name)
|
||||
return
|
||||
|
||||
# If the tool already has a toggled state, untoggle it
|
||||
if self._toggled[radio_group] == tool.name:
|
||||
toggled = None
|
||||
# If no tool was toggled in the radio_group
|
||||
# toggle it
|
||||
elif self._toggled[radio_group] is None:
|
||||
toggled = tool.name
|
||||
# Other tool in the radio_group is toggled
|
||||
else:
|
||||
# Untoggle previously toggled tool
|
||||
self.trigger_tool(self._toggled[radio_group],
|
||||
self,
|
||||
canvasevent,
|
||||
data)
|
||||
toggled = tool.name
|
||||
|
||||
# Keep track of the toggled tool in the radio_group
|
||||
self._toggled[radio_group] = toggled
|
||||
|
||||
def trigger_tool(self, name, sender=None, canvasevent=None, data=None):
|
||||
"""
|
||||
Trigger a tool and emit the ``tool_trigger_{name}`` event.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
Name of the tool.
|
||||
sender : object
|
||||
Object that wishes to trigger the tool.
|
||||
canvasevent : Event
|
||||
Original Canvas event or None.
|
||||
data : object
|
||||
Extra data to pass to the tool when triggering.
|
||||
"""
|
||||
tool = self.get_tool(name)
|
||||
if tool is None:
|
||||
return
|
||||
|
||||
if sender is None:
|
||||
sender = self
|
||||
|
||||
if isinstance(tool, backend_tools.ToolToggleBase):
|
||||
self._handle_toggle(tool, canvasevent, data)
|
||||
|
||||
tool.trigger(sender, canvasevent, data) # Actually trigger Tool.
|
||||
|
||||
s = 'tool_trigger_%s' % name
|
||||
event = ToolTriggerEvent(s, sender, tool, canvasevent, data)
|
||||
self._callbacks.process(s, event)
|
||||
|
||||
def _key_press(self, event):
|
||||
if event.key is None or self.keypresslock.locked():
|
||||
return
|
||||
|
||||
name = self._keys.get(event.key, None)
|
||||
if name is None:
|
||||
return
|
||||
self.trigger_tool(name, canvasevent=event)
|
||||
|
||||
@property
|
||||
def tools(self):
|
||||
"""A dict mapping tool name -> controlled tool."""
|
||||
return self._tools
|
||||
|
||||
def get_tool(self, name, warn=True):
|
||||
"""
|
||||
Return the tool object with the given name.
|
||||
|
||||
For convenience, this passes tool objects through.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str or `.ToolBase`
|
||||
Name of the tool, or the tool itself.
|
||||
warn : bool, default: True
|
||||
Whether a warning should be emitted it no tool with the given name
|
||||
exists.
|
||||
|
||||
Returns
|
||||
-------
|
||||
`.ToolBase` or None
|
||||
The tool or None if no tool with the given name exists.
|
||||
"""
|
||||
if (isinstance(name, backend_tools.ToolBase)
|
||||
and name.name in self._tools):
|
||||
return name
|
||||
if name not in self._tools:
|
||||
if warn:
|
||||
_api.warn_external(
|
||||
f"ToolManager does not control tool {name!r}")
|
||||
return None
|
||||
return self._tools[name]
|
||||
@ -0,0 +1,64 @@
|
||||
from matplotlib import backend_tools, widgets
|
||||
from matplotlib.backend_bases import FigureCanvasBase
|
||||
from matplotlib.figure import Figure
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, TypeVar
|
||||
|
||||
class ToolEvent:
|
||||
name: str
|
||||
sender: Any
|
||||
tool: backend_tools.ToolBase
|
||||
data: Any
|
||||
def __init__(self, name, sender, tool, data: Any | None = ...) -> None: ...
|
||||
|
||||
class ToolTriggerEvent(ToolEvent):
|
||||
canvasevent: ToolEvent
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
sender,
|
||||
tool,
|
||||
canvasevent: ToolEvent | None = ...,
|
||||
data: Any | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
class ToolManagerMessageEvent:
|
||||
name: str
|
||||
sender: Any
|
||||
message: str
|
||||
def __init__(self, name: str, sender: Any, message: str) -> None: ...
|
||||
|
||||
class ToolManager:
|
||||
keypresslock: widgets.LockDraw
|
||||
messagelock: widgets.LockDraw
|
||||
def __init__(self, figure: Figure | None = ...) -> None: ...
|
||||
@property
|
||||
def canvas(self) -> FigureCanvasBase | None: ...
|
||||
@property
|
||||
def figure(self) -> Figure | None: ...
|
||||
@figure.setter
|
||||
def figure(self, figure: Figure) -> None: ...
|
||||
def set_figure(self, figure: Figure, update_tools: bool = ...) -> None: ...
|
||||
def toolmanager_connect(self, s: str, func: Callable[[ToolEvent], Any]) -> int: ...
|
||||
def toolmanager_disconnect(self, cid: int) -> None: ...
|
||||
def message_event(self, message: str, sender: Any | None = ...) -> None: ...
|
||||
@property
|
||||
def active_toggle(self) -> dict[str | None, list[str] | str]: ...
|
||||
def get_tool_keymap(self, name: str) -> list[str]: ...
|
||||
def update_keymap(self, name: str, key: str | Iterable[str]) -> None: ...
|
||||
def remove_tool(self, name: str) -> None: ...
|
||||
_T = TypeVar("_T", bound=backend_tools.ToolBase)
|
||||
def add_tool(self, name: str, tool: type[_T], *args, **kwargs) -> _T: ...
|
||||
def trigger_tool(
|
||||
self,
|
||||
name: str | backend_tools.ToolBase,
|
||||
sender: Any | None = ...,
|
||||
canvasevent: ToolEvent | None = ...,
|
||||
data: Any | None = ...,
|
||||
) -> None: ...
|
||||
@property
|
||||
def tools(self) -> dict[str, backend_tools.ToolBase]: ...
|
||||
def get_tool(
|
||||
self, name: str | backend_tools.ToolBase, warn: bool = ...
|
||||
) -> backend_tools.ToolBase | None: ...
|
||||
1007
venv/lib/python3.12/site-packages/matplotlib/backend_tools.py
Normal file
1007
venv/lib/python3.12/site-packages/matplotlib/backend_tools.py
Normal file
File diff suppressed because it is too large
Load Diff
121
venv/lib/python3.12/site-packages/matplotlib/backend_tools.pyi
Normal file
121
venv/lib/python3.12/site-packages/matplotlib/backend_tools.pyi
Normal file
@ -0,0 +1,121 @@
|
||||
import enum
|
||||
from matplotlib import cbook
|
||||
from matplotlib.axes import Axes
|
||||
from matplotlib.backend_bases import ToolContainerBase, FigureCanvasBase
|
||||
from matplotlib.backend_managers import ToolManager, ToolEvent
|
||||
from matplotlib.figure import Figure
|
||||
from matplotlib.scale import ScaleBase
|
||||
|
||||
from typing import Any
|
||||
|
||||
class Cursors(enum.IntEnum):
|
||||
POINTER: int
|
||||
HAND: int
|
||||
SELECT_REGION: int
|
||||
MOVE: int
|
||||
WAIT: int
|
||||
RESIZE_HORIZONTAL: int
|
||||
RESIZE_VERTICAL: int
|
||||
|
||||
cursors = Cursors
|
||||
|
||||
class ToolBase:
|
||||
@property
|
||||
def default_keymap(self) -> list[str] | None: ...
|
||||
description: str | None
|
||||
image: str | None
|
||||
def __init__(self, toolmanager: ToolManager, name: str) -> None: ...
|
||||
@property
|
||||
def name(self) -> str: ...
|
||||
@property
|
||||
def toolmanager(self) -> ToolManager: ...
|
||||
@property
|
||||
def canvas(self) -> FigureCanvasBase | None: ...
|
||||
@property
|
||||
def figure(self) -> Figure | None: ...
|
||||
@figure.setter
|
||||
def figure(self, figure: Figure | None) -> None: ...
|
||||
def set_figure(self, figure: Figure | None) -> None: ...
|
||||
def trigger(self, sender: Any, event: ToolEvent, data: Any = ...) -> None: ...
|
||||
|
||||
class ToolToggleBase(ToolBase):
|
||||
radio_group: str | None
|
||||
cursor: Cursors | None
|
||||
default_toggled: bool
|
||||
def __init__(self, *args, **kwargs) -> None: ...
|
||||
def enable(self, event: ToolEvent | None = ...) -> None: ...
|
||||
def disable(self, event: ToolEvent | None = ...) -> None: ...
|
||||
@property
|
||||
def toggled(self) -> bool: ...
|
||||
def set_figure(self, figure: Figure | None) -> None: ...
|
||||
|
||||
class ToolSetCursor(ToolBase): ...
|
||||
|
||||
class ToolCursorPosition(ToolBase):
|
||||
def send_message(self, event: ToolEvent) -> None: ...
|
||||
|
||||
class RubberbandBase(ToolBase):
|
||||
def draw_rubberband(self, *data) -> None: ...
|
||||
def remove_rubberband(self) -> None: ...
|
||||
|
||||
class ToolQuit(ToolBase): ...
|
||||
class ToolQuitAll(ToolBase): ...
|
||||
class ToolGrid(ToolBase): ...
|
||||
class ToolMinorGrid(ToolBase): ...
|
||||
class ToolFullScreen(ToolBase): ...
|
||||
|
||||
class AxisScaleBase(ToolToggleBase):
|
||||
def enable(self, event: ToolEvent | None = ...) -> None: ...
|
||||
def disable(self, event: ToolEvent | None = ...) -> None: ...
|
||||
|
||||
class ToolYScale(AxisScaleBase):
|
||||
def set_scale(self, ax: Axes, scale: str | ScaleBase) -> None: ...
|
||||
|
||||
class ToolXScale(AxisScaleBase):
|
||||
def set_scale(self, ax, scale: str | ScaleBase) -> None: ...
|
||||
|
||||
class ToolViewsPositions(ToolBase):
|
||||
views: dict[Figure | Axes, cbook.Stack]
|
||||
positions: dict[Figure | Axes, cbook.Stack]
|
||||
home_views: dict[Figure, dict[Axes, tuple[float, float, float, float]]]
|
||||
def add_figure(self, figure: Figure) -> None: ...
|
||||
def clear(self, figure: Figure) -> None: ...
|
||||
def update_view(self) -> None: ...
|
||||
def push_current(self, figure: Figure | None = ...) -> None: ...
|
||||
def update_home_views(self, figure: Figure | None = ...) -> None: ...
|
||||
def home(self) -> None: ...
|
||||
def back(self) -> None: ...
|
||||
def forward(self) -> None: ...
|
||||
|
||||
class ViewsPositionsBase(ToolBase): ...
|
||||
class ToolHome(ViewsPositionsBase): ...
|
||||
class ToolBack(ViewsPositionsBase): ...
|
||||
class ToolForward(ViewsPositionsBase): ...
|
||||
class ConfigureSubplotsBase(ToolBase): ...
|
||||
class SaveFigureBase(ToolBase): ...
|
||||
|
||||
class ZoomPanBase(ToolToggleBase):
|
||||
base_scale: float
|
||||
scrollthresh: float
|
||||
lastscroll: float
|
||||
def __init__(self, *args) -> None: ...
|
||||
def enable(self, event: ToolEvent | None = ...) -> None: ...
|
||||
def disable(self, event: ToolEvent | None = ...) -> None: ...
|
||||
def scroll_zoom(self, event: ToolEvent) -> None: ...
|
||||
|
||||
class ToolZoom(ZoomPanBase): ...
|
||||
class ToolPan(ZoomPanBase): ...
|
||||
|
||||
class ToolHelpBase(ToolBase):
|
||||
@staticmethod
|
||||
def format_shortcut(key_sequence: str) -> str: ...
|
||||
|
||||
class ToolCopyToClipboardBase(ToolBase): ...
|
||||
|
||||
default_tools: dict[str, ToolBase]
|
||||
default_toolbar_tools: list[list[str | list[str]]]
|
||||
|
||||
def add_tools_to_manager(
|
||||
toolmanager: ToolManager, tools: dict[str, type[ToolBase]] = ...
|
||||
) -> None: ...
|
||||
def add_tools_to_container(container: ToolContainerBase, tools: list[Any] = ...) -> None: ...
|
||||
@ -0,0 +1,5 @@
|
||||
from .registry import BackendFilter, backend_registry # noqa: F401
|
||||
|
||||
# NOTE: plt.switch_backend() (called at import time) will add a "backend"
|
||||
# attribute here for backcompat.
|
||||
_QT_FORCE_QT5_BINDING = False
|
||||
Binary file not shown.
@ -0,0 +1,332 @@
|
||||
"""
|
||||
Common code for GTK3 and GTK4 backends.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import matplotlib as mpl
|
||||
from matplotlib import _api, backend_tools, cbook
|
||||
from matplotlib._pylab_helpers import Gcf
|
||||
from matplotlib.backend_bases import (
|
||||
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
|
||||
TimerBase)
|
||||
from matplotlib.backend_tools import Cursors
|
||||
|
||||
import gi
|
||||
# The GTK3/GTK4 backends will have already called `gi.require_version` to set
|
||||
# the desired GTK.
|
||||
from gi.repository import Gdk, Gio, GLib, Gtk
|
||||
|
||||
|
||||
try:
|
||||
gi.require_foreign("cairo")
|
||||
except ImportError as e:
|
||||
raise ImportError("Gtk-based backends require cairo") from e
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
_application = None # Placeholder
|
||||
|
||||
|
||||
def _shutdown_application(app):
|
||||
# The application might prematurely shut down if Ctrl-C'd out of IPython,
|
||||
# so close all windows.
|
||||
for win in app.get_windows():
|
||||
win.close()
|
||||
# The PyGObject wrapper incorrectly thinks that None is not allowed, or we
|
||||
# would call this:
|
||||
# Gio.Application.set_default(None)
|
||||
# Instead, we set this property and ignore default applications with it:
|
||||
app._created_by_matplotlib = True
|
||||
global _application
|
||||
_application = None
|
||||
|
||||
|
||||
def _create_application():
|
||||
global _application
|
||||
|
||||
if _application is None:
|
||||
app = Gio.Application.get_default()
|
||||
if app is None or getattr(app, '_created_by_matplotlib', False):
|
||||
# display_is_valid returns False only if on Linux and neither X11
|
||||
# nor Wayland display can be opened.
|
||||
if not mpl._c_internal_utils.display_is_valid():
|
||||
raise RuntimeError('Invalid DISPLAY variable')
|
||||
_application = Gtk.Application.new('org.matplotlib.Matplotlib3',
|
||||
Gio.ApplicationFlags.NON_UNIQUE)
|
||||
# The activate signal must be connected, but we don't care for
|
||||
# handling it, since we don't do any remote processing.
|
||||
_application.connect('activate', lambda *args, **kwargs: None)
|
||||
_application.connect('shutdown', _shutdown_application)
|
||||
_application.register()
|
||||
cbook._setup_new_guiapp()
|
||||
else:
|
||||
_application = app
|
||||
|
||||
return _application
|
||||
|
||||
|
||||
def mpl_to_gtk_cursor_name(mpl_cursor):
|
||||
return _api.check_getitem({
|
||||
Cursors.MOVE: "move",
|
||||
Cursors.HAND: "pointer",
|
||||
Cursors.POINTER: "default",
|
||||
Cursors.SELECT_REGION: "crosshair",
|
||||
Cursors.WAIT: "wait",
|
||||
Cursors.RESIZE_HORIZONTAL: "ew-resize",
|
||||
Cursors.RESIZE_VERTICAL: "ns-resize",
|
||||
}, cursor=mpl_cursor)
|
||||
|
||||
|
||||
class TimerGTK(TimerBase):
|
||||
"""Subclass of `.TimerBase` using GTK timer events."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._timer = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _timer_start(self):
|
||||
# Need to stop it, otherwise we potentially leak a timer id that will
|
||||
# never be stopped.
|
||||
self._timer_stop()
|
||||
self._timer = GLib.timeout_add(self._interval, self._on_timer)
|
||||
|
||||
def _timer_stop(self):
|
||||
if self._timer is not None:
|
||||
GLib.source_remove(self._timer)
|
||||
self._timer = None
|
||||
|
||||
def _timer_set_interval(self):
|
||||
# Only stop and restart it if the timer has already been started.
|
||||
if self._timer is not None:
|
||||
self._timer_stop()
|
||||
self._timer_start()
|
||||
|
||||
def _on_timer(self):
|
||||
super()._on_timer()
|
||||
|
||||
# Gtk timeout_add() requires that the callback returns True if it
|
||||
# is to be called again.
|
||||
if self.callbacks and not self._single:
|
||||
return True
|
||||
else:
|
||||
self._timer = None
|
||||
return False
|
||||
|
||||
|
||||
class _FigureCanvasGTK(FigureCanvasBase):
|
||||
_timer_cls = TimerGTK
|
||||
|
||||
|
||||
class _FigureManagerGTK(FigureManagerBase):
|
||||
"""
|
||||
Attributes
|
||||
----------
|
||||
canvas : `FigureCanvas`
|
||||
The FigureCanvas instance
|
||||
num : int or str
|
||||
The Figure number
|
||||
toolbar : Gtk.Toolbar or Gtk.Box
|
||||
The toolbar
|
||||
vbox : Gtk.VBox
|
||||
The Gtk.VBox containing the canvas and toolbar
|
||||
window : Gtk.Window
|
||||
The Gtk.Window
|
||||
"""
|
||||
|
||||
def __init__(self, canvas, num):
|
||||
self._gtk_ver = gtk_ver = Gtk.get_major_version()
|
||||
|
||||
app = _create_application()
|
||||
self.window = Gtk.Window()
|
||||
app.add_window(self.window)
|
||||
super().__init__(canvas, num)
|
||||
|
||||
if gtk_ver == 3:
|
||||
self.window.set_wmclass("matplotlib", "Matplotlib")
|
||||
icon_ext = "png" if sys.platform == "win32" else "svg"
|
||||
self.window.set_icon_from_file(
|
||||
str(cbook._get_data_path(f"images/matplotlib.{icon_ext}")))
|
||||
|
||||
self.vbox = Gtk.Box()
|
||||
self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
|
||||
|
||||
if gtk_ver == 3:
|
||||
self.window.add(self.vbox)
|
||||
self.vbox.show()
|
||||
self.canvas.show()
|
||||
self.vbox.pack_start(self.canvas, True, True, 0)
|
||||
elif gtk_ver == 4:
|
||||
self.window.set_child(self.vbox)
|
||||
self.vbox.prepend(self.canvas)
|
||||
|
||||
# calculate size for window
|
||||
w, h = self.canvas.get_width_height()
|
||||
|
||||
if self.toolbar is not None:
|
||||
if gtk_ver == 3:
|
||||
self.toolbar.show()
|
||||
self.vbox.pack_end(self.toolbar, False, False, 0)
|
||||
elif gtk_ver == 4:
|
||||
sw = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER)
|
||||
sw.set_child(self.toolbar)
|
||||
self.vbox.append(sw)
|
||||
min_size, nat_size = self.toolbar.get_preferred_size()
|
||||
h += nat_size.height
|
||||
|
||||
self.window.set_default_size(w, h)
|
||||
|
||||
self._destroying = False
|
||||
self.window.connect("destroy", lambda *args: Gcf.destroy(self))
|
||||
self.window.connect({3: "delete_event", 4: "close-request"}[gtk_ver],
|
||||
lambda *args: Gcf.destroy(self))
|
||||
if mpl.is_interactive():
|
||||
self.window.show()
|
||||
self.canvas.draw_idle()
|
||||
|
||||
self.canvas.grab_focus()
|
||||
|
||||
def destroy(self, *args):
|
||||
if self._destroying:
|
||||
# Otherwise, this can be called twice when the user presses 'q',
|
||||
# which calls Gcf.destroy(self), then this destroy(), then triggers
|
||||
# Gcf.destroy(self) once again via
|
||||
# `connect("destroy", lambda *args: Gcf.destroy(self))`.
|
||||
return
|
||||
self._destroying = True
|
||||
self.window.destroy()
|
||||
self.canvas.destroy()
|
||||
|
||||
@classmethod
|
||||
def start_main_loop(cls):
|
||||
global _application
|
||||
if _application is None:
|
||||
return
|
||||
|
||||
try:
|
||||
_application.run() # Quits when all added windows close.
|
||||
except KeyboardInterrupt:
|
||||
# Ensure all windows can process their close event from
|
||||
# _shutdown_application.
|
||||
context = GLib.MainContext.default()
|
||||
while context.pending():
|
||||
context.iteration(True)
|
||||
raise
|
||||
finally:
|
||||
# Running after quit is undefined, so create a new one next time.
|
||||
_application = None
|
||||
|
||||
def show(self):
|
||||
# show the figure window
|
||||
self.window.show()
|
||||
self.canvas.draw()
|
||||
if mpl.rcParams["figure.raise_window"]:
|
||||
meth_name = {3: "get_window", 4: "get_surface"}[self._gtk_ver]
|
||||
if getattr(self.window, meth_name)():
|
||||
self.window.present()
|
||||
else:
|
||||
# If this is called by a callback early during init,
|
||||
# self.window (a GtkWindow) may not have an associated
|
||||
# low-level GdkWindow (on GTK3) or GdkSurface (on GTK4) yet,
|
||||
# and present() would crash.
|
||||
_api.warn_external("Cannot raise window yet to be setup")
|
||||
|
||||
def full_screen_toggle(self):
|
||||
is_fullscreen = {
|
||||
3: lambda w: (w.get_window().get_state()
|
||||
& Gdk.WindowState.FULLSCREEN),
|
||||
4: lambda w: w.is_fullscreen(),
|
||||
}[self._gtk_ver]
|
||||
if is_fullscreen(self.window):
|
||||
self.window.unfullscreen()
|
||||
else:
|
||||
self.window.fullscreen()
|
||||
|
||||
def get_window_title(self):
|
||||
return self.window.get_title()
|
||||
|
||||
def set_window_title(self, title):
|
||||
self.window.set_title(title)
|
||||
|
||||
def resize(self, width, height):
|
||||
width = int(width / self.canvas.device_pixel_ratio)
|
||||
height = int(height / self.canvas.device_pixel_ratio)
|
||||
if self.toolbar:
|
||||
min_size, nat_size = self.toolbar.get_preferred_size()
|
||||
height += nat_size.height
|
||||
canvas_size = self.canvas.get_allocation()
|
||||
if self._gtk_ver >= 4 or canvas_size.width == canvas_size.height == 1:
|
||||
# A canvas size of (1, 1) cannot exist in most cases, because
|
||||
# window decorations would prevent such a small window. This call
|
||||
# must be before the window has been mapped and widgets have been
|
||||
# sized, so just change the window's starting size.
|
||||
self.window.set_default_size(width, height)
|
||||
else:
|
||||
self.window.resize(width, height)
|
||||
|
||||
|
||||
class _NavigationToolbar2GTK(NavigationToolbar2):
|
||||
# Must be implemented in GTK3/GTK4 backends:
|
||||
# * __init__
|
||||
# * save_figure
|
||||
|
||||
def set_message(self, s):
|
||||
escaped = GLib.markup_escape_text(s)
|
||||
self.message.set_markup(f'<small>{escaped}</small>')
|
||||
|
||||
def draw_rubberband(self, event, x0, y0, x1, y1):
|
||||
height = self.canvas.figure.bbox.height
|
||||
y1 = height - y1
|
||||
y0 = height - y0
|
||||
rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)]
|
||||
self.canvas._draw_rubberband(rect)
|
||||
|
||||
def remove_rubberband(self):
|
||||
self.canvas._draw_rubberband(None)
|
||||
|
||||
def _update_buttons_checked(self):
|
||||
for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]:
|
||||
button = self._gtk_ids.get(name)
|
||||
if button:
|
||||
with button.handler_block(button._signal_handler):
|
||||
button.set_active(self.mode.name == active)
|
||||
|
||||
def pan(self, *args):
|
||||
super().pan(*args)
|
||||
self._update_buttons_checked()
|
||||
|
||||
def zoom(self, *args):
|
||||
super().zoom(*args)
|
||||
self._update_buttons_checked()
|
||||
|
||||
def set_history_buttons(self):
|
||||
can_backward = self._nav_stack._pos > 0
|
||||
can_forward = self._nav_stack._pos < len(self._nav_stack) - 1
|
||||
if 'Back' in self._gtk_ids:
|
||||
self._gtk_ids['Back'].set_sensitive(can_backward)
|
||||
if 'Forward' in self._gtk_ids:
|
||||
self._gtk_ids['Forward'].set_sensitive(can_forward)
|
||||
|
||||
|
||||
class RubberbandGTK(backend_tools.RubberbandBase):
|
||||
def draw_rubberband(self, x0, y0, x1, y1):
|
||||
_NavigationToolbar2GTK.draw_rubberband(
|
||||
self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1)
|
||||
|
||||
def remove_rubberband(self):
|
||||
_NavigationToolbar2GTK.remove_rubberband(
|
||||
self._make_classic_style_pseudo_toolbar())
|
||||
|
||||
|
||||
class ConfigureSubplotsGTK(backend_tools.ConfigureSubplotsBase):
|
||||
def trigger(self, *args):
|
||||
_NavigationToolbar2GTK.configure_subplots(self, None)
|
||||
|
||||
|
||||
class _BackendGTK(_Backend):
|
||||
backend_version = "{}.{}.{}".format(
|
||||
Gtk.get_major_version(),
|
||||
Gtk.get_minor_version(),
|
||||
Gtk.get_micro_version(),
|
||||
)
|
||||
mainloop = _FigureManagerGTK.start_main_loop
|
||||
@ -0,0 +1,145 @@
|
||||
"""
|
||||
Common functionality between the PDF and PS backends.
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
import functools
|
||||
|
||||
from fontTools import subset
|
||||
|
||||
import matplotlib as mpl
|
||||
from .. import font_manager, ft2font
|
||||
from .._afm import AFM
|
||||
from ..backend_bases import RendererBase
|
||||
|
||||
|
||||
@functools.lru_cache(50)
|
||||
def _cached_get_afm_from_fname(fname):
|
||||
with open(fname, "rb") as fh:
|
||||
return AFM(fh)
|
||||
|
||||
|
||||
def get_glyphs_subset(fontfile, characters):
|
||||
"""
|
||||
Subset a TTF font
|
||||
|
||||
Reads the named fontfile and restricts the font to the characters.
|
||||
Returns a serialization of the subset font as file-like object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fontfile : str
|
||||
Path to the font file
|
||||
characters : str
|
||||
Continuous set of characters to include in subset
|
||||
"""
|
||||
|
||||
options = subset.Options(glyph_names=True, recommended_glyphs=True)
|
||||
|
||||
# Prevent subsetting extra tables.
|
||||
options.drop_tables += [
|
||||
'FFTM', # FontForge Timestamp.
|
||||
'PfEd', # FontForge personal table.
|
||||
'BDF', # X11 BDF header.
|
||||
'meta', # Metadata stores design/supported languages (meaningless for subsets).
|
||||
]
|
||||
|
||||
# if fontfile is a ttc, specify font number
|
||||
if fontfile.endswith(".ttc"):
|
||||
options.font_number = 0
|
||||
|
||||
with subset.load_font(fontfile, options) as font:
|
||||
subsetter = subset.Subsetter(options=options)
|
||||
subsetter.populate(text=characters)
|
||||
subsetter.subset(font)
|
||||
fh = BytesIO()
|
||||
font.save(fh, reorderTables=False)
|
||||
return fh
|
||||
|
||||
|
||||
class CharacterTracker:
|
||||
"""
|
||||
Helper for font subsetting by the pdf and ps backends.
|
||||
|
||||
Maintains a mapping of font paths to the set of character codepoints that
|
||||
are being used from that font.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.used = {}
|
||||
|
||||
def track(self, font, s):
|
||||
"""Record that string *s* is being typeset using font *font*."""
|
||||
char_to_font = font._get_fontmap(s)
|
||||
for _c, _f in char_to_font.items():
|
||||
self.used.setdefault(_f.fname, set()).add(ord(_c))
|
||||
|
||||
def track_glyph(self, font, glyph):
|
||||
"""Record that codepoint *glyph* is being typeset using font *font*."""
|
||||
self.used.setdefault(font.fname, set()).add(glyph)
|
||||
|
||||
|
||||
class RendererPDFPSBase(RendererBase):
|
||||
# The following attributes must be defined by the subclasses:
|
||||
# - _afm_font_dir
|
||||
# - _use_afm_rc_name
|
||||
|
||||
def __init__(self, width, height):
|
||||
super().__init__()
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def flipy(self):
|
||||
# docstring inherited
|
||||
return False # y increases from bottom to top.
|
||||
|
||||
def option_scale_image(self):
|
||||
# docstring inherited
|
||||
return True # PDF and PS support arbitrary image scaling.
|
||||
|
||||
def option_image_nocomposite(self):
|
||||
# docstring inherited
|
||||
# Decide whether to composite image based on rcParam value.
|
||||
return not mpl.rcParams["image.composite_image"]
|
||||
|
||||
def get_canvas_width_height(self):
|
||||
# docstring inherited
|
||||
return self.width * 72.0, self.height * 72.0
|
||||
|
||||
def get_text_width_height_descent(self, s, prop, ismath):
|
||||
# docstring inherited
|
||||
if ismath == "TeX":
|
||||
return super().get_text_width_height_descent(s, prop, ismath)
|
||||
elif ismath:
|
||||
parse = self._text2path.mathtext_parser.parse(s, 72, prop)
|
||||
return parse.width, parse.height, parse.depth
|
||||
elif mpl.rcParams[self._use_afm_rc_name]:
|
||||
font = self._get_font_afm(prop)
|
||||
l, b, w, h, d = font.get_str_bbox_and_descent(s)
|
||||
scale = prop.get_size_in_points() / 1000
|
||||
w *= scale
|
||||
h *= scale
|
||||
d *= scale
|
||||
return w, h, d
|
||||
else:
|
||||
font = self._get_font_ttf(prop)
|
||||
font.set_text(s, 0.0, flags=ft2font.LOAD_NO_HINTING)
|
||||
w, h = font.get_width_height()
|
||||
d = font.get_descent()
|
||||
scale = 1 / 64
|
||||
w *= scale
|
||||
h *= scale
|
||||
d *= scale
|
||||
return w, h, d
|
||||
|
||||
def _get_font_afm(self, prop):
|
||||
fname = font_manager.findfont(
|
||||
prop, fontext="afm", directory=self._afm_font_dir)
|
||||
return _cached_get_afm_from_fname(fname)
|
||||
|
||||
def _get_font_ttf(self, prop):
|
||||
fnames = font_manager.fontManager._find_fonts_by_props(prop)
|
||||
font = font_manager.get_font(fnames)
|
||||
font.clear()
|
||||
font.set_size(prop.get_size_in_points(), 72)
|
||||
return font
|
||||
1052
venv/lib/python3.12/site-packages/matplotlib/backends/_backend_tk.py
Normal file
1052
venv/lib/python3.12/site-packages/matplotlib/backends/_backend_tk.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -0,0 +1,15 @@
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
TK_PHOTO_COMPOSITE_OVERLAY: int
|
||||
TK_PHOTO_COMPOSITE_SET: int
|
||||
|
||||
def blit(
|
||||
interp: int,
|
||||
photo_name: str,
|
||||
data: NDArray[np.uint8],
|
||||
comp_rule: int,
|
||||
offset: tuple[int, int, int, int],
|
||||
bbox: tuple[int, int, int, int],
|
||||
) -> None: ...
|
||||
def enable_dpi_awareness(frame_handle: int, interp: int) -> bool | None: ...
|
||||
@ -0,0 +1,543 @@
|
||||
"""
|
||||
An `Anti-Grain Geometry`_ (AGG) backend.
|
||||
|
||||
Features that are implemented:
|
||||
|
||||
* capstyles and join styles
|
||||
* dashes
|
||||
* linewidth
|
||||
* lines, rectangles, ellipses
|
||||
* clipping to a rectangle
|
||||
* output to RGBA and Pillow-supported image formats
|
||||
* alpha blending
|
||||
* DPI scaling properly - everything scales properly (dashes, linewidths, etc)
|
||||
* draw polygon
|
||||
* freetype2 w/ ft2font
|
||||
|
||||
Still TODO:
|
||||
|
||||
* integrate screen dpi w/ ppi and text
|
||||
|
||||
.. _Anti-Grain Geometry: http://agg.sourceforge.net/antigrain.com
|
||||
"""
|
||||
|
||||
from contextlib import nullcontext
|
||||
from math import radians, cos, sin
|
||||
|
||||
import numpy as np
|
||||
|
||||
import matplotlib as mpl
|
||||
from matplotlib import _api, cbook
|
||||
from matplotlib.backend_bases import (
|
||||
_Backend, FigureCanvasBase, FigureManagerBase, RendererBase)
|
||||
from matplotlib.font_manager import fontManager as _fontManager, get_font
|
||||
from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING,
|
||||
LOAD_DEFAULT, LOAD_NO_AUTOHINT)
|
||||
from matplotlib.mathtext import MathTextParser
|
||||
from matplotlib.path import Path
|
||||
from matplotlib.transforms import Bbox, BboxBase
|
||||
from matplotlib.backends._backend_agg import RendererAgg as _RendererAgg
|
||||
|
||||
|
||||
def get_hinting_flag():
|
||||
mapping = {
|
||||
'default': LOAD_DEFAULT,
|
||||
'no_autohint': LOAD_NO_AUTOHINT,
|
||||
'force_autohint': LOAD_FORCE_AUTOHINT,
|
||||
'no_hinting': LOAD_NO_HINTING,
|
||||
True: LOAD_FORCE_AUTOHINT,
|
||||
False: LOAD_NO_HINTING,
|
||||
'either': LOAD_DEFAULT,
|
||||
'native': LOAD_NO_AUTOHINT,
|
||||
'auto': LOAD_FORCE_AUTOHINT,
|
||||
'none': LOAD_NO_HINTING,
|
||||
}
|
||||
return mapping[mpl.rcParams['text.hinting']]
|
||||
|
||||
|
||||
class RendererAgg(RendererBase):
|
||||
"""
|
||||
The renderer handles all the drawing primitives using a graphics
|
||||
context instance that controls the colors/styles
|
||||
"""
|
||||
|
||||
def __init__(self, width, height, dpi):
|
||||
super().__init__()
|
||||
|
||||
self.dpi = dpi
|
||||
self.width = width
|
||||
self.height = height
|
||||
self._renderer = _RendererAgg(int(width), int(height), dpi)
|
||||
self._filter_renderers = []
|
||||
|
||||
self._update_methods()
|
||||
self.mathtext_parser = MathTextParser('agg')
|
||||
|
||||
self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
|
||||
|
||||
def __getstate__(self):
|
||||
# We only want to preserve the init keywords of the Renderer.
|
||||
# Anything else can be re-created.
|
||||
return {'width': self.width, 'height': self.height, 'dpi': self.dpi}
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__init__(state['width'], state['height'], state['dpi'])
|
||||
|
||||
def _update_methods(self):
|
||||
self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles
|
||||
self.draw_image = self._renderer.draw_image
|
||||
self.draw_markers = self._renderer.draw_markers
|
||||
self.draw_path_collection = self._renderer.draw_path_collection
|
||||
self.draw_quad_mesh = self._renderer.draw_quad_mesh
|
||||
self.copy_from_bbox = self._renderer.copy_from_bbox
|
||||
|
||||
def draw_path(self, gc, path, transform, rgbFace=None):
|
||||
# docstring inherited
|
||||
nmax = mpl.rcParams['agg.path.chunksize'] # here at least for testing
|
||||
npts = path.vertices.shape[0]
|
||||
|
||||
if (npts > nmax > 100 and path.should_simplify and
|
||||
rgbFace is None and gc.get_hatch() is None):
|
||||
nch = np.ceil(npts / nmax)
|
||||
chsize = int(np.ceil(npts / nch))
|
||||
i0 = np.arange(0, npts, chsize)
|
||||
i1 = np.zeros_like(i0)
|
||||
i1[:-1] = i0[1:] - 1
|
||||
i1[-1] = npts
|
||||
for ii0, ii1 in zip(i0, i1):
|
||||
v = path.vertices[ii0:ii1, :]
|
||||
c = path.codes
|
||||
if c is not None:
|
||||
c = c[ii0:ii1]
|
||||
c[0] = Path.MOVETO # move to end of last chunk
|
||||
p = Path(v, c)
|
||||
p.simplify_threshold = path.simplify_threshold
|
||||
try:
|
||||
self._renderer.draw_path(gc, p, transform, rgbFace)
|
||||
except OverflowError:
|
||||
msg = (
|
||||
"Exceeded cell block limit in Agg.\n\n"
|
||||
"Please reduce the value of "
|
||||
f"rcParams['agg.path.chunksize'] (currently {nmax}) "
|
||||
"or increase the path simplification threshold"
|
||||
"(rcParams['path.simplify_threshold'] = "
|
||||
f"{mpl.rcParams['path.simplify_threshold']:.2f} by "
|
||||
"default and path.simplify_threshold = "
|
||||
f"{path.simplify_threshold:.2f} on the input)."
|
||||
)
|
||||
raise OverflowError(msg) from None
|
||||
else:
|
||||
try:
|
||||
self._renderer.draw_path(gc, path, transform, rgbFace)
|
||||
except OverflowError:
|
||||
cant_chunk = ''
|
||||
if rgbFace is not None:
|
||||
cant_chunk += "- cannot split filled path\n"
|
||||
if gc.get_hatch() is not None:
|
||||
cant_chunk += "- cannot split hatched path\n"
|
||||
if not path.should_simplify:
|
||||
cant_chunk += "- path.should_simplify is False\n"
|
||||
if len(cant_chunk):
|
||||
msg = (
|
||||
"Exceeded cell block limit in Agg, however for the "
|
||||
"following reasons:\n\n"
|
||||
f"{cant_chunk}\n"
|
||||
"we cannot automatically split up this path to draw."
|
||||
"\n\nPlease manually simplify your path."
|
||||
)
|
||||
|
||||
else:
|
||||
inc_threshold = (
|
||||
"or increase the path simplification threshold"
|
||||
"(rcParams['path.simplify_threshold'] = "
|
||||
f"{mpl.rcParams['path.simplify_threshold']} "
|
||||
"by default and path.simplify_threshold "
|
||||
f"= {path.simplify_threshold} "
|
||||
"on the input)."
|
||||
)
|
||||
if nmax > 100:
|
||||
msg = (
|
||||
"Exceeded cell block limit in Agg. Please reduce "
|
||||
"the value of rcParams['agg.path.chunksize'] "
|
||||
f"(currently {nmax}) {inc_threshold}"
|
||||
)
|
||||
else:
|
||||
msg = (
|
||||
"Exceeded cell block limit in Agg. Please set "
|
||||
"the value of rcParams['agg.path.chunksize'], "
|
||||
f"(currently {nmax}) to be greater than 100 "
|
||||
+ inc_threshold
|
||||
)
|
||||
|
||||
raise OverflowError(msg) from None
|
||||
|
||||
def draw_mathtext(self, gc, x, y, s, prop, angle):
|
||||
"""Draw mathtext using :mod:`matplotlib.mathtext`."""
|
||||
ox, oy, width, height, descent, font_image = \
|
||||
self.mathtext_parser.parse(s, self.dpi, prop,
|
||||
antialiased=gc.get_antialiased())
|
||||
|
||||
xd = descent * sin(radians(angle))
|
||||
yd = descent * cos(radians(angle))
|
||||
x = round(x + ox + xd)
|
||||
y = round(y - oy + yd)
|
||||
self._renderer.draw_text_image(font_image, x, y + 1, angle, gc)
|
||||
|
||||
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
|
||||
# docstring inherited
|
||||
if ismath:
|
||||
return self.draw_mathtext(gc, x, y, s, prop, angle)
|
||||
font = self._prepare_font(prop)
|
||||
# We pass '0' for angle here, since it will be rotated (in raster
|
||||
# space) in the following call to draw_text_image).
|
||||
font.set_text(s, 0, flags=get_hinting_flag())
|
||||
font.draw_glyphs_to_bitmap(
|
||||
antialiased=gc.get_antialiased())
|
||||
d = font.get_descent() / 64.0
|
||||
# The descent needs to be adjusted for the angle.
|
||||
xo, yo = font.get_bitmap_offset()
|
||||
xo /= 64.0
|
||||
yo /= 64.0
|
||||
xd = d * sin(radians(angle))
|
||||
yd = d * cos(radians(angle))
|
||||
x = round(x + xo + xd)
|
||||
y = round(y + yo + yd)
|
||||
self._renderer.draw_text_image(font, x, y + 1, angle, gc)
|
||||
|
||||
def get_text_width_height_descent(self, s, prop, ismath):
|
||||
# docstring inherited
|
||||
|
||||
_api.check_in_list(["TeX", True, False], ismath=ismath)
|
||||
if ismath == "TeX":
|
||||
return super().get_text_width_height_descent(s, prop, ismath)
|
||||
|
||||
if ismath:
|
||||
ox, oy, width, height, descent, font_image = \
|
||||
self.mathtext_parser.parse(s, self.dpi, prop)
|
||||
return width, height, descent
|
||||
|
||||
font = self._prepare_font(prop)
|
||||
font.set_text(s, 0.0, flags=get_hinting_flag())
|
||||
w, h = font.get_width_height() # width and height of unrotated string
|
||||
d = font.get_descent()
|
||||
w /= 64.0 # convert from subpixels
|
||||
h /= 64.0
|
||||
d /= 64.0
|
||||
return w, h, d
|
||||
|
||||
def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
|
||||
# docstring inherited
|
||||
# todo, handle props, angle, origins
|
||||
size = prop.get_size_in_points()
|
||||
|
||||
texmanager = self.get_texmanager()
|
||||
|
||||
Z = texmanager.get_grey(s, size, self.dpi)
|
||||
Z = np.array(Z * 255.0, np.uint8)
|
||||
|
||||
w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX")
|
||||
xd = d * sin(radians(angle))
|
||||
yd = d * cos(radians(angle))
|
||||
x = round(x + xd)
|
||||
y = round(y + yd)
|
||||
self._renderer.draw_text_image(Z, x, y, angle, gc)
|
||||
|
||||
def get_canvas_width_height(self):
|
||||
# docstring inherited
|
||||
return self.width, self.height
|
||||
|
||||
def _prepare_font(self, font_prop):
|
||||
"""
|
||||
Get the `.FT2Font` for *font_prop*, clear its buffer, and set its size.
|
||||
"""
|
||||
font = get_font(_fontManager._find_fonts_by_props(font_prop))
|
||||
font.clear()
|
||||
size = font_prop.get_size_in_points()
|
||||
font.set_size(size, self.dpi)
|
||||
return font
|
||||
|
||||
def points_to_pixels(self, points):
|
||||
# docstring inherited
|
||||
return points * self.dpi / 72
|
||||
|
||||
def buffer_rgba(self):
|
||||
return memoryview(self._renderer)
|
||||
|
||||
def tostring_argb(self):
|
||||
return np.asarray(self._renderer).take([3, 0, 1, 2], axis=2).tobytes()
|
||||
|
||||
@_api.deprecated("3.8", alternative="buffer_rgba")
|
||||
def tostring_rgb(self):
|
||||
return np.asarray(self._renderer).take([0, 1, 2], axis=2).tobytes()
|
||||
|
||||
def clear(self):
|
||||
self._renderer.clear()
|
||||
|
||||
def option_image_nocomposite(self):
|
||||
# docstring inherited
|
||||
|
||||
# It is generally faster to composite each image directly to
|
||||
# the Figure, and there's no file size benefit to compositing
|
||||
# with the Agg backend
|
||||
return True
|
||||
|
||||
def option_scale_image(self):
|
||||
# docstring inherited
|
||||
return False
|
||||
|
||||
def restore_region(self, region, bbox=None, xy=None):
|
||||
"""
|
||||
Restore the saved region. If bbox (instance of BboxBase, or
|
||||
its extents) is given, only the region specified by the bbox
|
||||
will be restored. *xy* (a pair of floats) optionally
|
||||
specifies the new position (the LLC of the original region,
|
||||
not the LLC of the bbox) where the region will be restored.
|
||||
|
||||
>>> region = renderer.copy_from_bbox()
|
||||
>>> x1, y1, x2, y2 = region.get_extents()
|
||||
>>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2),
|
||||
... xy=(x1-dx, y1))
|
||||
|
||||
"""
|
||||
if bbox is not None or xy is not None:
|
||||
if bbox is None:
|
||||
x1, y1, x2, y2 = region.get_extents()
|
||||
elif isinstance(bbox, BboxBase):
|
||||
x1, y1, x2, y2 = bbox.extents
|
||||
else:
|
||||
x1, y1, x2, y2 = bbox
|
||||
|
||||
if xy is None:
|
||||
ox, oy = x1, y1
|
||||
else:
|
||||
ox, oy = xy
|
||||
|
||||
# The incoming data is float, but the _renderer type-checking wants
|
||||
# to see integers.
|
||||
self._renderer.restore_region(region, int(x1), int(y1),
|
||||
int(x2), int(y2), int(ox), int(oy))
|
||||
|
||||
else:
|
||||
self._renderer.restore_region(region)
|
||||
|
||||
def start_filter(self):
|
||||
"""
|
||||
Start filtering. It simply creates a new canvas (the old one is saved).
|
||||
"""
|
||||
self._filter_renderers.append(self._renderer)
|
||||
self._renderer = _RendererAgg(int(self.width), int(self.height),
|
||||
self.dpi)
|
||||
self._update_methods()
|
||||
|
||||
def stop_filter(self, post_processing):
|
||||
"""
|
||||
Save the current canvas as an image and apply post processing.
|
||||
|
||||
The *post_processing* function::
|
||||
|
||||
def post_processing(image, dpi):
|
||||
# ny, nx, depth = image.shape
|
||||
# image (numpy array) has RGBA channels and has a depth of 4.
|
||||
...
|
||||
# create a new_image (numpy array of 4 channels, size can be
|
||||
# different). The resulting image may have offsets from
|
||||
# lower-left corner of the original image
|
||||
return new_image, offset_x, offset_y
|
||||
|
||||
The saved renderer is restored and the returned image from
|
||||
post_processing is plotted (using draw_image) on it.
|
||||
"""
|
||||
orig_img = np.asarray(self.buffer_rgba())
|
||||
slice_y, slice_x = cbook._get_nonzero_slices(orig_img[..., 3])
|
||||
cropped_img = orig_img[slice_y, slice_x]
|
||||
|
||||
self._renderer = self._filter_renderers.pop()
|
||||
self._update_methods()
|
||||
|
||||
if cropped_img.size:
|
||||
img, ox, oy = post_processing(cropped_img / 255, self.dpi)
|
||||
gc = self.new_gc()
|
||||
if img.dtype.kind == 'f':
|
||||
img = np.asarray(img * 255., np.uint8)
|
||||
self._renderer.draw_image(
|
||||
gc, slice_x.start + ox, int(self.height) - slice_y.stop + oy,
|
||||
img[::-1])
|
||||
|
||||
|
||||
class FigureCanvasAgg(FigureCanvasBase):
|
||||
# docstring inherited
|
||||
|
||||
_lastKey = None # Overwritten per-instance on the first draw.
|
||||
|
||||
def copy_from_bbox(self, bbox):
|
||||
renderer = self.get_renderer()
|
||||
return renderer.copy_from_bbox(bbox)
|
||||
|
||||
def restore_region(self, region, bbox=None, xy=None):
|
||||
renderer = self.get_renderer()
|
||||
return renderer.restore_region(region, bbox, xy)
|
||||
|
||||
def draw(self):
|
||||
# docstring inherited
|
||||
self.renderer = self.get_renderer()
|
||||
self.renderer.clear()
|
||||
# Acquire a lock on the shared font cache.
|
||||
with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
|
||||
else nullcontext()):
|
||||
self.figure.draw(self.renderer)
|
||||
# A GUI class may be need to update a window using this draw, so
|
||||
# don't forget to call the superclass.
|
||||
super().draw()
|
||||
|
||||
def get_renderer(self):
|
||||
w, h = self.figure.bbox.size
|
||||
key = w, h, self.figure.dpi
|
||||
reuse_renderer = (self._lastKey == key)
|
||||
if not reuse_renderer:
|
||||
self.renderer = RendererAgg(w, h, self.figure.dpi)
|
||||
self._lastKey = key
|
||||
return self.renderer
|
||||
|
||||
@_api.deprecated("3.8", alternative="buffer_rgba")
|
||||
def tostring_rgb(self):
|
||||
"""
|
||||
Get the image as RGB `bytes`.
|
||||
|
||||
`draw` must be called at least once before this function will work and
|
||||
to update the renderer for any subsequent changes to the Figure.
|
||||
"""
|
||||
return self.renderer.tostring_rgb()
|
||||
|
||||
def tostring_argb(self):
|
||||
"""
|
||||
Get the image as ARGB `bytes`.
|
||||
|
||||
`draw` must be called at least once before this function will work and
|
||||
to update the renderer for any subsequent changes to the Figure.
|
||||
"""
|
||||
return self.renderer.tostring_argb()
|
||||
|
||||
def buffer_rgba(self):
|
||||
"""
|
||||
Get the image as a `memoryview` to the renderer's buffer.
|
||||
|
||||
`draw` must be called at least once before this function will work and
|
||||
to update the renderer for any subsequent changes to the Figure.
|
||||
"""
|
||||
return self.renderer.buffer_rgba()
|
||||
|
||||
def print_raw(self, filename_or_obj, *, metadata=None):
|
||||
if metadata is not None:
|
||||
raise ValueError("metadata not supported for raw/rgba")
|
||||
FigureCanvasAgg.draw(self)
|
||||
renderer = self.get_renderer()
|
||||
with cbook.open_file_cm(filename_or_obj, "wb") as fh:
|
||||
fh.write(renderer.buffer_rgba())
|
||||
|
||||
print_rgba = print_raw
|
||||
|
||||
def _print_pil(self, filename_or_obj, fmt, pil_kwargs, metadata=None):
|
||||
"""
|
||||
Draw the canvas, then save it using `.image.imsave` (to which
|
||||
*pil_kwargs* and *metadata* are forwarded).
|
||||
"""
|
||||
FigureCanvasAgg.draw(self)
|
||||
mpl.image.imsave(
|
||||
filename_or_obj, self.buffer_rgba(), format=fmt, origin="upper",
|
||||
dpi=self.figure.dpi, metadata=metadata, pil_kwargs=pil_kwargs)
|
||||
|
||||
def print_png(self, filename_or_obj, *, metadata=None, pil_kwargs=None):
|
||||
"""
|
||||
Write the figure to a PNG file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename_or_obj : str or path-like or file-like
|
||||
The file to write to.
|
||||
|
||||
metadata : dict, optional
|
||||
Metadata in the PNG file as key-value pairs of bytes or latin-1
|
||||
encodable strings.
|
||||
According to the PNG specification, keys must be shorter than 79
|
||||
chars.
|
||||
|
||||
The `PNG specification`_ defines some common keywords that may be
|
||||
used as appropriate:
|
||||
|
||||
- Title: Short (one line) title or caption for image.
|
||||
- Author: Name of image's creator.
|
||||
- Description: Description of image (possibly long).
|
||||
- Copyright: Copyright notice.
|
||||
- Creation Time: Time of original image creation
|
||||
(usually RFC 1123 format).
|
||||
- Software: Software used to create the image.
|
||||
- Disclaimer: Legal disclaimer.
|
||||
- Warning: Warning of nature of content.
|
||||
- Source: Device used to create the image.
|
||||
- Comment: Miscellaneous comment;
|
||||
conversion from other image format.
|
||||
|
||||
Other keywords may be invented for other purposes.
|
||||
|
||||
If 'Software' is not given, an autogenerated value for Matplotlib
|
||||
will be used. This can be removed by setting it to *None*.
|
||||
|
||||
For more details see the `PNG specification`_.
|
||||
|
||||
.. _PNG specification: \
|
||||
https://www.w3.org/TR/2003/REC-PNG-20031110/#11keywords
|
||||
|
||||
pil_kwargs : dict, optional
|
||||
Keyword arguments passed to `PIL.Image.Image.save`.
|
||||
|
||||
If the 'pnginfo' key is present, it completely overrides
|
||||
*metadata*, including the default 'Software' key.
|
||||
"""
|
||||
self._print_pil(filename_or_obj, "png", pil_kwargs, metadata)
|
||||
|
||||
def print_to_buffer(self):
|
||||
FigureCanvasAgg.draw(self)
|
||||
renderer = self.get_renderer()
|
||||
return (bytes(renderer.buffer_rgba()),
|
||||
(int(renderer.width), int(renderer.height)))
|
||||
|
||||
# Note that these methods should typically be called via savefig() and
|
||||
# print_figure(), and the latter ensures that `self.figure.dpi` already
|
||||
# matches the dpi kwarg (if any).
|
||||
|
||||
def print_jpg(self, filename_or_obj, *, metadata=None, pil_kwargs=None):
|
||||
# savefig() has already applied savefig.facecolor; we now set it to
|
||||
# white to make imsave() blend semi-transparent figures against an
|
||||
# assumed white background.
|
||||
with mpl.rc_context({"savefig.facecolor": "white"}):
|
||||
self._print_pil(filename_or_obj, "jpeg", pil_kwargs, metadata)
|
||||
|
||||
print_jpeg = print_jpg
|
||||
|
||||
def print_tif(self, filename_or_obj, *, metadata=None, pil_kwargs=None):
|
||||
self._print_pil(filename_or_obj, "tiff", pil_kwargs, metadata)
|
||||
|
||||
print_tiff = print_tif
|
||||
|
||||
def print_webp(self, filename_or_obj, *, metadata=None, pil_kwargs=None):
|
||||
self._print_pil(filename_or_obj, "webp", pil_kwargs, metadata)
|
||||
|
||||
print_jpg.__doc__, print_tif.__doc__, print_webp.__doc__ = map(
|
||||
"""
|
||||
Write the figure to a {} file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename_or_obj : str or path-like or file-like
|
||||
The file to write to.
|
||||
pil_kwargs : dict, optional
|
||||
Additional keyword arguments that are passed to
|
||||
`PIL.Image.Image.save` when saving the figure.
|
||||
""".format, ["JPEG", "TIFF", "WebP"])
|
||||
|
||||
|
||||
@_Backend.export
|
||||
class _BackendAgg(_Backend):
|
||||
backend_version = 'v2.2'
|
||||
FigureCanvas = FigureCanvasAgg
|
||||
FigureManager = FigureManagerBase
|
||||
@ -0,0 +1,529 @@
|
||||
"""
|
||||
A Cairo backend for Matplotlib
|
||||
==============================
|
||||
:Author: Steve Chaplin and others
|
||||
|
||||
This backend depends on cairocffi or pycairo.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import gzip
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
import cairo
|
||||
if cairo.version_info < (1, 14, 0): # Introduced set_device_scale.
|
||||
raise ImportError(f"Cairo backend requires cairo>=1.14.0, "
|
||||
f"but only {cairo.version_info} is available")
|
||||
except ImportError:
|
||||
try:
|
||||
import cairocffi as cairo
|
||||
except ImportError as err:
|
||||
raise ImportError(
|
||||
"cairo backend requires that pycairo>=1.14.0 or cairocffi "
|
||||
"is installed") from err
|
||||
|
||||
from .. import _api, cbook, font_manager
|
||||
from matplotlib.backend_bases import (
|
||||
_Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
|
||||
RendererBase)
|
||||
from matplotlib.font_manager import ttfFontProperty
|
||||
from matplotlib.path import Path
|
||||
from matplotlib.transforms import Affine2D
|
||||
|
||||
|
||||
def _set_rgba(ctx, color, alpha, forced_alpha):
|
||||
if len(color) == 3 or forced_alpha:
|
||||
ctx.set_source_rgba(*color[:3], alpha)
|
||||
else:
|
||||
ctx.set_source_rgba(*color)
|
||||
|
||||
|
||||
def _append_path(ctx, path, transform, clip=None):
|
||||
for points, code in path.iter_segments(
|
||||
transform, remove_nans=True, clip=clip):
|
||||
if code == Path.MOVETO:
|
||||
ctx.move_to(*points)
|
||||
elif code == Path.CLOSEPOLY:
|
||||
ctx.close_path()
|
||||
elif code == Path.LINETO:
|
||||
ctx.line_to(*points)
|
||||
elif code == Path.CURVE3:
|
||||
cur = np.asarray(ctx.get_current_point())
|
||||
a = points[:2]
|
||||
b = points[-2:]
|
||||
ctx.curve_to(*(cur / 3 + a * 2 / 3), *(a * 2 / 3 + b / 3), *b)
|
||||
elif code == Path.CURVE4:
|
||||
ctx.curve_to(*points)
|
||||
|
||||
|
||||
def _cairo_font_args_from_font_prop(prop):
|
||||
"""
|
||||
Convert a `.FontProperties` or a `.FontEntry` to arguments that can be
|
||||
passed to `.Context.select_font_face`.
|
||||
"""
|
||||
def attr(field):
|
||||
try:
|
||||
return getattr(prop, f"get_{field}")()
|
||||
except AttributeError:
|
||||
return getattr(prop, field)
|
||||
|
||||
name = attr("name")
|
||||
slant = getattr(cairo, f"FONT_SLANT_{attr('style').upper()}")
|
||||
weight = attr("weight")
|
||||
weight = (cairo.FONT_WEIGHT_NORMAL
|
||||
if font_manager.weight_dict.get(weight, weight) < 550
|
||||
else cairo.FONT_WEIGHT_BOLD)
|
||||
return name, slant, weight
|
||||
|
||||
|
||||
class RendererCairo(RendererBase):
|
||||
def __init__(self, dpi):
|
||||
self.dpi = dpi
|
||||
self.gc = GraphicsContextCairo(renderer=self)
|
||||
self.width = None
|
||||
self.height = None
|
||||
self.text_ctx = cairo.Context(
|
||||
cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1))
|
||||
super().__init__()
|
||||
|
||||
def set_context(self, ctx):
|
||||
surface = ctx.get_target()
|
||||
if hasattr(surface, "get_width") and hasattr(surface, "get_height"):
|
||||
size = surface.get_width(), surface.get_height()
|
||||
elif hasattr(surface, "get_extents"): # GTK4 RecordingSurface.
|
||||
ext = surface.get_extents()
|
||||
size = ext.width, ext.height
|
||||
else: # vector surfaces.
|
||||
ctx.save()
|
||||
ctx.reset_clip()
|
||||
rect, *rest = ctx.copy_clip_rectangle_list()
|
||||
if rest:
|
||||
raise TypeError("Cannot infer surface size")
|
||||
_, _, *size = rect
|
||||
ctx.restore()
|
||||
self.gc.ctx = ctx
|
||||
self.width, self.height = size
|
||||
|
||||
@staticmethod
|
||||
def _fill_and_stroke(ctx, fill_c, alpha, alpha_overrides):
|
||||
if fill_c is not None:
|
||||
ctx.save()
|
||||
_set_rgba(ctx, fill_c, alpha, alpha_overrides)
|
||||
ctx.fill_preserve()
|
||||
ctx.restore()
|
||||
ctx.stroke()
|
||||
|
||||
def draw_path(self, gc, path, transform, rgbFace=None):
|
||||
# docstring inherited
|
||||
ctx = gc.ctx
|
||||
# Clip the path to the actual rendering extents if it isn't filled.
|
||||
clip = (ctx.clip_extents()
|
||||
if rgbFace is None and gc.get_hatch() is None
|
||||
else None)
|
||||
transform = (transform
|
||||
+ Affine2D().scale(1, -1).translate(0, self.height))
|
||||
ctx.new_path()
|
||||
_append_path(ctx, path, transform, clip)
|
||||
if rgbFace is not None:
|
||||
ctx.save()
|
||||
_set_rgba(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
|
||||
ctx.fill_preserve()
|
||||
ctx.restore()
|
||||
hatch_path = gc.get_hatch_path()
|
||||
if hatch_path:
|
||||
dpi = int(self.dpi)
|
||||
hatch_surface = ctx.get_target().create_similar(
|
||||
cairo.Content.COLOR_ALPHA, dpi, dpi)
|
||||
hatch_ctx = cairo.Context(hatch_surface)
|
||||
_append_path(hatch_ctx, hatch_path,
|
||||
Affine2D().scale(dpi, -dpi).translate(0, dpi),
|
||||
None)
|
||||
hatch_ctx.set_line_width(self.points_to_pixels(gc.get_hatch_linewidth()))
|
||||
hatch_ctx.set_source_rgba(*gc.get_hatch_color())
|
||||
hatch_ctx.fill_preserve()
|
||||
hatch_ctx.stroke()
|
||||
hatch_pattern = cairo.SurfacePattern(hatch_surface)
|
||||
hatch_pattern.set_extend(cairo.Extend.REPEAT)
|
||||
ctx.save()
|
||||
ctx.set_source(hatch_pattern)
|
||||
ctx.fill_preserve()
|
||||
ctx.restore()
|
||||
ctx.stroke()
|
||||
|
||||
def draw_markers(self, gc, marker_path, marker_trans, path, transform,
|
||||
rgbFace=None):
|
||||
# docstring inherited
|
||||
|
||||
ctx = gc.ctx
|
||||
ctx.new_path()
|
||||
# Create the path for the marker; it needs to be flipped here already!
|
||||
_append_path(ctx, marker_path, marker_trans + Affine2D().scale(1, -1))
|
||||
marker_path = ctx.copy_path_flat()
|
||||
|
||||
# Figure out whether the path has a fill
|
||||
x1, y1, x2, y2 = ctx.fill_extents()
|
||||
if x1 == 0 and y1 == 0 and x2 == 0 and y2 == 0:
|
||||
filled = False
|
||||
# No fill, just unset this (so we don't try to fill it later on)
|
||||
rgbFace = None
|
||||
else:
|
||||
filled = True
|
||||
|
||||
transform = (transform
|
||||
+ Affine2D().scale(1, -1).translate(0, self.height))
|
||||
|
||||
ctx.new_path()
|
||||
for i, (vertices, codes) in enumerate(
|
||||
path.iter_segments(transform, simplify=False)):
|
||||
if len(vertices):
|
||||
x, y = vertices[-2:]
|
||||
ctx.save()
|
||||
|
||||
# Translate and apply path
|
||||
ctx.translate(x, y)
|
||||
ctx.append_path(marker_path)
|
||||
|
||||
ctx.restore()
|
||||
|
||||
# Slower code path if there is a fill; we need to draw
|
||||
# the fill and stroke for each marker at the same time.
|
||||
# Also flush out the drawing every once in a while to
|
||||
# prevent the paths from getting way too long.
|
||||
if filled or i % 1000 == 0:
|
||||
self._fill_and_stroke(
|
||||
ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
|
||||
|
||||
# Fast path, if there is no fill, draw everything in one step
|
||||
if not filled:
|
||||
self._fill_and_stroke(
|
||||
ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
|
||||
|
||||
def draw_image(self, gc, x, y, im):
|
||||
im = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(im[::-1])
|
||||
surface = cairo.ImageSurface.create_for_data(
|
||||
im.ravel().data, cairo.FORMAT_ARGB32,
|
||||
im.shape[1], im.shape[0], im.shape[1] * 4)
|
||||
ctx = gc.ctx
|
||||
y = self.height - y - im.shape[0]
|
||||
|
||||
ctx.save()
|
||||
ctx.set_source_surface(surface, float(x), float(y))
|
||||
ctx.paint()
|
||||
ctx.restore()
|
||||
|
||||
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
|
||||
# docstring inherited
|
||||
|
||||
# Note: (x, y) are device/display coords, not user-coords, unlike other
|
||||
# draw_* methods
|
||||
if ismath:
|
||||
self._draw_mathtext(gc, x, y, s, prop, angle)
|
||||
|
||||
else:
|
||||
ctx = gc.ctx
|
||||
ctx.new_path()
|
||||
ctx.move_to(x, y)
|
||||
|
||||
ctx.save()
|
||||
ctx.select_font_face(*_cairo_font_args_from_font_prop(prop))
|
||||
ctx.set_font_size(self.points_to_pixels(prop.get_size_in_points()))
|
||||
opts = cairo.FontOptions()
|
||||
opts.set_antialias(gc.get_antialiased())
|
||||
ctx.set_font_options(opts)
|
||||
if angle:
|
||||
ctx.rotate(np.deg2rad(-angle))
|
||||
ctx.show_text(s)
|
||||
ctx.restore()
|
||||
|
||||
def _draw_mathtext(self, gc, x, y, s, prop, angle):
|
||||
ctx = gc.ctx
|
||||
width, height, descent, glyphs, rects = \
|
||||
self._text2path.mathtext_parser.parse(s, self.dpi, prop)
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(x, y)
|
||||
if angle:
|
||||
ctx.rotate(np.deg2rad(-angle))
|
||||
|
||||
for font, fontsize, idx, ox, oy in glyphs:
|
||||
ctx.new_path()
|
||||
ctx.move_to(ox, -oy)
|
||||
ctx.select_font_face(
|
||||
*_cairo_font_args_from_font_prop(ttfFontProperty(font)))
|
||||
ctx.set_font_size(self.points_to_pixels(fontsize))
|
||||
ctx.show_text(chr(idx))
|
||||
|
||||
for ox, oy, w, h in rects:
|
||||
ctx.new_path()
|
||||
ctx.rectangle(ox, -oy, w, -h)
|
||||
ctx.set_source_rgb(0, 0, 0)
|
||||
ctx.fill_preserve()
|
||||
|
||||
ctx.restore()
|
||||
|
||||
def get_canvas_width_height(self):
|
||||
# docstring inherited
|
||||
return self.width, self.height
|
||||
|
||||
def get_text_width_height_descent(self, s, prop, ismath):
|
||||
# docstring inherited
|
||||
|
||||
if ismath == 'TeX':
|
||||
return super().get_text_width_height_descent(s, prop, ismath)
|
||||
|
||||
if ismath:
|
||||
width, height, descent, *_ = \
|
||||
self._text2path.mathtext_parser.parse(s, self.dpi, prop)
|
||||
return width, height, descent
|
||||
|
||||
ctx = self.text_ctx
|
||||
# problem - scale remembers last setting and font can become
|
||||
# enormous causing program to crash
|
||||
# save/restore prevents the problem
|
||||
ctx.save()
|
||||
ctx.select_font_face(*_cairo_font_args_from_font_prop(prop))
|
||||
ctx.set_font_size(self.points_to_pixels(prop.get_size_in_points()))
|
||||
|
||||
y_bearing, w, h = ctx.text_extents(s)[1:4]
|
||||
ctx.restore()
|
||||
|
||||
return w, h, h + y_bearing
|
||||
|
||||
def new_gc(self):
|
||||
# docstring inherited
|
||||
self.gc.ctx.save()
|
||||
# FIXME: The following doesn't properly implement a stack-like behavior
|
||||
# and relies instead on the (non-guaranteed) fact that artists never
|
||||
# rely on nesting gc states, so directly resetting the attributes (IOW
|
||||
# a single-level stack) is enough.
|
||||
self.gc._alpha = 1
|
||||
self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA
|
||||
self.gc._hatch = None
|
||||
return self.gc
|
||||
|
||||
def points_to_pixels(self, points):
|
||||
# docstring inherited
|
||||
return points / 72 * self.dpi
|
||||
|
||||
|
||||
class GraphicsContextCairo(GraphicsContextBase):
|
||||
_joind = {
|
||||
'bevel': cairo.LINE_JOIN_BEVEL,
|
||||
'miter': cairo.LINE_JOIN_MITER,
|
||||
'round': cairo.LINE_JOIN_ROUND,
|
||||
}
|
||||
|
||||
_capd = {
|
||||
'butt': cairo.LINE_CAP_BUTT,
|
||||
'projecting': cairo.LINE_CAP_SQUARE,
|
||||
'round': cairo.LINE_CAP_ROUND,
|
||||
}
|
||||
|
||||
def __init__(self, renderer):
|
||||
super().__init__()
|
||||
self.renderer = renderer
|
||||
|
||||
def restore(self):
|
||||
self.ctx.restore()
|
||||
|
||||
def set_alpha(self, alpha):
|
||||
super().set_alpha(alpha)
|
||||
_set_rgba(
|
||||
self.ctx, self._rgb, self.get_alpha(), self.get_forced_alpha())
|
||||
|
||||
def set_antialiased(self, b):
|
||||
self.ctx.set_antialias(
|
||||
cairo.ANTIALIAS_DEFAULT if b else cairo.ANTIALIAS_NONE)
|
||||
|
||||
def get_antialiased(self):
|
||||
return self.ctx.get_antialias()
|
||||
|
||||
def set_capstyle(self, cs):
|
||||
self.ctx.set_line_cap(_api.check_getitem(self._capd, capstyle=cs))
|
||||
self._capstyle = cs
|
||||
|
||||
def set_clip_rectangle(self, rectangle):
|
||||
if not rectangle:
|
||||
return
|
||||
x, y, w, h = np.round(rectangle.bounds)
|
||||
ctx = self.ctx
|
||||
ctx.new_path()
|
||||
ctx.rectangle(x, self.renderer.height - h - y, w, h)
|
||||
ctx.clip()
|
||||
|
||||
def set_clip_path(self, path):
|
||||
if not path:
|
||||
return
|
||||
tpath, affine = path.get_transformed_path_and_affine()
|
||||
ctx = self.ctx
|
||||
ctx.new_path()
|
||||
affine = (affine
|
||||
+ Affine2D().scale(1, -1).translate(0, self.renderer.height))
|
||||
_append_path(ctx, tpath, affine)
|
||||
ctx.clip()
|
||||
|
||||
def set_dashes(self, offset, dashes):
|
||||
self._dashes = offset, dashes
|
||||
if dashes is None:
|
||||
self.ctx.set_dash([], 0) # switch dashes off
|
||||
else:
|
||||
self.ctx.set_dash(
|
||||
list(self.renderer.points_to_pixels(np.asarray(dashes))),
|
||||
offset)
|
||||
|
||||
def set_foreground(self, fg, isRGBA=None):
|
||||
super().set_foreground(fg, isRGBA)
|
||||
if len(self._rgb) == 3:
|
||||
self.ctx.set_source_rgb(*self._rgb)
|
||||
else:
|
||||
self.ctx.set_source_rgba(*self._rgb)
|
||||
|
||||
def get_rgb(self):
|
||||
return self.ctx.get_source().get_rgba()[:3]
|
||||
|
||||
def set_joinstyle(self, js):
|
||||
self.ctx.set_line_join(_api.check_getitem(self._joind, joinstyle=js))
|
||||
self._joinstyle = js
|
||||
|
||||
def set_linewidth(self, w):
|
||||
self._linewidth = float(w)
|
||||
self.ctx.set_line_width(self.renderer.points_to_pixels(w))
|
||||
|
||||
|
||||
class _CairoRegion:
|
||||
def __init__(self, slices, data):
|
||||
self._slices = slices
|
||||
self._data = data
|
||||
|
||||
|
||||
class FigureCanvasCairo(FigureCanvasBase):
|
||||
@property
|
||||
def _renderer(self):
|
||||
# In theory, _renderer should be set in __init__, but GUI canvas
|
||||
# subclasses (FigureCanvasFooCairo) don't always interact well with
|
||||
# multiple inheritance (FigureCanvasFoo inits but doesn't super-init
|
||||
# FigureCanvasCairo), so initialize it in the getter instead.
|
||||
if not hasattr(self, "_cached_renderer"):
|
||||
self._cached_renderer = RendererCairo(self.figure.dpi)
|
||||
return self._cached_renderer
|
||||
|
||||
def get_renderer(self):
|
||||
return self._renderer
|
||||
|
||||
def copy_from_bbox(self, bbox):
|
||||
surface = self._renderer.gc.ctx.get_target()
|
||||
if not isinstance(surface, cairo.ImageSurface):
|
||||
raise RuntimeError(
|
||||
"copy_from_bbox only works when rendering to an ImageSurface")
|
||||
sw = surface.get_width()
|
||||
sh = surface.get_height()
|
||||
x0 = math.ceil(bbox.x0)
|
||||
x1 = math.floor(bbox.x1)
|
||||
y0 = math.ceil(sh - bbox.y1)
|
||||
y1 = math.floor(sh - bbox.y0)
|
||||
if not (0 <= x0 and x1 <= sw and bbox.x0 <= bbox.x1
|
||||
and 0 <= y0 and y1 <= sh and bbox.y0 <= bbox.y1):
|
||||
raise ValueError("Invalid bbox")
|
||||
sls = slice(y0, y0 + max(y1 - y0, 0)), slice(x0, x0 + max(x1 - x0, 0))
|
||||
data = (np.frombuffer(surface.get_data(), np.uint32)
|
||||
.reshape((sh, sw))[sls].copy())
|
||||
return _CairoRegion(sls, data)
|
||||
|
||||
def restore_region(self, region):
|
||||
surface = self._renderer.gc.ctx.get_target()
|
||||
if not isinstance(surface, cairo.ImageSurface):
|
||||
raise RuntimeError(
|
||||
"restore_region only works when rendering to an ImageSurface")
|
||||
surface.flush()
|
||||
sw = surface.get_width()
|
||||
sh = surface.get_height()
|
||||
sly, slx = region._slices
|
||||
(np.frombuffer(surface.get_data(), np.uint32)
|
||||
.reshape((sh, sw))[sly, slx]) = region._data
|
||||
surface.mark_dirty_rectangle(
|
||||
slx.start, sly.start, slx.stop - slx.start, sly.stop - sly.start)
|
||||
|
||||
def print_png(self, fobj):
|
||||
self._get_printed_image_surface().write_to_png(fobj)
|
||||
|
||||
def print_rgba(self, fobj):
|
||||
width, height = self.get_width_height()
|
||||
buf = self._get_printed_image_surface().get_data()
|
||||
fobj.write(cbook._premultiplied_argb32_to_unmultiplied_rgba8888(
|
||||
np.asarray(buf).reshape((width, height, 4))))
|
||||
|
||||
print_raw = print_rgba
|
||||
|
||||
def _get_printed_image_surface(self):
|
||||
self._renderer.dpi = self.figure.dpi
|
||||
width, height = self.get_width_height()
|
||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
|
||||
self._renderer.set_context(cairo.Context(surface))
|
||||
self.figure.draw(self._renderer)
|
||||
return surface
|
||||
|
||||
def _save(self, fmt, fobj, *, orientation='portrait'):
|
||||
# save PDF/PS/SVG
|
||||
|
||||
dpi = 72
|
||||
self.figure.dpi = dpi
|
||||
w_in, h_in = self.figure.get_size_inches()
|
||||
width_in_points, height_in_points = w_in * dpi, h_in * dpi
|
||||
|
||||
if orientation == 'landscape':
|
||||
width_in_points, height_in_points = (
|
||||
height_in_points, width_in_points)
|
||||
|
||||
if fmt == 'ps':
|
||||
if not hasattr(cairo, 'PSSurface'):
|
||||
raise RuntimeError('cairo has not been compiled with PS '
|
||||
'support enabled')
|
||||
surface = cairo.PSSurface(fobj, width_in_points, height_in_points)
|
||||
elif fmt == 'pdf':
|
||||
if not hasattr(cairo, 'PDFSurface'):
|
||||
raise RuntimeError('cairo has not been compiled with PDF '
|
||||
'support enabled')
|
||||
surface = cairo.PDFSurface(fobj, width_in_points, height_in_points)
|
||||
elif fmt in ('svg', 'svgz'):
|
||||
if not hasattr(cairo, 'SVGSurface'):
|
||||
raise RuntimeError('cairo has not been compiled with SVG '
|
||||
'support enabled')
|
||||
if fmt == 'svgz':
|
||||
if isinstance(fobj, str):
|
||||
fobj = gzip.GzipFile(fobj, 'wb')
|
||||
else:
|
||||
fobj = gzip.GzipFile(None, 'wb', fileobj=fobj)
|
||||
surface = cairo.SVGSurface(fobj, width_in_points, height_in_points)
|
||||
else:
|
||||
raise ValueError(f"Unknown format: {fmt!r}")
|
||||
|
||||
self._renderer.dpi = self.figure.dpi
|
||||
self._renderer.set_context(cairo.Context(surface))
|
||||
ctx = self._renderer.gc.ctx
|
||||
|
||||
if orientation == 'landscape':
|
||||
ctx.rotate(np.pi / 2)
|
||||
ctx.translate(0, -height_in_points)
|
||||
# Perhaps add an '%%Orientation: Landscape' comment?
|
||||
|
||||
self.figure.draw(self._renderer)
|
||||
|
||||
ctx.show_page()
|
||||
surface.finish()
|
||||
if fmt == 'svgz':
|
||||
fobj.close()
|
||||
|
||||
print_pdf = functools.partialmethod(_save, "pdf")
|
||||
print_ps = functools.partialmethod(_save, "ps")
|
||||
print_svg = functools.partialmethod(_save, "svg")
|
||||
print_svgz = functools.partialmethod(_save, "svgz")
|
||||
|
||||
|
||||
@_Backend.export
|
||||
class _BackendCairo(_Backend):
|
||||
backend_version = cairo.version
|
||||
FigureCanvas = FigureCanvasCairo
|
||||
FigureManager = FigureManagerBase
|
||||
@ -0,0 +1,582 @@
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib as mpl
|
||||
from matplotlib import _api, backend_tools, cbook
|
||||
from matplotlib.backend_bases import (
|
||||
ToolContainerBase, CloseEvent, KeyEvent, LocationEvent, MouseEvent,
|
||||
ResizeEvent)
|
||||
|
||||
try:
|
||||
import gi
|
||||
except ImportError as err:
|
||||
raise ImportError("The GTK3 backends require PyGObject") from err
|
||||
|
||||
try:
|
||||
# :raises ValueError: If module/version is already loaded, already
|
||||
# required, or unavailable.
|
||||
gi.require_version("Gtk", "3.0")
|
||||
except ValueError as e:
|
||||
# in this case we want to re-raise as ImportError so the
|
||||
# auto-backend selection logic correctly skips.
|
||||
raise ImportError(e) from e
|
||||
|
||||
from gi.repository import Gio, GLib, GObject, Gtk, Gdk
|
||||
from . import _backend_gtk
|
||||
from ._backend_gtk import ( # noqa: F401 # pylint: disable=W0611
|
||||
_BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
|
||||
TimerGTK as TimerGTK3,
|
||||
)
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _mpl_to_gtk_cursor(mpl_cursor):
|
||||
return Gdk.Cursor.new_from_name(
|
||||
Gdk.Display.get_default(),
|
||||
_backend_gtk.mpl_to_gtk_cursor_name(mpl_cursor))
|
||||
|
||||
|
||||
class FigureCanvasGTK3(_FigureCanvasGTK, Gtk.DrawingArea):
|
||||
required_interactive_framework = "gtk3"
|
||||
manager_class = _api.classproperty(lambda cls: FigureManagerGTK3)
|
||||
# Setting this as a static constant prevents
|
||||
# this resulting expression from leaking
|
||||
event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK
|
||||
| Gdk.EventMask.BUTTON_RELEASE_MASK
|
||||
| Gdk.EventMask.EXPOSURE_MASK
|
||||
| Gdk.EventMask.KEY_PRESS_MASK
|
||||
| Gdk.EventMask.KEY_RELEASE_MASK
|
||||
| Gdk.EventMask.ENTER_NOTIFY_MASK
|
||||
| Gdk.EventMask.LEAVE_NOTIFY_MASK
|
||||
| Gdk.EventMask.POINTER_MOTION_MASK
|
||||
| Gdk.EventMask.SCROLL_MASK)
|
||||
|
||||
def __init__(self, figure=None):
|
||||
super().__init__(figure=figure)
|
||||
|
||||
self._idle_draw_id = 0
|
||||
self._rubberband_rect = None
|
||||
|
||||
self.connect('scroll_event', self.scroll_event)
|
||||
self.connect('button_press_event', self.button_press_event)
|
||||
self.connect('button_release_event', self.button_release_event)
|
||||
self.connect('configure_event', self.configure_event)
|
||||
self.connect('screen-changed', self._update_device_pixel_ratio)
|
||||
self.connect('notify::scale-factor', self._update_device_pixel_ratio)
|
||||
self.connect('draw', self.on_draw_event)
|
||||
self.connect('draw', self._post_draw)
|
||||
self.connect('key_press_event', self.key_press_event)
|
||||
self.connect('key_release_event', self.key_release_event)
|
||||
self.connect('motion_notify_event', self.motion_notify_event)
|
||||
self.connect('enter_notify_event', self.enter_notify_event)
|
||||
self.connect('leave_notify_event', self.leave_notify_event)
|
||||
self.connect('size_allocate', self.size_allocate)
|
||||
|
||||
self.set_events(self.__class__.event_mask)
|
||||
|
||||
self.set_can_focus(True)
|
||||
|
||||
css = Gtk.CssProvider()
|
||||
css.load_from_data(b".matplotlib-canvas { background-color: white; }")
|
||||
style_ctx = self.get_style_context()
|
||||
style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||
style_ctx.add_class("matplotlib-canvas")
|
||||
|
||||
def destroy(self):
|
||||
CloseEvent("close_event", self)._process()
|
||||
|
||||
def set_cursor(self, cursor):
|
||||
# docstring inherited
|
||||
window = self.get_property("window")
|
||||
if window is not None:
|
||||
window.set_cursor(_mpl_to_gtk_cursor(cursor))
|
||||
context = GLib.MainContext.default()
|
||||
context.iteration(True)
|
||||
|
||||
def _mpl_coords(self, event=None):
|
||||
"""
|
||||
Convert the position of a GTK event, or of the current cursor position
|
||||
if *event* is None, to Matplotlib coordinates.
|
||||
|
||||
GTK use logical pixels, but the figure is scaled to physical pixels for
|
||||
rendering. Transform to physical pixels so that all of the down-stream
|
||||
transforms work as expected.
|
||||
|
||||
Also, the origin is different and needs to be corrected.
|
||||
"""
|
||||
if event is None:
|
||||
window = self.get_window()
|
||||
t, x, y, state = window.get_device_position(
|
||||
window.get_display().get_device_manager().get_client_pointer())
|
||||
else:
|
||||
x, y = event.x, event.y
|
||||
x = x * self.device_pixel_ratio
|
||||
# flip y so y=0 is bottom of canvas
|
||||
y = self.figure.bbox.height - y * self.device_pixel_ratio
|
||||
return x, y
|
||||
|
||||
def scroll_event(self, widget, event):
|
||||
step = 1 if event.direction == Gdk.ScrollDirection.UP else -1
|
||||
MouseEvent("scroll_event", self,
|
||||
*self._mpl_coords(event), step=step,
|
||||
modifiers=self._mpl_modifiers(event.state),
|
||||
guiEvent=event)._process()
|
||||
return False # finish event propagation?
|
||||
|
||||
def button_press_event(self, widget, event):
|
||||
MouseEvent("button_press_event", self,
|
||||
*self._mpl_coords(event), event.button,
|
||||
modifiers=self._mpl_modifiers(event.state),
|
||||
guiEvent=event)._process()
|
||||
return False # finish event propagation?
|
||||
|
||||
def button_release_event(self, widget, event):
|
||||
MouseEvent("button_release_event", self,
|
||||
*self._mpl_coords(event), event.button,
|
||||
modifiers=self._mpl_modifiers(event.state),
|
||||
guiEvent=event)._process()
|
||||
return False # finish event propagation?
|
||||
|
||||
def key_press_event(self, widget, event):
|
||||
KeyEvent("key_press_event", self,
|
||||
self._get_key(event), *self._mpl_coords(),
|
||||
guiEvent=event)._process()
|
||||
return True # stop event propagation
|
||||
|
||||
def key_release_event(self, widget, event):
|
||||
KeyEvent("key_release_event", self,
|
||||
self._get_key(event), *self._mpl_coords(),
|
||||
guiEvent=event)._process()
|
||||
return True # stop event propagation
|
||||
|
||||
def motion_notify_event(self, widget, event):
|
||||
MouseEvent("motion_notify_event", self, *self._mpl_coords(event),
|
||||
modifiers=self._mpl_modifiers(event.state),
|
||||
guiEvent=event)._process()
|
||||
return False # finish event propagation?
|
||||
|
||||
def enter_notify_event(self, widget, event):
|
||||
gtk_mods = Gdk.Keymap.get_for_display(
|
||||
self.get_display()).get_modifier_state()
|
||||
LocationEvent("figure_enter_event", self, *self._mpl_coords(event),
|
||||
modifiers=self._mpl_modifiers(gtk_mods),
|
||||
guiEvent=event)._process()
|
||||
|
||||
def leave_notify_event(self, widget, event):
|
||||
gtk_mods = Gdk.Keymap.get_for_display(
|
||||
self.get_display()).get_modifier_state()
|
||||
LocationEvent("figure_leave_event", self, *self._mpl_coords(event),
|
||||
modifiers=self._mpl_modifiers(gtk_mods),
|
||||
guiEvent=event)._process()
|
||||
|
||||
def size_allocate(self, widget, allocation):
|
||||
dpival = self.figure.dpi
|
||||
winch = allocation.width * self.device_pixel_ratio / dpival
|
||||
hinch = allocation.height * self.device_pixel_ratio / dpival
|
||||
self.figure.set_size_inches(winch, hinch, forward=False)
|
||||
ResizeEvent("resize_event", self)._process()
|
||||
self.draw_idle()
|
||||
|
||||
@staticmethod
|
||||
def _mpl_modifiers(event_state, *, exclude=None):
|
||||
modifiers = [
|
||||
("ctrl", Gdk.ModifierType.CONTROL_MASK, "control"),
|
||||
("alt", Gdk.ModifierType.MOD1_MASK, "alt"),
|
||||
("shift", Gdk.ModifierType.SHIFT_MASK, "shift"),
|
||||
("super", Gdk.ModifierType.MOD4_MASK, "super"),
|
||||
]
|
||||
return [name for name, mask, key in modifiers
|
||||
if exclude != key and event_state & mask]
|
||||
|
||||
def _get_key(self, event):
|
||||
unikey = chr(Gdk.keyval_to_unicode(event.keyval))
|
||||
key = cbook._unikey_or_keysym_to_mplkey(
|
||||
unikey, Gdk.keyval_name(event.keyval))
|
||||
mods = self._mpl_modifiers(event.state, exclude=key)
|
||||
if "shift" in mods and unikey.isprintable():
|
||||
mods.remove("shift")
|
||||
return "+".join([*mods, key])
|
||||
|
||||
def _update_device_pixel_ratio(self, *args, **kwargs):
|
||||
# We need to be careful in cases with mixed resolution displays if
|
||||
# device_pixel_ratio changes.
|
||||
if self._set_device_pixel_ratio(self.get_scale_factor()):
|
||||
# The easiest way to resize the canvas is to emit a resize event
|
||||
# since we implement all the logic for resizing the canvas for that
|
||||
# event.
|
||||
self.queue_resize()
|
||||
self.queue_draw()
|
||||
|
||||
def configure_event(self, widget, event):
|
||||
if widget.get_property("window") is None:
|
||||
return
|
||||
w = event.width * self.device_pixel_ratio
|
||||
h = event.height * self.device_pixel_ratio
|
||||
if w < 3 or h < 3:
|
||||
return # empty fig
|
||||
# resize the figure (in inches)
|
||||
dpi = self.figure.dpi
|
||||
self.figure.set_size_inches(w / dpi, h / dpi, forward=False)
|
||||
return False # finish event propagation?
|
||||
|
||||
def _draw_rubberband(self, rect):
|
||||
self._rubberband_rect = rect
|
||||
# TODO: Only update the rubberband area.
|
||||
self.queue_draw()
|
||||
|
||||
def _post_draw(self, widget, ctx):
|
||||
if self._rubberband_rect is None:
|
||||
return
|
||||
|
||||
x0, y0, w, h = (dim / self.device_pixel_ratio
|
||||
for dim in self._rubberband_rect)
|
||||
x1 = x0 + w
|
||||
y1 = y0 + h
|
||||
|
||||
# Draw the lines from x0, y0 towards x1, y1 so that the
|
||||
# dashes don't "jump" when moving the zoom box.
|
||||
ctx.move_to(x0, y0)
|
||||
ctx.line_to(x0, y1)
|
||||
ctx.move_to(x0, y0)
|
||||
ctx.line_to(x1, y0)
|
||||
ctx.move_to(x0, y1)
|
||||
ctx.line_to(x1, y1)
|
||||
ctx.move_to(x1, y0)
|
||||
ctx.line_to(x1, y1)
|
||||
|
||||
ctx.set_antialias(1)
|
||||
ctx.set_line_width(1)
|
||||
ctx.set_dash((3, 3), 0)
|
||||
ctx.set_source_rgb(0, 0, 0)
|
||||
ctx.stroke_preserve()
|
||||
|
||||
ctx.set_dash((3, 3), 3)
|
||||
ctx.set_source_rgb(1, 1, 1)
|
||||
ctx.stroke()
|
||||
|
||||
def on_draw_event(self, widget, ctx):
|
||||
# to be overwritten by GTK3Agg or GTK3Cairo
|
||||
pass
|
||||
|
||||
def draw(self):
|
||||
# docstring inherited
|
||||
if self.is_drawable():
|
||||
self.queue_draw()
|
||||
|
||||
def draw_idle(self):
|
||||
# docstring inherited
|
||||
if self._idle_draw_id != 0:
|
||||
return
|
||||
def idle_draw(*args):
|
||||
try:
|
||||
self.draw()
|
||||
finally:
|
||||
self._idle_draw_id = 0
|
||||
return False
|
||||
self._idle_draw_id = GLib.idle_add(idle_draw)
|
||||
|
||||
def flush_events(self):
|
||||
# docstring inherited
|
||||
context = GLib.MainContext.default()
|
||||
while context.pending():
|
||||
context.iteration(True)
|
||||
|
||||
|
||||
class NavigationToolbar2GTK3(_NavigationToolbar2GTK, Gtk.Toolbar):
|
||||
def __init__(self, canvas):
|
||||
GObject.GObject.__init__(self)
|
||||
|
||||
self.set_style(Gtk.ToolbarStyle.ICONS)
|
||||
|
||||
self._gtk_ids = {}
|
||||
for text, tooltip_text, image_file, callback in self.toolitems:
|
||||
if text is None:
|
||||
self.insert(Gtk.SeparatorToolItem(), -1)
|
||||
continue
|
||||
image = Gtk.Image.new_from_gicon(
|
||||
Gio.Icon.new_for_string(
|
||||
str(cbook._get_data_path('images',
|
||||
f'{image_file}-symbolic.svg'))),
|
||||
Gtk.IconSize.LARGE_TOOLBAR)
|
||||
self._gtk_ids[text] = button = (
|
||||
Gtk.ToggleToolButton() if callback in ['zoom', 'pan'] else
|
||||
Gtk.ToolButton())
|
||||
button.set_label(text)
|
||||
button.set_icon_widget(image)
|
||||
# Save the handler id, so that we can block it as needed.
|
||||
button._signal_handler = button.connect(
|
||||
'clicked', getattr(self, callback))
|
||||
button.set_tooltip_text(tooltip_text)
|
||||
self.insert(button, -1)
|
||||
|
||||
# This filler item ensures the toolbar is always at least two text
|
||||
# lines high. Otherwise the canvas gets redrawn as the mouse hovers
|
||||
# over images because those use two-line messages which resize the
|
||||
# toolbar.
|
||||
toolitem = Gtk.ToolItem()
|
||||
self.insert(toolitem, -1)
|
||||
label = Gtk.Label()
|
||||
label.set_markup(
|
||||
'<small>\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}</small>')
|
||||
toolitem.set_expand(True) # Push real message to the right.
|
||||
toolitem.add(label)
|
||||
|
||||
toolitem = Gtk.ToolItem()
|
||||
self.insert(toolitem, -1)
|
||||
self.message = Gtk.Label()
|
||||
self.message.set_justify(Gtk.Justification.RIGHT)
|
||||
toolitem.add(self.message)
|
||||
|
||||
self.show_all()
|
||||
|
||||
_NavigationToolbar2GTK.__init__(self, canvas)
|
||||
|
||||
def save_figure(self, *args):
|
||||
dialog = Gtk.FileChooserDialog(
|
||||
title="Save the figure",
|
||||
parent=self.canvas.get_toplevel(),
|
||||
action=Gtk.FileChooserAction.SAVE,
|
||||
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
||||
Gtk.STOCK_SAVE, Gtk.ResponseType.OK),
|
||||
)
|
||||
for name, fmts \
|
||||
in self.canvas.get_supported_filetypes_grouped().items():
|
||||
ff = Gtk.FileFilter()
|
||||
ff.set_name(name)
|
||||
for fmt in fmts:
|
||||
ff.add_pattern(f'*.{fmt}')
|
||||
dialog.add_filter(ff)
|
||||
if self.canvas.get_default_filetype() in fmts:
|
||||
dialog.set_filter(ff)
|
||||
|
||||
@functools.partial(dialog.connect, "notify::filter")
|
||||
def on_notify_filter(*args):
|
||||
name = dialog.get_filter().get_name()
|
||||
fmt = self.canvas.get_supported_filetypes_grouped()[name][0]
|
||||
dialog.set_current_name(
|
||||
str(Path(dialog.get_current_name()).with_suffix(f'.{fmt}')))
|
||||
|
||||
dialog.set_current_folder(mpl.rcParams["savefig.directory"])
|
||||
dialog.set_current_name(self.canvas.get_default_filename())
|
||||
dialog.set_do_overwrite_confirmation(True)
|
||||
|
||||
response = dialog.run()
|
||||
fname = dialog.get_filename()
|
||||
ff = dialog.get_filter() # Doesn't autoadjust to filename :/
|
||||
fmt = self.canvas.get_supported_filetypes_grouped()[ff.get_name()][0]
|
||||
dialog.destroy()
|
||||
if response != Gtk.ResponseType.OK:
|
||||
return
|
||||
# Save dir for next time, unless empty str (which means use cwd).
|
||||
if mpl.rcParams['savefig.directory']:
|
||||
mpl.rcParams['savefig.directory'] = os.path.dirname(fname)
|
||||
try:
|
||||
self.canvas.figure.savefig(fname, format=fmt)
|
||||
except Exception as e:
|
||||
dialog = Gtk.MessageDialog(
|
||||
parent=self.canvas.get_toplevel(), message_format=str(e),
|
||||
type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
|
||||
class ToolbarGTK3(ToolContainerBase, Gtk.Box):
|
||||
_icon_extension = '-symbolic.svg'
|
||||
|
||||
def __init__(self, toolmanager):
|
||||
ToolContainerBase.__init__(self, toolmanager)
|
||||
Gtk.Box.__init__(self)
|
||||
self.set_property('orientation', Gtk.Orientation.HORIZONTAL)
|
||||
self._message = Gtk.Label()
|
||||
self._message.set_justify(Gtk.Justification.RIGHT)
|
||||
self.pack_end(self._message, False, False, 0)
|
||||
self.show_all()
|
||||
self._groups = {}
|
||||
self._toolitems = {}
|
||||
|
||||
def add_toolitem(self, name, group, position, image_file, description,
|
||||
toggle):
|
||||
if toggle:
|
||||
button = Gtk.ToggleToolButton()
|
||||
else:
|
||||
button = Gtk.ToolButton()
|
||||
button.set_label(name)
|
||||
|
||||
if image_file is not None:
|
||||
image = Gtk.Image.new_from_gicon(
|
||||
Gio.Icon.new_for_string(image_file),
|
||||
Gtk.IconSize.LARGE_TOOLBAR)
|
||||
button.set_icon_widget(image)
|
||||
|
||||
if position is None:
|
||||
position = -1
|
||||
|
||||
self._add_button(button, group, position)
|
||||
signal = button.connect('clicked', self._call_tool, name)
|
||||
button.set_tooltip_text(description)
|
||||
button.show_all()
|
||||
self._toolitems.setdefault(name, [])
|
||||
self._toolitems[name].append((button, signal))
|
||||
|
||||
def _add_button(self, button, group, position):
|
||||
if group not in self._groups:
|
||||
if self._groups:
|
||||
self._add_separator()
|
||||
toolbar = Gtk.Toolbar()
|
||||
toolbar.set_style(Gtk.ToolbarStyle.ICONS)
|
||||
self.pack_start(toolbar, False, False, 0)
|
||||
toolbar.show_all()
|
||||
self._groups[group] = toolbar
|
||||
self._groups[group].insert(button, position)
|
||||
|
||||
def _call_tool(self, btn, name):
|
||||
self.trigger_tool(name)
|
||||
|
||||
def toggle_toolitem(self, name, toggled):
|
||||
if name not in self._toolitems:
|
||||
return
|
||||
for toolitem, signal in self._toolitems[name]:
|
||||
toolitem.handler_block(signal)
|
||||
toolitem.set_active(toggled)
|
||||
toolitem.handler_unblock(signal)
|
||||
|
||||
def remove_toolitem(self, name):
|
||||
for toolitem, _signal in self._toolitems.pop(name, []):
|
||||
for group in self._groups:
|
||||
if toolitem in self._groups[group]:
|
||||
self._groups[group].remove(toolitem)
|
||||
|
||||
def _add_separator(self):
|
||||
sep = Gtk.Separator()
|
||||
sep.set_property("orientation", Gtk.Orientation.VERTICAL)
|
||||
self.pack_start(sep, False, True, 0)
|
||||
sep.show_all()
|
||||
|
||||
def set_message(self, s):
|
||||
self._message.set_label(s)
|
||||
|
||||
|
||||
@backend_tools._register_tool_class(FigureCanvasGTK3)
|
||||
class SaveFigureGTK3(backend_tools.SaveFigureBase):
|
||||
def trigger(self, *args, **kwargs):
|
||||
NavigationToolbar2GTK3.save_figure(
|
||||
self._make_classic_style_pseudo_toolbar())
|
||||
|
||||
|
||||
@backend_tools._register_tool_class(FigureCanvasGTK3)
|
||||
class HelpGTK3(backend_tools.ToolHelpBase):
|
||||
def _normalize_shortcut(self, key):
|
||||
"""
|
||||
Convert Matplotlib key presses to GTK+ accelerator identifiers.
|
||||
|
||||
Related to `FigureCanvasGTK3._get_key`.
|
||||
"""
|
||||
special = {
|
||||
'backspace': 'BackSpace',
|
||||
'pagedown': 'Page_Down',
|
||||
'pageup': 'Page_Up',
|
||||
'scroll_lock': 'Scroll_Lock',
|
||||
}
|
||||
|
||||
parts = key.split('+')
|
||||
mods = ['<' + mod + '>' for mod in parts[:-1]]
|
||||
key = parts[-1]
|
||||
|
||||
if key in special:
|
||||
key = special[key]
|
||||
elif len(key) > 1:
|
||||
key = key.capitalize()
|
||||
elif key.isupper():
|
||||
mods += ['<shift>']
|
||||
|
||||
return ''.join(mods) + key
|
||||
|
||||
def _is_valid_shortcut(self, key):
|
||||
"""
|
||||
Check for a valid shortcut to be displayed.
|
||||
|
||||
- GTK will never send 'cmd+' (see `FigureCanvasGTK3._get_key`).
|
||||
- The shortcut window only shows keyboard shortcuts, not mouse buttons.
|
||||
"""
|
||||
return 'cmd+' not in key and not key.startswith('MouseButton.')
|
||||
|
||||
def _show_shortcuts_window(self):
|
||||
section = Gtk.ShortcutsSection()
|
||||
|
||||
for name, tool in sorted(self.toolmanager.tools.items()):
|
||||
if not tool.description:
|
||||
continue
|
||||
|
||||
# Putting everything in a separate group allows GTK to
|
||||
# automatically split them into separate columns/pages, which is
|
||||
# useful because we have lots of shortcuts, some with many keys
|
||||
# that are very wide.
|
||||
group = Gtk.ShortcutsGroup()
|
||||
section.add(group)
|
||||
# A hack to remove the title since we have no group naming.
|
||||
group.forall(lambda widget, data: widget.set_visible(False), None)
|
||||
|
||||
shortcut = Gtk.ShortcutsShortcut(
|
||||
accelerator=' '.join(
|
||||
self._normalize_shortcut(key)
|
||||
for key in self.toolmanager.get_tool_keymap(name)
|
||||
if self._is_valid_shortcut(key)),
|
||||
title=tool.name,
|
||||
subtitle=tool.description)
|
||||
group.add(shortcut)
|
||||
|
||||
window = Gtk.ShortcutsWindow(
|
||||
title='Help',
|
||||
modal=True,
|
||||
transient_for=self._figure.canvas.get_toplevel())
|
||||
section.show() # Must be done explicitly before add!
|
||||
window.add(section)
|
||||
|
||||
window.show_all()
|
||||
|
||||
def _show_shortcuts_dialog(self):
|
||||
dialog = Gtk.MessageDialog(
|
||||
self._figure.canvas.get_toplevel(),
|
||||
0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, self._get_help_text(),
|
||||
title="Help")
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
def trigger(self, *args):
|
||||
if Gtk.check_version(3, 20, 0) is None:
|
||||
self._show_shortcuts_window()
|
||||
else:
|
||||
self._show_shortcuts_dialog()
|
||||
|
||||
|
||||
@backend_tools._register_tool_class(FigureCanvasGTK3)
|
||||
class ToolCopyToClipboardGTK3(backend_tools.ToolCopyToClipboardBase):
|
||||
def trigger(self, *args, **kwargs):
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
window = self.canvas.get_window()
|
||||
x, y, width, height = window.get_geometry()
|
||||
pb = Gdk.pixbuf_get_from_window(window, x, y, width, height)
|
||||
clipboard.set_image(pb)
|
||||
|
||||
|
||||
Toolbar = ToolbarGTK3
|
||||
backend_tools._register_tool_class(
|
||||
FigureCanvasGTK3, _backend_gtk.ConfigureSubplotsGTK)
|
||||
backend_tools._register_tool_class(
|
||||
FigureCanvasGTK3, _backend_gtk.RubberbandGTK)
|
||||
|
||||
|
||||
class FigureManagerGTK3(_FigureManagerGTK):
|
||||
_toolbar2_class = NavigationToolbar2GTK3
|
||||
_toolmanager_toolbar_class = ToolbarGTK3
|
||||
|
||||
|
||||
@_BackendGTK.export
|
||||
class _BackendGTK3(_BackendGTK):
|
||||
FigureCanvas = FigureCanvasGTK3
|
||||
FigureManager = FigureManagerGTK3
|
||||
@ -0,0 +1,74 @@
|
||||
import numpy as np
|
||||
|
||||
from .. import cbook, transforms
|
||||
from . import backend_agg, backend_gtk3
|
||||
from .backend_gtk3 import GLib, Gtk, _BackendGTK3
|
||||
|
||||
import cairo # Presence of cairo is already checked by _backend_gtk.
|
||||
|
||||
|
||||
class FigureCanvasGTK3Agg(backend_agg.FigureCanvasAgg,
|
||||
backend_gtk3.FigureCanvasGTK3):
|
||||
def __init__(self, figure):
|
||||
super().__init__(figure=figure)
|
||||
self._bbox_queue = []
|
||||
|
||||
def on_draw_event(self, widget, ctx):
|
||||
if self._idle_draw_id:
|
||||
GLib.source_remove(self._idle_draw_id)
|
||||
self._idle_draw_id = 0
|
||||
self.draw()
|
||||
|
||||
scale = self.device_pixel_ratio
|
||||
allocation = self.get_allocation()
|
||||
w = allocation.width * scale
|
||||
h = allocation.height * scale
|
||||
|
||||
if not len(self._bbox_queue):
|
||||
Gtk.render_background(
|
||||
self.get_style_context(), ctx,
|
||||
allocation.x, allocation.y,
|
||||
allocation.width, allocation.height)
|
||||
bbox_queue = [transforms.Bbox([[0, 0], [w, h]])]
|
||||
else:
|
||||
bbox_queue = self._bbox_queue
|
||||
|
||||
for bbox in bbox_queue:
|
||||
x = int(bbox.x0)
|
||||
y = h - int(bbox.y1)
|
||||
width = int(bbox.x1) - int(bbox.x0)
|
||||
height = int(bbox.y1) - int(bbox.y0)
|
||||
|
||||
buf = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(
|
||||
np.asarray(self.copy_from_bbox(bbox)))
|
||||
image = cairo.ImageSurface.create_for_data(
|
||||
buf.ravel().data, cairo.FORMAT_ARGB32, width, height)
|
||||
image.set_device_scale(scale, scale)
|
||||
ctx.set_source_surface(image, x / scale, y / scale)
|
||||
ctx.paint()
|
||||
|
||||
if len(self._bbox_queue):
|
||||
self._bbox_queue = []
|
||||
|
||||
return False
|
||||
|
||||
def blit(self, bbox=None):
|
||||
# If bbox is None, blit the entire canvas to gtk. Otherwise
|
||||
# blit only the area defined by the bbox.
|
||||
if bbox is None:
|
||||
bbox = self.figure.bbox
|
||||
|
||||
scale = self.device_pixel_ratio
|
||||
allocation = self.get_allocation()
|
||||
x = int(bbox.x0 / scale)
|
||||
y = allocation.height - int(bbox.y1 / scale)
|
||||
width = (int(bbox.x1) - int(bbox.x0)) // scale
|
||||
height = (int(bbox.y1) - int(bbox.y0)) // scale
|
||||
|
||||
self._bbox_queue.append(bbox)
|
||||
self.queue_draw_area(x, y, width, height)
|
||||
|
||||
|
||||
@_BackendGTK3.export
|
||||
class _BackendGTK3Cairo(_BackendGTK3):
|
||||
FigureCanvas = FigureCanvasGTK3Agg
|
||||
@ -0,0 +1,35 @@
|
||||
from contextlib import nullcontext
|
||||
|
||||
from .backend_cairo import FigureCanvasCairo
|
||||
from .backend_gtk3 import GLib, Gtk, FigureCanvasGTK3, _BackendGTK3
|
||||
|
||||
|
||||
class FigureCanvasGTK3Cairo(FigureCanvasCairo, FigureCanvasGTK3):
|
||||
def on_draw_event(self, widget, ctx):
|
||||
if self._idle_draw_id:
|
||||
GLib.source_remove(self._idle_draw_id)
|
||||
self._idle_draw_id = 0
|
||||
self.draw()
|
||||
|
||||
with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
|
||||
else nullcontext()):
|
||||
allocation = self.get_allocation()
|
||||
# Render the background before scaling, as the allocated size here is in
|
||||
# logical pixels.
|
||||
Gtk.render_background(
|
||||
self.get_style_context(), ctx,
|
||||
0, 0, allocation.width, allocation.height)
|
||||
scale = self.device_pixel_ratio
|
||||
# Scale physical drawing to logical size.
|
||||
ctx.scale(1 / scale, 1 / scale)
|
||||
self._renderer.set_context(ctx)
|
||||
# Set renderer to physical size so it renders in full resolution.
|
||||
self._renderer.width = allocation.width * scale
|
||||
self._renderer.height = allocation.height * scale
|
||||
self._renderer.dpi = self.figure.dpi
|
||||
self.figure.draw(self._renderer)
|
||||
|
||||
|
||||
@_BackendGTK3.export
|
||||
class _BackendGTK3Cairo(_BackendGTK3):
|
||||
FigureCanvas = FigureCanvasGTK3Cairo
|
||||
@ -0,0 +1,595 @@
|
||||
import functools
|
||||
import io
|
||||
import os
|
||||
|
||||
import matplotlib as mpl
|
||||
from matplotlib import _api, backend_tools, cbook
|
||||
from matplotlib.backend_bases import (
|
||||
ToolContainerBase, KeyEvent, LocationEvent, MouseEvent, ResizeEvent,
|
||||
CloseEvent)
|
||||
|
||||
try:
|
||||
import gi
|
||||
except ImportError as err:
|
||||
raise ImportError("The GTK4 backends require PyGObject") from err
|
||||
|
||||
try:
|
||||
# :raises ValueError: If module/version is already loaded, already
|
||||
# required, or unavailable.
|
||||
gi.require_version("Gtk", "4.0")
|
||||
except ValueError as e:
|
||||
# in this case we want to re-raise as ImportError so the
|
||||
# auto-backend selection logic correctly skips.
|
||||
raise ImportError(e) from e
|
||||
|
||||
from gi.repository import Gio, GLib, Gtk, Gdk, GdkPixbuf
|
||||
from . import _backend_gtk
|
||||
from ._backend_gtk import ( # noqa: F401 # pylint: disable=W0611
|
||||
_BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
|
||||
TimerGTK as TimerGTK4,
|
||||
)
|
||||
|
||||
|
||||
class FigureCanvasGTK4(_FigureCanvasGTK, Gtk.DrawingArea):
|
||||
required_interactive_framework = "gtk4"
|
||||
supports_blit = False
|
||||
manager_class = _api.classproperty(lambda cls: FigureManagerGTK4)
|
||||
|
||||
def __init__(self, figure=None):
|
||||
super().__init__(figure=figure)
|
||||
|
||||
self.set_hexpand(True)
|
||||
self.set_vexpand(True)
|
||||
|
||||
self._idle_draw_id = 0
|
||||
self._rubberband_rect = None
|
||||
|
||||
self.set_draw_func(self._draw_func)
|
||||
self.connect('resize', self.resize_event)
|
||||
self.connect('notify::scale-factor', self._update_device_pixel_ratio)
|
||||
|
||||
click = Gtk.GestureClick()
|
||||
click.set_button(0) # All buttons.
|
||||
click.connect('pressed', self.button_press_event)
|
||||
click.connect('released', self.button_release_event)
|
||||
self.add_controller(click)
|
||||
|
||||
key = Gtk.EventControllerKey()
|
||||
key.connect('key-pressed', self.key_press_event)
|
||||
key.connect('key-released', self.key_release_event)
|
||||
self.add_controller(key)
|
||||
|
||||
motion = Gtk.EventControllerMotion()
|
||||
motion.connect('motion', self.motion_notify_event)
|
||||
motion.connect('enter', self.enter_notify_event)
|
||||
motion.connect('leave', self.leave_notify_event)
|
||||
self.add_controller(motion)
|
||||
|
||||
scroll = Gtk.EventControllerScroll.new(
|
||||
Gtk.EventControllerScrollFlags.VERTICAL)
|
||||
scroll.connect('scroll', self.scroll_event)
|
||||
self.add_controller(scroll)
|
||||
|
||||
self.set_focusable(True)
|
||||
|
||||
css = Gtk.CssProvider()
|
||||
style = '.matplotlib-canvas { background-color: white; }'
|
||||
if Gtk.check_version(4, 9, 3) is None:
|
||||
css.load_from_data(style, -1)
|
||||
else:
|
||||
css.load_from_data(style.encode('utf-8'))
|
||||
style_ctx = self.get_style_context()
|
||||
style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||
style_ctx.add_class("matplotlib-canvas")
|
||||
|
||||
def destroy(self):
|
||||
CloseEvent("close_event", self)._process()
|
||||
|
||||
def set_cursor(self, cursor):
|
||||
# docstring inherited
|
||||
self.set_cursor_from_name(_backend_gtk.mpl_to_gtk_cursor_name(cursor))
|
||||
|
||||
def _mpl_coords(self, xy=None):
|
||||
"""
|
||||
Convert the *xy* position of a GTK event, or of the current cursor
|
||||
position if *xy* is None, to Matplotlib coordinates.
|
||||
|
||||
GTK use logical pixels, but the figure is scaled to physical pixels for
|
||||
rendering. Transform to physical pixels so that all of the down-stream
|
||||
transforms work as expected.
|
||||
|
||||
Also, the origin is different and needs to be corrected.
|
||||
"""
|
||||
if xy is None:
|
||||
surface = self.get_native().get_surface()
|
||||
is_over, x, y, mask = surface.get_device_position(
|
||||
self.get_display().get_default_seat().get_pointer())
|
||||
else:
|
||||
x, y = xy
|
||||
x = x * self.device_pixel_ratio
|
||||
# flip y so y=0 is bottom of canvas
|
||||
y = self.figure.bbox.height - y * self.device_pixel_ratio
|
||||
return x, y
|
||||
|
||||
def scroll_event(self, controller, dx, dy):
|
||||
MouseEvent(
|
||||
"scroll_event", self, *self._mpl_coords(), step=dy,
|
||||
modifiers=self._mpl_modifiers(controller),
|
||||
)._process()
|
||||
return True
|
||||
|
||||
def button_press_event(self, controller, n_press, x, y):
|
||||
MouseEvent(
|
||||
"button_press_event", self, *self._mpl_coords((x, y)),
|
||||
controller.get_current_button(),
|
||||
modifiers=self._mpl_modifiers(controller),
|
||||
)._process()
|
||||
self.grab_focus()
|
||||
|
||||
def button_release_event(self, controller, n_press, x, y):
|
||||
MouseEvent(
|
||||
"button_release_event", self, *self._mpl_coords((x, y)),
|
||||
controller.get_current_button(),
|
||||
modifiers=self._mpl_modifiers(controller),
|
||||
)._process()
|
||||
|
||||
def key_press_event(self, controller, keyval, keycode, state):
|
||||
KeyEvent(
|
||||
"key_press_event", self, self._get_key(keyval, keycode, state),
|
||||
*self._mpl_coords(),
|
||||
)._process()
|
||||
return True
|
||||
|
||||
def key_release_event(self, controller, keyval, keycode, state):
|
||||
KeyEvent(
|
||||
"key_release_event", self, self._get_key(keyval, keycode, state),
|
||||
*self._mpl_coords(),
|
||||
)._process()
|
||||
return True
|
||||
|
||||
def motion_notify_event(self, controller, x, y):
|
||||
MouseEvent(
|
||||
"motion_notify_event", self, *self._mpl_coords((x, y)),
|
||||
modifiers=self._mpl_modifiers(controller),
|
||||
)._process()
|
||||
|
||||
def enter_notify_event(self, controller, x, y):
|
||||
LocationEvent(
|
||||
"figure_enter_event", self, *self._mpl_coords((x, y)),
|
||||
modifiers=self._mpl_modifiers(),
|
||||
)._process()
|
||||
|
||||
def leave_notify_event(self, controller):
|
||||
LocationEvent(
|
||||
"figure_leave_event", self, *self._mpl_coords(),
|
||||
modifiers=self._mpl_modifiers(),
|
||||
)._process()
|
||||
|
||||
def resize_event(self, area, width, height):
|
||||
self._update_device_pixel_ratio()
|
||||
dpi = self.figure.dpi
|
||||
winch = width * self.device_pixel_ratio / dpi
|
||||
hinch = height * self.device_pixel_ratio / dpi
|
||||
self.figure.set_size_inches(winch, hinch, forward=False)
|
||||
ResizeEvent("resize_event", self)._process()
|
||||
self.draw_idle()
|
||||
|
||||
def _mpl_modifiers(self, controller=None):
|
||||
if controller is None:
|
||||
surface = self.get_native().get_surface()
|
||||
is_over, x, y, event_state = surface.get_device_position(
|
||||
self.get_display().get_default_seat().get_pointer())
|
||||
else:
|
||||
event_state = controller.get_current_event_state()
|
||||
mod_table = [
|
||||
("ctrl", Gdk.ModifierType.CONTROL_MASK),
|
||||
("alt", Gdk.ModifierType.ALT_MASK),
|
||||
("shift", Gdk.ModifierType.SHIFT_MASK),
|
||||
("super", Gdk.ModifierType.SUPER_MASK),
|
||||
]
|
||||
return [name for name, mask in mod_table if event_state & mask]
|
||||
|
||||
def _get_key(self, keyval, keycode, state):
|
||||
unikey = chr(Gdk.keyval_to_unicode(keyval))
|
||||
key = cbook._unikey_or_keysym_to_mplkey(
|
||||
unikey,
|
||||
Gdk.keyval_name(keyval))
|
||||
modifiers = [
|
||||
("ctrl", Gdk.ModifierType.CONTROL_MASK, "control"),
|
||||
("alt", Gdk.ModifierType.ALT_MASK, "alt"),
|
||||
("shift", Gdk.ModifierType.SHIFT_MASK, "shift"),
|
||||
("super", Gdk.ModifierType.SUPER_MASK, "super"),
|
||||
]
|
||||
mods = [
|
||||
mod for mod, mask, mod_key in modifiers
|
||||
if (mod_key != key and state & mask
|
||||
and not (mod == "shift" and unikey.isprintable()))]
|
||||
return "+".join([*mods, key])
|
||||
|
||||
def _update_device_pixel_ratio(self, *args, **kwargs):
|
||||
# We need to be careful in cases with mixed resolution displays if
|
||||
# device_pixel_ratio changes.
|
||||
if self._set_device_pixel_ratio(self.get_scale_factor()):
|
||||
self.draw()
|
||||
|
||||
def _draw_rubberband(self, rect):
|
||||
self._rubberband_rect = rect
|
||||
# TODO: Only update the rubberband area.
|
||||
self.queue_draw()
|
||||
|
||||
def _draw_func(self, drawing_area, ctx, width, height):
|
||||
self.on_draw_event(self, ctx)
|
||||
self._post_draw(self, ctx)
|
||||
|
||||
def _post_draw(self, widget, ctx):
|
||||
if self._rubberband_rect is None:
|
||||
return
|
||||
|
||||
lw = 1
|
||||
dash = 3
|
||||
x0, y0, w, h = (dim / self.device_pixel_ratio
|
||||
for dim in self._rubberband_rect)
|
||||
x1 = x0 + w
|
||||
y1 = y0 + h
|
||||
|
||||
# Draw the lines from x0, y0 towards x1, y1 so that the
|
||||
# dashes don't "jump" when moving the zoom box.
|
||||
ctx.move_to(x0, y0)
|
||||
ctx.line_to(x0, y1)
|
||||
ctx.move_to(x0, y0)
|
||||
ctx.line_to(x1, y0)
|
||||
ctx.move_to(x0, y1)
|
||||
ctx.line_to(x1, y1)
|
||||
ctx.move_to(x1, y0)
|
||||
ctx.line_to(x1, y1)
|
||||
|
||||
ctx.set_antialias(1)
|
||||
ctx.set_line_width(lw)
|
||||
ctx.set_dash((dash, dash), 0)
|
||||
ctx.set_source_rgb(0, 0, 0)
|
||||
ctx.stroke_preserve()
|
||||
|
||||
ctx.set_dash((dash, dash), dash)
|
||||
ctx.set_source_rgb(1, 1, 1)
|
||||
ctx.stroke()
|
||||
|
||||
def on_draw_event(self, widget, ctx):
|
||||
# to be overwritten by GTK4Agg or GTK4Cairo
|
||||
pass
|
||||
|
||||
def draw(self):
|
||||
# docstring inherited
|
||||
if self.is_drawable():
|
||||
self.queue_draw()
|
||||
|
||||
def draw_idle(self):
|
||||
# docstring inherited
|
||||
if self._idle_draw_id != 0:
|
||||
return
|
||||
def idle_draw(*args):
|
||||
try:
|
||||
self.draw()
|
||||
finally:
|
||||
self._idle_draw_id = 0
|
||||
return False
|
||||
self._idle_draw_id = GLib.idle_add(idle_draw)
|
||||
|
||||
def flush_events(self):
|
||||
# docstring inherited
|
||||
context = GLib.MainContext.default()
|
||||
while context.pending():
|
||||
context.iteration(True)
|
||||
|
||||
|
||||
class NavigationToolbar2GTK4(_NavigationToolbar2GTK, Gtk.Box):
|
||||
def __init__(self, canvas):
|
||||
Gtk.Box.__init__(self)
|
||||
|
||||
self.add_css_class('toolbar')
|
||||
|
||||
self._gtk_ids = {}
|
||||
for text, tooltip_text, image_file, callback in self.toolitems:
|
||||
if text is None:
|
||||
self.append(Gtk.Separator())
|
||||
continue
|
||||
image = Gtk.Image.new_from_gicon(
|
||||
Gio.Icon.new_for_string(
|
||||
str(cbook._get_data_path('images',
|
||||
f'{image_file}-symbolic.svg'))))
|
||||
self._gtk_ids[text] = button = (
|
||||
Gtk.ToggleButton() if callback in ['zoom', 'pan'] else
|
||||
Gtk.Button())
|
||||
button.set_child(image)
|
||||
button.add_css_class('flat')
|
||||
button.add_css_class('image-button')
|
||||
# Save the handler id, so that we can block it as needed.
|
||||
button._signal_handler = button.connect(
|
||||
'clicked', getattr(self, callback))
|
||||
button.set_tooltip_text(tooltip_text)
|
||||
self.append(button)
|
||||
|
||||
# This filler item ensures the toolbar is always at least two text
|
||||
# lines high. Otherwise the canvas gets redrawn as the mouse hovers
|
||||
# over images because those use two-line messages which resize the
|
||||
# toolbar.
|
||||
label = Gtk.Label()
|
||||
label.set_markup(
|
||||
'<small>\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}</small>')
|
||||
label.set_hexpand(True) # Push real message to the right.
|
||||
self.append(label)
|
||||
|
||||
self.message = Gtk.Label()
|
||||
self.message.set_justify(Gtk.Justification.RIGHT)
|
||||
self.append(self.message)
|
||||
|
||||
_NavigationToolbar2GTK.__init__(self, canvas)
|
||||
|
||||
def save_figure(self, *args):
|
||||
dialog = Gtk.FileChooserNative(
|
||||
title='Save the figure',
|
||||
transient_for=self.canvas.get_root(),
|
||||
action=Gtk.FileChooserAction.SAVE,
|
||||
modal=True)
|
||||
self._save_dialog = dialog # Must keep a reference.
|
||||
|
||||
ff = Gtk.FileFilter()
|
||||
ff.set_name('All files')
|
||||
ff.add_pattern('*')
|
||||
dialog.add_filter(ff)
|
||||
dialog.set_filter(ff)
|
||||
|
||||
formats = []
|
||||
default_format = None
|
||||
for i, (name, fmts) in enumerate(
|
||||
self.canvas.get_supported_filetypes_grouped().items()):
|
||||
ff = Gtk.FileFilter()
|
||||
ff.set_name(name)
|
||||
for fmt in fmts:
|
||||
ff.add_pattern(f'*.{fmt}')
|
||||
dialog.add_filter(ff)
|
||||
formats.append(name)
|
||||
if self.canvas.get_default_filetype() in fmts:
|
||||
default_format = i
|
||||
# Setting the choice doesn't always work, so make sure the default
|
||||
# format is first.
|
||||
formats = [formats[default_format], *formats[:default_format],
|
||||
*formats[default_format+1:]]
|
||||
dialog.add_choice('format', 'File format', formats, formats)
|
||||
dialog.set_choice('format', formats[0])
|
||||
|
||||
dialog.set_current_folder(Gio.File.new_for_path(
|
||||
os.path.expanduser(mpl.rcParams['savefig.directory'])))
|
||||
dialog.set_current_name(self.canvas.get_default_filename())
|
||||
|
||||
@functools.partial(dialog.connect, 'response')
|
||||
def on_response(dialog, response):
|
||||
file = dialog.get_file()
|
||||
fmt = dialog.get_choice('format')
|
||||
fmt = self.canvas.get_supported_filetypes_grouped()[fmt][0]
|
||||
dialog.destroy()
|
||||
self._save_dialog = None
|
||||
if response != Gtk.ResponseType.ACCEPT:
|
||||
return
|
||||
# Save dir for next time, unless empty str (which means use cwd).
|
||||
if mpl.rcParams['savefig.directory']:
|
||||
parent = file.get_parent()
|
||||
mpl.rcParams['savefig.directory'] = parent.get_path()
|
||||
try:
|
||||
self.canvas.figure.savefig(file.get_path(), format=fmt)
|
||||
except Exception as e:
|
||||
msg = Gtk.MessageDialog(
|
||||
transient_for=self.canvas.get_root(),
|
||||
message_type=Gtk.MessageType.ERROR,
|
||||
buttons=Gtk.ButtonsType.OK, modal=True,
|
||||
text=str(e))
|
||||
msg.show()
|
||||
|
||||
dialog.show()
|
||||
|
||||
|
||||
class ToolbarGTK4(ToolContainerBase, Gtk.Box):
|
||||
_icon_extension = '-symbolic.svg'
|
||||
|
||||
def __init__(self, toolmanager):
|
||||
ToolContainerBase.__init__(self, toolmanager)
|
||||
Gtk.Box.__init__(self)
|
||||
self.set_property('orientation', Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
# Tool items are created later, but must appear before the message.
|
||||
self._tool_box = Gtk.Box()
|
||||
self.append(self._tool_box)
|
||||
self._groups = {}
|
||||
self._toolitems = {}
|
||||
|
||||
# This filler item ensures the toolbar is always at least two text
|
||||
# lines high. Otherwise the canvas gets redrawn as the mouse hovers
|
||||
# over images because those use two-line messages which resize the
|
||||
# toolbar.
|
||||
label = Gtk.Label()
|
||||
label.set_markup(
|
||||
'<small>\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}</small>')
|
||||
label.set_hexpand(True) # Push real message to the right.
|
||||
self.append(label)
|
||||
|
||||
self._message = Gtk.Label()
|
||||
self._message.set_justify(Gtk.Justification.RIGHT)
|
||||
self.append(self._message)
|
||||
|
||||
def add_toolitem(self, name, group, position, image_file, description,
|
||||
toggle):
|
||||
if toggle:
|
||||
button = Gtk.ToggleButton()
|
||||
else:
|
||||
button = Gtk.Button()
|
||||
button.set_label(name)
|
||||
button.add_css_class('flat')
|
||||
|
||||
if image_file is not None:
|
||||
image = Gtk.Image.new_from_gicon(
|
||||
Gio.Icon.new_for_string(image_file))
|
||||
button.set_child(image)
|
||||
button.add_css_class('image-button')
|
||||
|
||||
if position is None:
|
||||
position = -1
|
||||
|
||||
self._add_button(button, group, position)
|
||||
signal = button.connect('clicked', self._call_tool, name)
|
||||
button.set_tooltip_text(description)
|
||||
self._toolitems.setdefault(name, [])
|
||||
self._toolitems[name].append((button, signal))
|
||||
|
||||
def _find_child_at_position(self, group, position):
|
||||
children = [None]
|
||||
child = self._groups[group].get_first_child()
|
||||
while child is not None:
|
||||
children.append(child)
|
||||
child = child.get_next_sibling()
|
||||
return children[position]
|
||||
|
||||
def _add_button(self, button, group, position):
|
||||
if group not in self._groups:
|
||||
if self._groups:
|
||||
self._add_separator()
|
||||
group_box = Gtk.Box()
|
||||
self._tool_box.append(group_box)
|
||||
self._groups[group] = group_box
|
||||
self._groups[group].insert_child_after(
|
||||
button, self._find_child_at_position(group, position))
|
||||
|
||||
def _call_tool(self, btn, name):
|
||||
self.trigger_tool(name)
|
||||
|
||||
def toggle_toolitem(self, name, toggled):
|
||||
if name not in self._toolitems:
|
||||
return
|
||||
for toolitem, signal in self._toolitems[name]:
|
||||
toolitem.handler_block(signal)
|
||||
toolitem.set_active(toggled)
|
||||
toolitem.handler_unblock(signal)
|
||||
|
||||
def remove_toolitem(self, name):
|
||||
for toolitem, _signal in self._toolitems.pop(name, []):
|
||||
for group in self._groups:
|
||||
if toolitem in self._groups[group]:
|
||||
self._groups[group].remove(toolitem)
|
||||
|
||||
def _add_separator(self):
|
||||
sep = Gtk.Separator()
|
||||
sep.set_property("orientation", Gtk.Orientation.VERTICAL)
|
||||
self._tool_box.append(sep)
|
||||
|
||||
def set_message(self, s):
|
||||
self._message.set_label(s)
|
||||
|
||||
|
||||
@backend_tools._register_tool_class(FigureCanvasGTK4)
|
||||
class SaveFigureGTK4(backend_tools.SaveFigureBase):
|
||||
def trigger(self, *args, **kwargs):
|
||||
NavigationToolbar2GTK4.save_figure(
|
||||
self._make_classic_style_pseudo_toolbar())
|
||||
|
||||
|
||||
@backend_tools._register_tool_class(FigureCanvasGTK4)
|
||||
class HelpGTK4(backend_tools.ToolHelpBase):
|
||||
def _normalize_shortcut(self, key):
|
||||
"""
|
||||
Convert Matplotlib key presses to GTK+ accelerator identifiers.
|
||||
|
||||
Related to `FigureCanvasGTK4._get_key`.
|
||||
"""
|
||||
special = {
|
||||
'backspace': 'BackSpace',
|
||||
'pagedown': 'Page_Down',
|
||||
'pageup': 'Page_Up',
|
||||
'scroll_lock': 'Scroll_Lock',
|
||||
}
|
||||
|
||||
parts = key.split('+')
|
||||
mods = ['<' + mod + '>' for mod in parts[:-1]]
|
||||
key = parts[-1]
|
||||
|
||||
if key in special:
|
||||
key = special[key]
|
||||
elif len(key) > 1:
|
||||
key = key.capitalize()
|
||||
elif key.isupper():
|
||||
mods += ['<shift>']
|
||||
|
||||
return ''.join(mods) + key
|
||||
|
||||
def _is_valid_shortcut(self, key):
|
||||
"""
|
||||
Check for a valid shortcut to be displayed.
|
||||
|
||||
- GTK will never send 'cmd+' (see `FigureCanvasGTK4._get_key`).
|
||||
- The shortcut window only shows keyboard shortcuts, not mouse buttons.
|
||||
"""
|
||||
return 'cmd+' not in key and not key.startswith('MouseButton.')
|
||||
|
||||
def trigger(self, *args):
|
||||
section = Gtk.ShortcutsSection()
|
||||
|
||||
for name, tool in sorted(self.toolmanager.tools.items()):
|
||||
if not tool.description:
|
||||
continue
|
||||
|
||||
# Putting everything in a separate group allows GTK to
|
||||
# automatically split them into separate columns/pages, which is
|
||||
# useful because we have lots of shortcuts, some with many keys
|
||||
# that are very wide.
|
||||
group = Gtk.ShortcutsGroup()
|
||||
section.append(group)
|
||||
# A hack to remove the title since we have no group naming.
|
||||
child = group.get_first_child()
|
||||
while child is not None:
|
||||
child.set_visible(False)
|
||||
child = child.get_next_sibling()
|
||||
|
||||
shortcut = Gtk.ShortcutsShortcut(
|
||||
accelerator=' '.join(
|
||||
self._normalize_shortcut(key)
|
||||
for key in self.toolmanager.get_tool_keymap(name)
|
||||
if self._is_valid_shortcut(key)),
|
||||
title=tool.name,
|
||||
subtitle=tool.description)
|
||||
group.append(shortcut)
|
||||
|
||||
window = Gtk.ShortcutsWindow(
|
||||
title='Help',
|
||||
modal=True,
|
||||
transient_for=self._figure.canvas.get_root())
|
||||
window.set_child(section)
|
||||
|
||||
window.show()
|
||||
|
||||
|
||||
@backend_tools._register_tool_class(FigureCanvasGTK4)
|
||||
class ToolCopyToClipboardGTK4(backend_tools.ToolCopyToClipboardBase):
|
||||
def trigger(self, *args, **kwargs):
|
||||
with io.BytesIO() as f:
|
||||
self.canvas.print_rgba(f)
|
||||
w, h = self.canvas.get_width_height()
|
||||
pb = GdkPixbuf.Pixbuf.new_from_data(f.getbuffer(),
|
||||
GdkPixbuf.Colorspace.RGB, True,
|
||||
8, w, h, w*4)
|
||||
clipboard = self.canvas.get_clipboard()
|
||||
clipboard.set(pb)
|
||||
|
||||
|
||||
backend_tools._register_tool_class(
|
||||
FigureCanvasGTK4, _backend_gtk.ConfigureSubplotsGTK)
|
||||
backend_tools._register_tool_class(
|
||||
FigureCanvasGTK4, _backend_gtk.RubberbandGTK)
|
||||
Toolbar = ToolbarGTK4
|
||||
|
||||
|
||||
class FigureManagerGTK4(_FigureManagerGTK):
|
||||
_toolbar2_class = NavigationToolbar2GTK4
|
||||
_toolmanager_toolbar_class = ToolbarGTK4
|
||||
|
||||
|
||||
@_BackendGTK.export
|
||||
class _BackendGTK4(_BackendGTK):
|
||||
FigureCanvas = FigureCanvasGTK4
|
||||
FigureManager = FigureManagerGTK4
|
||||
@ -0,0 +1,41 @@
|
||||
import numpy as np
|
||||
|
||||
from .. import cbook
|
||||
from . import backend_agg, backend_gtk4
|
||||
from .backend_gtk4 import GLib, Gtk, _BackendGTK4
|
||||
|
||||
import cairo # Presence of cairo is already checked by _backend_gtk.
|
||||
|
||||
|
||||
class FigureCanvasGTK4Agg(backend_agg.FigureCanvasAgg,
|
||||
backend_gtk4.FigureCanvasGTK4):
|
||||
|
||||
def on_draw_event(self, widget, ctx):
|
||||
if self._idle_draw_id:
|
||||
GLib.source_remove(self._idle_draw_id)
|
||||
self._idle_draw_id = 0
|
||||
self.draw()
|
||||
|
||||
scale = self.device_pixel_ratio
|
||||
allocation = self.get_allocation()
|
||||
|
||||
Gtk.render_background(
|
||||
self.get_style_context(), ctx,
|
||||
allocation.x, allocation.y,
|
||||
allocation.width, allocation.height)
|
||||
|
||||
buf = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(
|
||||
np.asarray(self.get_renderer().buffer_rgba()))
|
||||
height, width, _ = buf.shape
|
||||
image = cairo.ImageSurface.create_for_data(
|
||||
buf.ravel().data, cairo.FORMAT_ARGB32, width, height)
|
||||
image.set_device_scale(scale, scale)
|
||||
ctx.set_source_surface(image, 0, 0)
|
||||
ctx.paint()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@_BackendGTK4.export
|
||||
class _BackendGTK4Agg(_BackendGTK4):
|
||||
FigureCanvas = FigureCanvasGTK4Agg
|
||||
@ -0,0 +1,32 @@
|
||||
from contextlib import nullcontext
|
||||
|
||||
from .backend_cairo import FigureCanvasCairo
|
||||
from .backend_gtk4 import GLib, Gtk, FigureCanvasGTK4, _BackendGTK4
|
||||
|
||||
|
||||
class FigureCanvasGTK4Cairo(FigureCanvasCairo, FigureCanvasGTK4):
|
||||
def _set_device_pixel_ratio(self, ratio):
|
||||
# Cairo in GTK4 always uses logical pixels, so we don't need to do anything for
|
||||
# changes to the device pixel ratio.
|
||||
return False
|
||||
|
||||
def on_draw_event(self, widget, ctx):
|
||||
if self._idle_draw_id:
|
||||
GLib.source_remove(self._idle_draw_id)
|
||||
self._idle_draw_id = 0
|
||||
self.draw()
|
||||
|
||||
with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
|
||||
else nullcontext()):
|
||||
self._renderer.set_context(ctx)
|
||||
allocation = self.get_allocation()
|
||||
Gtk.render_background(
|
||||
self.get_style_context(), ctx,
|
||||
allocation.x, allocation.y,
|
||||
allocation.width, allocation.height)
|
||||
self.figure.draw(self._renderer)
|
||||
|
||||
|
||||
@_BackendGTK4.export
|
||||
class _BackendGTK4Cairo(_BackendGTK4):
|
||||
FigureCanvas = FigureCanvasGTK4Cairo
|
||||
@ -0,0 +1,195 @@
|
||||
import os
|
||||
|
||||
import matplotlib as mpl
|
||||
from matplotlib import _api, cbook
|
||||
from matplotlib._pylab_helpers import Gcf
|
||||
from . import _macosx
|
||||
from .backend_agg import FigureCanvasAgg
|
||||
from matplotlib.backend_bases import (
|
||||
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
|
||||
ResizeEvent, TimerBase, _allow_interrupt)
|
||||
|
||||
|
||||
class TimerMac(_macosx.Timer, TimerBase):
|
||||
"""Subclass of `.TimerBase` using CFRunLoop timer events."""
|
||||
# completely implemented at the C-level (in _macosx.Timer)
|
||||
|
||||
|
||||
def _allow_interrupt_macos():
|
||||
"""A context manager that allows terminating a plot by sending a SIGINT."""
|
||||
return _allow_interrupt(
|
||||
lambda rsock: _macosx.wake_on_fd_write(rsock.fileno()), _macosx.stop)
|
||||
|
||||
|
||||
class FigureCanvasMac(FigureCanvasAgg, _macosx.FigureCanvas, FigureCanvasBase):
|
||||
# docstring inherited
|
||||
|
||||
# Ideally this class would be `class FCMacAgg(FCAgg, FCMac)`
|
||||
# (FC=FigureCanvas) where FCMac would be an ObjC-implemented mac-specific
|
||||
# class also inheriting from FCBase (this is the approach with other GUI
|
||||
# toolkits). However, writing an extension type inheriting from a Python
|
||||
# base class is slightly tricky (the extension type must be a heap type),
|
||||
# and we can just as well lift the FCBase base up one level, keeping it *at
|
||||
# the end* to have the right method resolution order.
|
||||
|
||||
# Events such as button presses, mouse movements, and key presses are
|
||||
# handled in C and events (MouseEvent, etc.) are triggered from there.
|
||||
|
||||
required_interactive_framework = "macosx"
|
||||
_timer_cls = TimerMac
|
||||
manager_class = _api.classproperty(lambda cls: FigureManagerMac)
|
||||
|
||||
def __init__(self, figure):
|
||||
super().__init__(figure=figure)
|
||||
self._draw_pending = False
|
||||
self._is_drawing = False
|
||||
# Keep track of the timers that are alive
|
||||
self._timers = set()
|
||||
|
||||
def draw(self):
|
||||
"""Render the figure and update the macosx canvas."""
|
||||
# The renderer draw is done here; delaying causes problems with code
|
||||
# that uses the result of the draw() to update plot elements.
|
||||
if self._is_drawing:
|
||||
return
|
||||
with cbook._setattr_cm(self, _is_drawing=True):
|
||||
super().draw()
|
||||
self.update()
|
||||
|
||||
def draw_idle(self):
|
||||
# docstring inherited
|
||||
if not (getattr(self, '_draw_pending', False) or
|
||||
getattr(self, '_is_drawing', False)):
|
||||
self._draw_pending = True
|
||||
# Add a singleshot timer to the eventloop that will call back
|
||||
# into the Python method _draw_idle to take care of the draw
|
||||
self._single_shot_timer(self._draw_idle)
|
||||
|
||||
def _single_shot_timer(self, callback):
|
||||
"""Add a single shot timer with the given callback"""
|
||||
def callback_func(callback, timer):
|
||||
callback()
|
||||
self._timers.remove(timer)
|
||||
timer = self.new_timer(interval=0)
|
||||
timer.single_shot = True
|
||||
timer.add_callback(callback_func, callback, timer)
|
||||
self._timers.add(timer)
|
||||
timer.start()
|
||||
|
||||
def _draw_idle(self):
|
||||
"""
|
||||
Draw method for singleshot timer
|
||||
|
||||
This draw method can be added to a singleshot timer, which can
|
||||
accumulate draws while the eventloop is spinning. This method will
|
||||
then only draw the first time and short-circuit the others.
|
||||
"""
|
||||
with self._idle_draw_cntx():
|
||||
if not self._draw_pending:
|
||||
# Short-circuit because our draw request has already been
|
||||
# taken care of
|
||||
return
|
||||
self._draw_pending = False
|
||||
self.draw()
|
||||
|
||||
def blit(self, bbox=None):
|
||||
# docstring inherited
|
||||
super().blit(bbox)
|
||||
self.update()
|
||||
|
||||
def resize(self, width, height):
|
||||
# Size from macOS is logical pixels, dpi is physical.
|
||||
scale = self.figure.dpi / self.device_pixel_ratio
|
||||
width /= scale
|
||||
height /= scale
|
||||
self.figure.set_size_inches(width, height, forward=False)
|
||||
ResizeEvent("resize_event", self)._process()
|
||||
self.draw_idle()
|
||||
|
||||
def start_event_loop(self, timeout=0):
|
||||
# docstring inherited
|
||||
# Set up a SIGINT handler to allow terminating a plot via CTRL-C.
|
||||
with _allow_interrupt_macos():
|
||||
self._start_event_loop(timeout=timeout) # Forward to ObjC implementation.
|
||||
|
||||
|
||||
class NavigationToolbar2Mac(_macosx.NavigationToolbar2, NavigationToolbar2):
|
||||
|
||||
def __init__(self, canvas):
|
||||
data_path = cbook._get_data_path('images')
|
||||
_, tooltips, image_names, _ = zip(*NavigationToolbar2.toolitems)
|
||||
_macosx.NavigationToolbar2.__init__(
|
||||
self, canvas,
|
||||
tuple(str(data_path / image_name) + ".pdf"
|
||||
for image_name in image_names if image_name is not None),
|
||||
tuple(tooltip for tooltip in tooltips if tooltip is not None))
|
||||
NavigationToolbar2.__init__(self, canvas)
|
||||
|
||||
def draw_rubberband(self, event, x0, y0, x1, y1):
|
||||
self.canvas.set_rubberband(int(x0), int(y0), int(x1), int(y1))
|
||||
|
||||
def remove_rubberband(self):
|
||||
self.canvas.remove_rubberband()
|
||||
|
||||
def save_figure(self, *args):
|
||||
directory = os.path.expanduser(mpl.rcParams['savefig.directory'])
|
||||
filename = _macosx.choose_save_file('Save the figure',
|
||||
directory,
|
||||
self.canvas.get_default_filename())
|
||||
if filename is None: # Cancel
|
||||
return
|
||||
# Save dir for next time, unless empty str (which means use cwd).
|
||||
if mpl.rcParams['savefig.directory']:
|
||||
mpl.rcParams['savefig.directory'] = os.path.dirname(filename)
|
||||
self.canvas.figure.savefig(filename)
|
||||
|
||||
|
||||
class FigureManagerMac(_macosx.FigureManager, FigureManagerBase):
|
||||
_toolbar2_class = NavigationToolbar2Mac
|
||||
|
||||
def __init__(self, canvas, num):
|
||||
self._shown = False
|
||||
_macosx.FigureManager.__init__(self, canvas)
|
||||
icon_path = str(cbook._get_data_path('images/matplotlib.pdf'))
|
||||
_macosx.FigureManager.set_icon(icon_path)
|
||||
FigureManagerBase.__init__(self, canvas, num)
|
||||
self._set_window_mode(mpl.rcParams["macosx.window_mode"])
|
||||
if self.toolbar is not None:
|
||||
self.toolbar.update()
|
||||
if mpl.is_interactive():
|
||||
self.show()
|
||||
self.canvas.draw_idle()
|
||||
|
||||
def _close_button_pressed(self):
|
||||
Gcf.destroy(self)
|
||||
self.canvas.flush_events()
|
||||
|
||||
def destroy(self):
|
||||
# We need to clear any pending timers that never fired, otherwise
|
||||
# we get a memory leak from the timer callbacks holding a reference
|
||||
while self.canvas._timers:
|
||||
timer = self.canvas._timers.pop()
|
||||
timer.stop()
|
||||
super().destroy()
|
||||
|
||||
@classmethod
|
||||
def start_main_loop(cls):
|
||||
# Set up a SIGINT handler to allow terminating a plot via CTRL-C.
|
||||
with _allow_interrupt_macos():
|
||||
_macosx.show()
|
||||
|
||||
def show(self):
|
||||
if self.canvas.figure.stale:
|
||||
self.canvas.draw_idle()
|
||||
if not self._shown:
|
||||
self._show()
|
||||
self._shown = True
|
||||
if mpl.rcParams["figure.raise_window"]:
|
||||
self._raise()
|
||||
|
||||
|
||||
@_Backend.export
|
||||
class _BackendMac(_Backend):
|
||||
FigureCanvas = FigureCanvasMac
|
||||
FigureManager = FigureManagerMac
|
||||
mainloop = FigureManagerMac.start_main_loop
|
||||
@ -0,0 +1,119 @@
|
||||
import numpy as np
|
||||
|
||||
from matplotlib import cbook
|
||||
from .backend_agg import RendererAgg
|
||||
from matplotlib._tight_bbox import process_figure_for_rasterizing
|
||||
|
||||
|
||||
class MixedModeRenderer:
|
||||
"""
|
||||
A helper class to implement a renderer that switches between
|
||||
vector and raster drawing. An example may be a PDF writer, where
|
||||
most things are drawn with PDF vector commands, but some very
|
||||
complex objects, such as quad meshes, are rasterised and then
|
||||
output as images.
|
||||
"""
|
||||
def __init__(self, figure, width, height, dpi, vector_renderer,
|
||||
raster_renderer_class=None,
|
||||
bbox_inches_restore=None):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
figure : `~matplotlib.figure.Figure`
|
||||
The figure instance.
|
||||
width : scalar
|
||||
The width of the canvas in logical units
|
||||
height : scalar
|
||||
The height of the canvas in logical units
|
||||
dpi : float
|
||||
The dpi of the canvas
|
||||
vector_renderer : `~matplotlib.backend_bases.RendererBase`
|
||||
An instance of a subclass of
|
||||
`~matplotlib.backend_bases.RendererBase` that will be used for the
|
||||
vector drawing.
|
||||
raster_renderer_class : `~matplotlib.backend_bases.RendererBase`
|
||||
The renderer class to use for the raster drawing. If not provided,
|
||||
this will use the Agg backend (which is currently the only viable
|
||||
option anyway.)
|
||||
|
||||
"""
|
||||
if raster_renderer_class is None:
|
||||
raster_renderer_class = RendererAgg
|
||||
|
||||
self._raster_renderer_class = raster_renderer_class
|
||||
self._width = width
|
||||
self._height = height
|
||||
self.dpi = dpi
|
||||
|
||||
self._vector_renderer = vector_renderer
|
||||
|
||||
self._raster_renderer = None
|
||||
|
||||
# A reference to the figure is needed as we need to change
|
||||
# the figure dpi before and after the rasterization. Although
|
||||
# this looks ugly, I couldn't find a better solution. -JJL
|
||||
self.figure = figure
|
||||
self._figdpi = figure.dpi
|
||||
|
||||
self._bbox_inches_restore = bbox_inches_restore
|
||||
|
||||
self._renderer = vector_renderer
|
||||
|
||||
def __getattr__(self, attr):
|
||||
# Proxy everything that hasn't been overridden to the base
|
||||
# renderer. Things that *are* overridden can call methods
|
||||
# on self._renderer directly, but must not cache/store
|
||||
# methods (because things like RendererAgg change their
|
||||
# methods on the fly in order to optimise proxying down
|
||||
# to the underlying C implementation).
|
||||
return getattr(self._renderer, attr)
|
||||
|
||||
def start_rasterizing(self):
|
||||
"""
|
||||
Enter "raster" mode. All subsequent drawing commands (until
|
||||
`stop_rasterizing` is called) will be drawn with the raster backend.
|
||||
"""
|
||||
# change the dpi of the figure temporarily.
|
||||
self.figure.dpi = self.dpi
|
||||
if self._bbox_inches_restore: # when tight bbox is used
|
||||
r = process_figure_for_rasterizing(self.figure,
|
||||
self._bbox_inches_restore)
|
||||
self._bbox_inches_restore = r
|
||||
|
||||
self._raster_renderer = self._raster_renderer_class(
|
||||
self._width*self.dpi, self._height*self.dpi, self.dpi)
|
||||
self._renderer = self._raster_renderer
|
||||
|
||||
def stop_rasterizing(self):
|
||||
"""
|
||||
Exit "raster" mode. All of the drawing that was done since
|
||||
the last `start_rasterizing` call will be copied to the
|
||||
vector backend by calling draw_image.
|
||||
"""
|
||||
|
||||
self._renderer = self._vector_renderer
|
||||
|
||||
height = self._height * self.dpi
|
||||
img = np.asarray(self._raster_renderer.buffer_rgba())
|
||||
slice_y, slice_x = cbook._get_nonzero_slices(img[..., 3])
|
||||
cropped_img = img[slice_y, slice_x]
|
||||
if cropped_img.size:
|
||||
gc = self._renderer.new_gc()
|
||||
# TODO: If the mixedmode resolution differs from the figure's
|
||||
# dpi, the image must be scaled (dpi->_figdpi). Not all
|
||||
# backends support this.
|
||||
self._renderer.draw_image(
|
||||
gc,
|
||||
slice_x.start * self._figdpi / self.dpi,
|
||||
(height - slice_y.stop) * self._figdpi / self.dpi,
|
||||
cropped_img[::-1])
|
||||
self._raster_renderer = None
|
||||
|
||||
# restore the figure dpi.
|
||||
self.figure.dpi = self._figdpi
|
||||
|
||||
if self._bbox_inches_restore: # when tight bbox is used
|
||||
r = process_figure_for_rasterizing(self.figure,
|
||||
self._bbox_inches_restore,
|
||||
self._figdpi)
|
||||
self._bbox_inches_restore = r
|
||||
@ -0,0 +1,243 @@
|
||||
"""Interactive figures in the IPython notebook."""
|
||||
# Note: There is a notebook in
|
||||
# lib/matplotlib/backends/web_backend/nbagg_uat.ipynb to help verify
|
||||
# that changes made maintain expected behaviour.
|
||||
|
||||
from base64 import b64encode
|
||||
import io
|
||||
import json
|
||||
import pathlib
|
||||
import uuid
|
||||
|
||||
from ipykernel.comm import Comm
|
||||
from IPython.display import display, Javascript, HTML
|
||||
|
||||
from matplotlib import is_interactive
|
||||
from matplotlib._pylab_helpers import Gcf
|
||||
from matplotlib.backend_bases import _Backend, CloseEvent, NavigationToolbar2
|
||||
from .backend_webagg_core import (
|
||||
FigureCanvasWebAggCore, FigureManagerWebAgg, NavigationToolbar2WebAgg)
|
||||
from .backend_webagg_core import ( # noqa: F401 # pylint: disable=W0611
|
||||
TimerTornado, TimerAsyncio)
|
||||
|
||||
|
||||
def connection_info():
|
||||
"""
|
||||
Return a string showing the figure and connection status for the backend.
|
||||
|
||||
This is intended as a diagnostic tool, and not for general use.
|
||||
"""
|
||||
result = [
|
||||
'{fig} - {socket}'.format(
|
||||
fig=(manager.canvas.figure.get_label()
|
||||
or f"Figure {manager.num}"),
|
||||
socket=manager.web_sockets)
|
||||
for manager in Gcf.get_all_fig_managers()
|
||||
]
|
||||
if not is_interactive():
|
||||
result.append(f'Figures pending show: {len(Gcf.figs)}')
|
||||
return '\n'.join(result)
|
||||
|
||||
|
||||
_FONT_AWESOME_CLASSES = { # font-awesome 4 names
|
||||
'home': 'fa fa-home',
|
||||
'back': 'fa fa-arrow-left',
|
||||
'forward': 'fa fa-arrow-right',
|
||||
'zoom_to_rect': 'fa fa-square-o',
|
||||
'move': 'fa fa-arrows',
|
||||
'download': 'fa fa-floppy-o',
|
||||
None: None
|
||||
}
|
||||
|
||||
|
||||
class NavigationIPy(NavigationToolbar2WebAgg):
|
||||
|
||||
# Use the standard toolbar items + download button
|
||||
toolitems = [(text, tooltip_text,
|
||||
_FONT_AWESOME_CLASSES[image_file], name_of_method)
|
||||
for text, tooltip_text, image_file, name_of_method
|
||||
in (NavigationToolbar2.toolitems +
|
||||
(('Download', 'Download plot', 'download', 'download'),))
|
||||
if image_file in _FONT_AWESOME_CLASSES]
|
||||
|
||||
|
||||
class FigureManagerNbAgg(FigureManagerWebAgg):
|
||||
_toolbar2_class = ToolbarCls = NavigationIPy
|
||||
|
||||
def __init__(self, canvas, num):
|
||||
self._shown = False
|
||||
super().__init__(canvas, num)
|
||||
|
||||
@classmethod
|
||||
def create_with_canvas(cls, canvas_class, figure, num):
|
||||
canvas = canvas_class(figure)
|
||||
manager = cls(canvas, num)
|
||||
if is_interactive():
|
||||
manager.show()
|
||||
canvas.draw_idle()
|
||||
|
||||
def destroy(event):
|
||||
canvas.mpl_disconnect(cid)
|
||||
Gcf.destroy(manager)
|
||||
|
||||
cid = canvas.mpl_connect('close_event', destroy)
|
||||
return manager
|
||||
|
||||
def display_js(self):
|
||||
# XXX How to do this just once? It has to deal with multiple
|
||||
# browser instances using the same kernel (require.js - but the
|
||||
# file isn't static?).
|
||||
display(Javascript(FigureManagerNbAgg.get_javascript()))
|
||||
|
||||
def show(self):
|
||||
if not self._shown:
|
||||
self.display_js()
|
||||
self._create_comm()
|
||||
else:
|
||||
self.canvas.draw_idle()
|
||||
self._shown = True
|
||||
# plt.figure adds an event which makes the figure in focus the active
|
||||
# one. Disable this behaviour, as it results in figures being put as
|
||||
# the active figure after they have been shown, even in non-interactive
|
||||
# mode.
|
||||
if hasattr(self, '_cidgcf'):
|
||||
self.canvas.mpl_disconnect(self._cidgcf)
|
||||
if not is_interactive():
|
||||
from matplotlib._pylab_helpers import Gcf
|
||||
Gcf.figs.pop(self.num, None)
|
||||
|
||||
def reshow(self):
|
||||
"""
|
||||
A special method to re-show the figure in the notebook.
|
||||
|
||||
"""
|
||||
self._shown = False
|
||||
self.show()
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
return bool(self.web_sockets)
|
||||
|
||||
@classmethod
|
||||
def get_javascript(cls, stream=None):
|
||||
if stream is None:
|
||||
output = io.StringIO()
|
||||
else:
|
||||
output = stream
|
||||
super().get_javascript(stream=output)
|
||||
output.write((pathlib.Path(__file__).parent
|
||||
/ "web_backend/js/nbagg_mpl.js")
|
||||
.read_text(encoding="utf-8"))
|
||||
if stream is None:
|
||||
return output.getvalue()
|
||||
|
||||
def _create_comm(self):
|
||||
comm = CommSocket(self)
|
||||
self.add_web_socket(comm)
|
||||
return comm
|
||||
|
||||
def destroy(self):
|
||||
self._send_event('close')
|
||||
# need to copy comms as callbacks will modify this list
|
||||
for comm in list(self.web_sockets):
|
||||
comm.on_close()
|
||||
self.clearup_closed()
|
||||
|
||||
def clearup_closed(self):
|
||||
"""Clear up any closed Comms."""
|
||||
self.web_sockets = {socket for socket in self.web_sockets
|
||||
if socket.is_open()}
|
||||
|
||||
if len(self.web_sockets) == 0:
|
||||
CloseEvent("close_event", self.canvas)._process()
|
||||
|
||||
def remove_comm(self, comm_id):
|
||||
self.web_sockets = {socket for socket in self.web_sockets
|
||||
if socket.comm.comm_id != comm_id}
|
||||
|
||||
|
||||
class FigureCanvasNbAgg(FigureCanvasWebAggCore):
|
||||
manager_class = FigureManagerNbAgg
|
||||
|
||||
|
||||
class CommSocket:
|
||||
"""
|
||||
Manages the Comm connection between IPython and the browser (client).
|
||||
|
||||
Comms are 2 way, with the CommSocket being able to publish a message
|
||||
via the send_json method, and handle a message with on_message. On the
|
||||
JS side figure.send_message and figure.ws.onmessage do the sending and
|
||||
receiving respectively.
|
||||
|
||||
"""
|
||||
def __init__(self, manager):
|
||||
self.supports_binary = None
|
||||
self.manager = manager
|
||||
self.uuid = str(uuid.uuid4())
|
||||
# Publish an output area with a unique ID. The javascript can then
|
||||
# hook into this area.
|
||||
display(HTML("<div id=%r></div>" % self.uuid))
|
||||
try:
|
||||
self.comm = Comm('matplotlib', data={'id': self.uuid})
|
||||
except AttributeError as err:
|
||||
raise RuntimeError('Unable to create an IPython notebook Comm '
|
||||
'instance. Are you in the IPython '
|
||||
'notebook?') from err
|
||||
self.comm.on_msg(self.on_message)
|
||||
|
||||
manager = self.manager
|
||||
self._ext_close = False
|
||||
|
||||
def _on_close(close_message):
|
||||
self._ext_close = True
|
||||
manager.remove_comm(close_message['content']['comm_id'])
|
||||
manager.clearup_closed()
|
||||
|
||||
self.comm.on_close(_on_close)
|
||||
|
||||
def is_open(self):
|
||||
return not (self._ext_close or self.comm._closed)
|
||||
|
||||
def on_close(self):
|
||||
# When the socket is closed, deregister the websocket with
|
||||
# the FigureManager.
|
||||
if self.is_open():
|
||||
try:
|
||||
self.comm.close()
|
||||
except KeyError:
|
||||
# apparently already cleaned it up?
|
||||
pass
|
||||
|
||||
def send_json(self, content):
|
||||
self.comm.send({'data': json.dumps(content)})
|
||||
|
||||
def send_binary(self, blob):
|
||||
if self.supports_binary:
|
||||
self.comm.send({'blob': 'image/png'}, buffers=[blob])
|
||||
else:
|
||||
# The comm is ASCII, so we send the image in base64 encoded data
|
||||
# URL form.
|
||||
data = b64encode(blob).decode('ascii')
|
||||
data_uri = f"data:image/png;base64,{data}"
|
||||
self.comm.send({'data': data_uri})
|
||||
|
||||
def on_message(self, message):
|
||||
# The 'supports_binary' message is relevant to the
|
||||
# websocket itself. The other messages get passed along
|
||||
# to matplotlib as-is.
|
||||
|
||||
# Every message has a "type" and a "figure_id".
|
||||
message = json.loads(message['content']['data'])
|
||||
if message['type'] == 'closing':
|
||||
self.on_close()
|
||||
self.manager.clearup_closed()
|
||||
elif message['type'] == 'supports_binary':
|
||||
self.supports_binary = message['value']
|
||||
else:
|
||||
self.manager.handle_json(message)
|
||||
|
||||
|
||||
@_Backend.export
|
||||
class _BackendNbAgg(_Backend):
|
||||
FigureCanvas = FigureCanvasNbAgg
|
||||
FigureManager = FigureManagerNbAgg
|
||||
2819
venv/lib/python3.12/site-packages/matplotlib/backends/backend_pdf.py
Normal file
2819
venv/lib/python3.12/site-packages/matplotlib/backends/backend_pdf.py
Normal file
File diff suppressed because it is too large
Load Diff
1012
venv/lib/python3.12/site-packages/matplotlib/backends/backend_pgf.py
Normal file
1012
venv/lib/python3.12/site-packages/matplotlib/backends/backend_pgf.py
Normal file
File diff suppressed because it is too large
Load Diff
1326
venv/lib/python3.12/site-packages/matplotlib/backends/backend_ps.py
Normal file
1326
venv/lib/python3.12/site-packages/matplotlib/backends/backend_ps.py
Normal file
File diff suppressed because it is too large
Load Diff
1067
venv/lib/python3.12/site-packages/matplotlib/backends/backend_qt.py
Normal file
1067
venv/lib/python3.12/site-packages/matplotlib/backends/backend_qt.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
||||
from .. import backends
|
||||
|
||||
backends._QT_FORCE_QT5_BINDING = True
|
||||
|
||||
|
||||
from .backend_qt import ( # noqa
|
||||
SPECIAL_KEYS,
|
||||
# Public API
|
||||
cursord, _create_qApp, _BackendQT, TimerQT, MainWindow, FigureCanvasQT,
|
||||
FigureManagerQT, ToolbarQt, NavigationToolbar2QT, SubplotToolQt,
|
||||
SaveFigureQt, ConfigureSubplotsQt, RubberbandQt,
|
||||
HelpQt, ToolCopyToClipboardQT,
|
||||
# internal re-exports
|
||||
FigureCanvasBase, FigureManagerBase, MouseButton, NavigationToolbar2,
|
||||
TimerBase, ToolContainerBase, figureoptions, Gcf
|
||||
)
|
||||
from . import backend_qt as _backend_qt # noqa
|
||||
|
||||
|
||||
@_BackendQT.export
|
||||
class _BackendQT5(_BackendQT):
|
||||
pass
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
if name == 'qApp':
|
||||
return _backend_qt.qApp
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@ -0,0 +1,14 @@
|
||||
"""
|
||||
Render to qt from agg
|
||||
"""
|
||||
from .. import backends
|
||||
|
||||
backends._QT_FORCE_QT5_BINDING = True
|
||||
from .backend_qtagg import ( # noqa: F401, E402 # pylint: disable=W0611
|
||||
_BackendQTAgg, FigureCanvasQTAgg, FigureManagerQT, NavigationToolbar2QT,
|
||||
FigureCanvasAgg, FigureCanvasQT)
|
||||
|
||||
|
||||
@_BackendQTAgg.export
|
||||
class _BackendQT5Agg(_BackendQTAgg):
|
||||
pass
|
||||
@ -0,0 +1,11 @@
|
||||
from .. import backends
|
||||
|
||||
backends._QT_FORCE_QT5_BINDING = True
|
||||
from .backend_qtcairo import ( # noqa: F401, E402 # pylint: disable=W0611
|
||||
_BackendQTCairo, FigureCanvasQTCairo, FigureCanvasCairo, FigureCanvasQT
|
||||
)
|
||||
|
||||
|
||||
@_BackendQTCairo.export
|
||||
class _BackendQT5Cairo(_BackendQTCairo):
|
||||
pass
|
||||
@ -0,0 +1,86 @@
|
||||
"""
|
||||
Render to qt from agg.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
|
||||
from matplotlib.transforms import Bbox
|
||||
|
||||
from .qt_compat import QT_API, QtCore, QtGui
|
||||
from .backend_agg import FigureCanvasAgg
|
||||
from .backend_qt import _BackendQT, FigureCanvasQT
|
||||
from .backend_qt import ( # noqa: F401 # pylint: disable=W0611
|
||||
FigureManagerQT, NavigationToolbar2QT)
|
||||
|
||||
|
||||
class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT):
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""
|
||||
Copy the image from the Agg canvas to the qt.drawable.
|
||||
|
||||
In Qt, all drawing should be done inside of here when a widget is
|
||||
shown onscreen.
|
||||
"""
|
||||
self._draw_idle() # Only does something if a draw is pending.
|
||||
|
||||
# If the canvas does not have a renderer, then give up and wait for
|
||||
# FigureCanvasAgg.draw(self) to be called.
|
||||
if not hasattr(self, 'renderer'):
|
||||
return
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
try:
|
||||
# See documentation of QRect: bottom() and right() are off
|
||||
# by 1, so use left() + width() and top() + height().
|
||||
rect = event.rect()
|
||||
# scale rect dimensions using the screen dpi ratio to get
|
||||
# correct values for the Figure coordinates (rather than
|
||||
# QT5's coords)
|
||||
width = rect.width() * self.device_pixel_ratio
|
||||
height = rect.height() * self.device_pixel_ratio
|
||||
left, top = self.mouseEventCoords(rect.topLeft())
|
||||
# shift the "top" by the height of the image to get the
|
||||
# correct corner for our coordinate system
|
||||
bottom = top - height
|
||||
# same with the right side of the image
|
||||
right = left + width
|
||||
# create a buffer using the image bounding box
|
||||
bbox = Bbox([[left, bottom], [right, top]])
|
||||
buf = memoryview(self.copy_from_bbox(bbox))
|
||||
|
||||
if QT_API == "PyQt6":
|
||||
from PyQt6 import sip
|
||||
ptr = int(sip.voidptr(buf))
|
||||
else:
|
||||
ptr = buf
|
||||
|
||||
painter.eraseRect(rect) # clear the widget canvas
|
||||
qimage = QtGui.QImage(ptr, buf.shape[1], buf.shape[0],
|
||||
QtGui.QImage.Format.Format_RGBA8888)
|
||||
qimage.setDevicePixelRatio(self.device_pixel_ratio)
|
||||
# set origin using original QT coordinates
|
||||
origin = QtCore.QPoint(rect.left(), rect.top())
|
||||
painter.drawImage(origin, qimage)
|
||||
# Adjust the buf reference count to work around a memory
|
||||
# leak bug in QImage under PySide.
|
||||
if QT_API == "PySide2" and QtCore.__version_info__ < (5, 12):
|
||||
ctypes.c_long.from_address(id(buf)).value = 1
|
||||
|
||||
self._draw_rect_callback(painter)
|
||||
finally:
|
||||
painter.end()
|
||||
|
||||
def print_figure(self, *args, **kwargs):
|
||||
super().print_figure(*args, **kwargs)
|
||||
# In some cases, Qt will itself trigger a paint event after closing the file
|
||||
# save dialog. When that happens, we need to be sure that the internal canvas is
|
||||
# re-drawn. However, if the user is using an automatically-chosen Qt backend but
|
||||
# saving with a different backend (such as pgf), we do not want to trigger a
|
||||
# full draw in Qt, so just set the flag for next time.
|
||||
self._draw_pending = True
|
||||
|
||||
|
||||
@_BackendQT.export
|
||||
class _BackendQTAgg(_BackendQT):
|
||||
FigureCanvas = FigureCanvasQTAgg
|
||||
@ -0,0 +1,46 @@
|
||||
import ctypes
|
||||
|
||||
from .backend_cairo import cairo, FigureCanvasCairo
|
||||
from .backend_qt import _BackendQT, FigureCanvasQT
|
||||
from .qt_compat import QT_API, QtCore, QtGui
|
||||
|
||||
|
||||
class FigureCanvasQTCairo(FigureCanvasCairo, FigureCanvasQT):
|
||||
def draw(self):
|
||||
if hasattr(self._renderer.gc, "ctx"):
|
||||
self._renderer.dpi = self.figure.dpi
|
||||
self.figure.draw(self._renderer)
|
||||
super().draw()
|
||||
|
||||
def paintEvent(self, event):
|
||||
width = int(self.device_pixel_ratio * self.width())
|
||||
height = int(self.device_pixel_ratio * self.height())
|
||||
if (width, height) != self._renderer.get_canvas_width_height():
|
||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
|
||||
self._renderer.set_context(cairo.Context(surface))
|
||||
self._renderer.dpi = self.figure.dpi
|
||||
self.figure.draw(self._renderer)
|
||||
buf = self._renderer.gc.ctx.get_target().get_data()
|
||||
if QT_API == "PyQt6":
|
||||
from PyQt6 import sip
|
||||
ptr = int(sip.voidptr(buf))
|
||||
else:
|
||||
ptr = buf
|
||||
qimage = QtGui.QImage(
|
||||
ptr, width, height,
|
||||
QtGui.QImage.Format.Format_ARGB32_Premultiplied)
|
||||
# Adjust the buf reference count to work around a memory leak bug in
|
||||
# QImage under PySide.
|
||||
if QT_API == "PySide2" and QtCore.__version_info__ < (5, 12):
|
||||
ctypes.c_long.from_address(id(buf)).value = 1
|
||||
qimage.setDevicePixelRatio(self.device_pixel_ratio)
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.eraseRect(event.rect())
|
||||
painter.drawImage(0, 0, qimage)
|
||||
self._draw_rect_callback(painter)
|
||||
painter.end()
|
||||
|
||||
|
||||
@_BackendQT.export
|
||||
class _BackendQTCairo(_BackendQT):
|
||||
FigureCanvas = FigureCanvasQTCairo
|
||||
1368
venv/lib/python3.12/site-packages/matplotlib/backends/backend_svg.py
Normal file
1368
venv/lib/python3.12/site-packages/matplotlib/backends/backend_svg.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,213 @@
|
||||
"""
|
||||
A fully functional, do-nothing backend intended as a template for backend
|
||||
writers. It is fully functional in that you can select it as a backend e.g.
|
||||
with ::
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("template")
|
||||
|
||||
and your program will (should!) run without error, though no output is
|
||||
produced. This provides a starting point for backend writers; you can
|
||||
selectively implement drawing methods (`~.RendererTemplate.draw_path`,
|
||||
`~.RendererTemplate.draw_image`, etc.) and slowly see your figure come to life
|
||||
instead having to have a full-blown implementation before getting any results.
|
||||
|
||||
Copy this file to a directory outside the Matplotlib source tree, somewhere
|
||||
where Python can import it (by adding the directory to your ``sys.path`` or by
|
||||
packaging it as a normal Python package); if the backend is importable as
|
||||
``import my.backend`` you can then select it using ::
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("module://my.backend")
|
||||
|
||||
If your backend implements support for saving figures (i.e. has a `print_xyz`
|
||||
method), you can register it as the default handler for a given file type::
|
||||
|
||||
from matplotlib.backend_bases import register_backend
|
||||
register_backend('xyz', 'my_backend', 'XYZ File Format')
|
||||
...
|
||||
plt.savefig("figure.xyz")
|
||||
"""
|
||||
|
||||
from matplotlib import _api
|
||||
from matplotlib._pylab_helpers import Gcf
|
||||
from matplotlib.backend_bases import (
|
||||
FigureCanvasBase, FigureManagerBase, GraphicsContextBase, RendererBase)
|
||||
from matplotlib.figure import Figure
|
||||
|
||||
|
||||
class RendererTemplate(RendererBase):
|
||||
"""
|
||||
The renderer handles drawing/rendering operations.
|
||||
|
||||
This is a minimal do-nothing class that can be used to get started when
|
||||
writing a new backend. Refer to `.backend_bases.RendererBase` for
|
||||
documentation of the methods.
|
||||
"""
|
||||
|
||||
def __init__(self, dpi):
|
||||
super().__init__()
|
||||
self.dpi = dpi
|
||||
|
||||
def draw_path(self, gc, path, transform, rgbFace=None):
|
||||
pass
|
||||
|
||||
# draw_markers is optional, and we get more correct relative
|
||||
# timings by leaving it out. backend implementers concerned with
|
||||
# performance will probably want to implement it
|
||||
# def draw_markers(self, gc, marker_path, marker_trans, path, trans,
|
||||
# rgbFace=None):
|
||||
# pass
|
||||
|
||||
# draw_path_collection is optional, and we get more correct
|
||||
# relative timings by leaving it out. backend implementers concerned with
|
||||
# performance will probably want to implement it
|
||||
# def draw_path_collection(self, gc, master_transform, paths,
|
||||
# all_transforms, offsets, offset_trans,
|
||||
# facecolors, edgecolors, linewidths, linestyles,
|
||||
# antialiaseds):
|
||||
# pass
|
||||
|
||||
# draw_quad_mesh is optional, and we get more correct
|
||||
# relative timings by leaving it out. backend implementers concerned with
|
||||
# performance will probably want to implement it
|
||||
# def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
|
||||
# coordinates, offsets, offsetTrans, facecolors,
|
||||
# antialiased, edgecolors):
|
||||
# pass
|
||||
|
||||
def draw_image(self, gc, x, y, im):
|
||||
pass
|
||||
|
||||
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
|
||||
pass
|
||||
|
||||
def flipy(self):
|
||||
# docstring inherited
|
||||
return True
|
||||
|
||||
def get_canvas_width_height(self):
|
||||
# docstring inherited
|
||||
return 100, 100
|
||||
|
||||
def get_text_width_height_descent(self, s, prop, ismath):
|
||||
return 1, 1, 1
|
||||
|
||||
def new_gc(self):
|
||||
# docstring inherited
|
||||
return GraphicsContextTemplate()
|
||||
|
||||
def points_to_pixels(self, points):
|
||||
# if backend doesn't have dpi, e.g., postscript or svg
|
||||
return points
|
||||
# elif backend assumes a value for pixels_per_inch
|
||||
# return points/72.0 * self.dpi.get() * pixels_per_inch/72.0
|
||||
# else
|
||||
# return points/72.0 * self.dpi.get()
|
||||
|
||||
|
||||
class GraphicsContextTemplate(GraphicsContextBase):
|
||||
"""
|
||||
The graphics context provides the color, line styles, etc. See the cairo
|
||||
and postscript backends for examples of mapping the graphics context
|
||||
attributes (cap styles, join styles, line widths, colors) to a particular
|
||||
backend. In cairo this is done by wrapping a cairo.Context object and
|
||||
forwarding the appropriate calls to it using a dictionary mapping styles
|
||||
to gdk constants. In Postscript, all the work is done by the renderer,
|
||||
mapping line styles to postscript calls.
|
||||
|
||||
If it's more appropriate to do the mapping at the renderer level (as in
|
||||
the postscript backend), you don't need to override any of the GC methods.
|
||||
If it's more appropriate to wrap an instance (as in the cairo backend) and
|
||||
do the mapping here, you'll need to override several of the setter
|
||||
methods.
|
||||
|
||||
The base GraphicsContext stores colors as an RGB tuple on the unit
|
||||
interval, e.g., (0.5, 0.0, 1.0). You may need to map this to colors
|
||||
appropriate for your backend.
|
||||
"""
|
||||
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# The following functions and classes are for pyplot and implement
|
||||
# window/figure managers, etc.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
|
||||
class FigureManagerTemplate(FigureManagerBase):
|
||||
"""
|
||||
Helper class for pyplot mode, wraps everything up into a neat bundle.
|
||||
|
||||
For non-interactive backends, the base class is sufficient. For
|
||||
interactive backends, see the documentation of the `.FigureManagerBase`
|
||||
class for the list of methods that can/should be overridden.
|
||||
"""
|
||||
|
||||
|
||||
class FigureCanvasTemplate(FigureCanvasBase):
|
||||
"""
|
||||
The canvas the figure renders into. Calls the draw and print fig
|
||||
methods, creates the renderers, etc.
|
||||
|
||||
Note: GUI templates will want to connect events for button presses,
|
||||
mouse movements and key presses to functions that call the base
|
||||
class methods button_press_event, button_release_event,
|
||||
motion_notify_event, key_press_event, and key_release_event. See the
|
||||
implementations of the interactive backends for examples.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
figure : `~matplotlib.figure.Figure`
|
||||
A high-level Figure instance
|
||||
"""
|
||||
|
||||
# The instantiated manager class. For further customization,
|
||||
# ``FigureManager.create_with_canvas`` can also be overridden; see the
|
||||
# wx-based backends for an example.
|
||||
manager_class = FigureManagerTemplate
|
||||
|
||||
def draw(self):
|
||||
"""
|
||||
Draw the figure using the renderer.
|
||||
|
||||
It is important that this method actually walk the artist tree
|
||||
even if not output is produced because this will trigger
|
||||
deferred work (like computing limits auto-limits and tick
|
||||
values) that users may want access to before saving to disk.
|
||||
"""
|
||||
renderer = RendererTemplate(self.figure.dpi)
|
||||
self.figure.draw(renderer)
|
||||
|
||||
# You should provide a print_xxx function for every file format
|
||||
# you can write.
|
||||
|
||||
# If the file type is not in the base set of filetypes,
|
||||
# you should add it to the class-scope filetypes dictionary as follows:
|
||||
filetypes = {**FigureCanvasBase.filetypes, 'foo': 'My magic Foo format'}
|
||||
|
||||
def print_foo(self, filename, **kwargs):
|
||||
"""
|
||||
Write out format foo.
|
||||
|
||||
This method is normally called via `.Figure.savefig` and
|
||||
`.FigureCanvasBase.print_figure`, which take care of setting the figure
|
||||
facecolor, edgecolor, and dpi to the desired output values, and will
|
||||
restore them to the original values. Therefore, `print_foo` does not
|
||||
need to handle these settings.
|
||||
"""
|
||||
self.draw()
|
||||
|
||||
def get_default_filetype(self):
|
||||
return 'foo'
|
||||
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# Now just provide the standard names that backend.__init__ is expecting
|
||||
#
|
||||
########################################################################
|
||||
|
||||
FigureCanvas = FigureCanvasTemplate
|
||||
FigureManager = FigureManagerTemplate
|
||||
@ -0,0 +1,20 @@
|
||||
from . import _backend_tk
|
||||
from .backend_agg import FigureCanvasAgg
|
||||
from ._backend_tk import _BackendTk, FigureCanvasTk
|
||||
from ._backend_tk import ( # noqa: F401 # pylint: disable=W0611
|
||||
FigureManagerTk, NavigationToolbar2Tk)
|
||||
|
||||
|
||||
class FigureCanvasTkAgg(FigureCanvasAgg, FigureCanvasTk):
|
||||
def draw(self):
|
||||
super().draw()
|
||||
self.blit()
|
||||
|
||||
def blit(self, bbox=None):
|
||||
_backend_tk.blit(self._tkphoto, self.renderer.buffer_rgba(),
|
||||
(0, 1, 2, 3), bbox=bbox)
|
||||
|
||||
|
||||
@_BackendTk.export
|
||||
class _BackendTkAgg(_BackendTk):
|
||||
FigureCanvas = FigureCanvasTkAgg
|
||||
@ -0,0 +1,26 @@
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
from . import _backend_tk
|
||||
from .backend_cairo import cairo, FigureCanvasCairo
|
||||
from ._backend_tk import _BackendTk, FigureCanvasTk
|
||||
|
||||
|
||||
class FigureCanvasTkCairo(FigureCanvasCairo, FigureCanvasTk):
|
||||
def draw(self):
|
||||
width = int(self.figure.bbox.width)
|
||||
height = int(self.figure.bbox.height)
|
||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
|
||||
self._renderer.set_context(cairo.Context(surface))
|
||||
self._renderer.dpi = self.figure.dpi
|
||||
self.figure.draw(self._renderer)
|
||||
buf = np.reshape(surface.get_data(), (height, width, 4))
|
||||
_backend_tk.blit(
|
||||
self._tkphoto, buf,
|
||||
(2, 1, 0, 3) if sys.byteorder == "little" else (1, 2, 3, 0))
|
||||
|
||||
|
||||
@_BackendTk.export
|
||||
class _BackendTkCairo(_BackendTk):
|
||||
FigureCanvas = FigureCanvasTkCairo
|
||||
@ -0,0 +1,328 @@
|
||||
"""Displays Agg images in the browser, with interactivity."""
|
||||
|
||||
# The WebAgg backend is divided into two modules:
|
||||
#
|
||||
# - `backend_webagg_core.py` contains code necessary to embed a WebAgg
|
||||
# plot inside of a web application, and communicate in an abstract
|
||||
# way over a web socket.
|
||||
#
|
||||
# - `backend_webagg.py` contains a concrete implementation of a basic
|
||||
# application, implemented with tornado.
|
||||
|
||||
from contextlib import contextmanager
|
||||
import errno
|
||||
from io import BytesIO
|
||||
import json
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
import random
|
||||
import sys
|
||||
import signal
|
||||
import threading
|
||||
|
||||
try:
|
||||
import tornado
|
||||
except ImportError as err:
|
||||
raise RuntimeError("The WebAgg backend requires Tornado.") from err
|
||||
|
||||
import tornado.web
|
||||
import tornado.ioloop
|
||||
import tornado.websocket
|
||||
|
||||
import matplotlib as mpl
|
||||
from matplotlib.backend_bases import _Backend
|
||||
from matplotlib._pylab_helpers import Gcf
|
||||
from . import backend_webagg_core as core
|
||||
from .backend_webagg_core import ( # noqa: F401 # pylint: disable=W0611
|
||||
TimerAsyncio, TimerTornado)
|
||||
|
||||
|
||||
webagg_server_thread = threading.Thread(
|
||||
target=lambda: tornado.ioloop.IOLoop.instance().start())
|
||||
|
||||
|
||||
class FigureManagerWebAgg(core.FigureManagerWebAgg):
|
||||
_toolbar2_class = core.NavigationToolbar2WebAgg
|
||||
|
||||
@classmethod
|
||||
def pyplot_show(cls, *, block=None):
|
||||
WebAggApplication.initialize()
|
||||
|
||||
url = "http://{address}:{port}{prefix}".format(
|
||||
address=WebAggApplication.address,
|
||||
port=WebAggApplication.port,
|
||||
prefix=WebAggApplication.url_prefix)
|
||||
|
||||
if mpl.rcParams['webagg.open_in_browser']:
|
||||
import webbrowser
|
||||
if not webbrowser.open(url):
|
||||
print(f"To view figure, visit {url}")
|
||||
else:
|
||||
print(f"To view figure, visit {url}")
|
||||
|
||||
WebAggApplication.start()
|
||||
|
||||
|
||||
class FigureCanvasWebAgg(core.FigureCanvasWebAggCore):
|
||||
manager_class = FigureManagerWebAgg
|
||||
|
||||
|
||||
class WebAggApplication(tornado.web.Application):
|
||||
initialized = False
|
||||
started = False
|
||||
|
||||
class FavIcon(tornado.web.RequestHandler):
|
||||
def get(self):
|
||||
self.set_header('Content-Type', 'image/png')
|
||||
self.write(Path(mpl.get_data_path(),
|
||||
'images/matplotlib.png').read_bytes())
|
||||
|
||||
class SingleFigurePage(tornado.web.RequestHandler):
|
||||
def __init__(self, application, request, *, url_prefix='', **kwargs):
|
||||
self.url_prefix = url_prefix
|
||||
super().__init__(application, request, **kwargs)
|
||||
|
||||
def get(self, fignum):
|
||||
fignum = int(fignum)
|
||||
manager = Gcf.get_fig_manager(fignum)
|
||||
|
||||
ws_uri = f'ws://{self.request.host}{self.url_prefix}/'
|
||||
self.render(
|
||||
"single_figure.html",
|
||||
prefix=self.url_prefix,
|
||||
ws_uri=ws_uri,
|
||||
fig_id=fignum,
|
||||
toolitems=core.NavigationToolbar2WebAgg.toolitems,
|
||||
canvas=manager.canvas)
|
||||
|
||||
class AllFiguresPage(tornado.web.RequestHandler):
|
||||
def __init__(self, application, request, *, url_prefix='', **kwargs):
|
||||
self.url_prefix = url_prefix
|
||||
super().__init__(application, request, **kwargs)
|
||||
|
||||
def get(self):
|
||||
ws_uri = f'ws://{self.request.host}{self.url_prefix}/'
|
||||
self.render(
|
||||
"all_figures.html",
|
||||
prefix=self.url_prefix,
|
||||
ws_uri=ws_uri,
|
||||
figures=sorted(Gcf.figs.items()),
|
||||
toolitems=core.NavigationToolbar2WebAgg.toolitems)
|
||||
|
||||
class MplJs(tornado.web.RequestHandler):
|
||||
def get(self):
|
||||
self.set_header('Content-Type', 'application/javascript')
|
||||
|
||||
js_content = core.FigureManagerWebAgg.get_javascript()
|
||||
|
||||
self.write(js_content)
|
||||
|
||||
class Download(tornado.web.RequestHandler):
|
||||
def get(self, fignum, fmt):
|
||||
fignum = int(fignum)
|
||||
manager = Gcf.get_fig_manager(fignum)
|
||||
self.set_header(
|
||||
'Content-Type', mimetypes.types_map.get(fmt, 'binary'))
|
||||
buff = BytesIO()
|
||||
manager.canvas.figure.savefig(buff, format=fmt)
|
||||
self.write(buff.getvalue())
|
||||
|
||||
class WebSocket(tornado.websocket.WebSocketHandler):
|
||||
supports_binary = True
|
||||
|
||||
def open(self, fignum):
|
||||
self.fignum = int(fignum)
|
||||
self.manager = Gcf.get_fig_manager(self.fignum)
|
||||
self.manager.add_web_socket(self)
|
||||
if hasattr(self, 'set_nodelay'):
|
||||
self.set_nodelay(True)
|
||||
|
||||
def on_close(self):
|
||||
self.manager.remove_web_socket(self)
|
||||
|
||||
def on_message(self, message):
|
||||
message = json.loads(message)
|
||||
# The 'supports_binary' message is on a client-by-client
|
||||
# basis. The others affect the (shared) canvas as a
|
||||
# whole.
|
||||
if message['type'] == 'supports_binary':
|
||||
self.supports_binary = message['value']
|
||||
else:
|
||||
manager = Gcf.get_fig_manager(self.fignum)
|
||||
# It is possible for a figure to be closed,
|
||||
# but a stale figure UI is still sending messages
|
||||
# from the browser.
|
||||
if manager is not None:
|
||||
manager.handle_json(message)
|
||||
|
||||
def send_json(self, content):
|
||||
self.write_message(json.dumps(content))
|
||||
|
||||
def send_binary(self, blob):
|
||||
if self.supports_binary:
|
||||
self.write_message(blob, binary=True)
|
||||
else:
|
||||
data_uri = "data:image/png;base64,{}".format(
|
||||
blob.encode('base64').replace('\n', ''))
|
||||
self.write_message(data_uri)
|
||||
|
||||
def __init__(self, url_prefix=''):
|
||||
if url_prefix:
|
||||
assert url_prefix[0] == '/' and url_prefix[-1] != '/', \
|
||||
'url_prefix must start with a "/" and not end with one.'
|
||||
|
||||
super().__init__(
|
||||
[
|
||||
# Static files for the CSS and JS
|
||||
(url_prefix + r'/_static/(.*)',
|
||||
tornado.web.StaticFileHandler,
|
||||
{'path': core.FigureManagerWebAgg.get_static_file_path()}),
|
||||
|
||||
# Static images for the toolbar
|
||||
(url_prefix + r'/_images/(.*)',
|
||||
tornado.web.StaticFileHandler,
|
||||
{'path': Path(mpl.get_data_path(), 'images')}),
|
||||
|
||||
# A Matplotlib favicon
|
||||
(url_prefix + r'/favicon.ico', self.FavIcon),
|
||||
|
||||
# The page that contains all of the pieces
|
||||
(url_prefix + r'/([0-9]+)', self.SingleFigurePage,
|
||||
{'url_prefix': url_prefix}),
|
||||
|
||||
# The page that contains all of the figures
|
||||
(url_prefix + r'/?', self.AllFiguresPage,
|
||||
{'url_prefix': url_prefix}),
|
||||
|
||||
(url_prefix + r'/js/mpl.js', self.MplJs),
|
||||
|
||||
# Sends images and events to the browser, and receives
|
||||
# events from the browser
|
||||
(url_prefix + r'/([0-9]+)/ws', self.WebSocket),
|
||||
|
||||
# Handles the downloading (i.e., saving) of static images
|
||||
(url_prefix + r'/([0-9]+)/download.([a-z0-9.]+)',
|
||||
self.Download),
|
||||
],
|
||||
template_path=core.FigureManagerWebAgg.get_static_file_path())
|
||||
|
||||
@classmethod
|
||||
def initialize(cls, url_prefix='', port=None, address=None):
|
||||
if cls.initialized:
|
||||
return
|
||||
|
||||
# Create the class instance
|
||||
app = cls(url_prefix=url_prefix)
|
||||
|
||||
cls.url_prefix = url_prefix
|
||||
|
||||
# This port selection algorithm is borrowed, more or less
|
||||
# verbatim, from IPython.
|
||||
def random_ports(port, n):
|
||||
"""
|
||||
Generate a list of n random ports near the given port.
|
||||
|
||||
The first 5 ports will be sequential, and the remaining n-5 will be
|
||||
randomly selected in the range [port-2*n, port+2*n].
|
||||
"""
|
||||
for i in range(min(5, n)):
|
||||
yield port + i
|
||||
for i in range(n - 5):
|
||||
yield port + random.randint(-2 * n, 2 * n)
|
||||
|
||||
if address is None:
|
||||
cls.address = mpl.rcParams['webagg.address']
|
||||
else:
|
||||
cls.address = address
|
||||
cls.port = mpl.rcParams['webagg.port']
|
||||
for port in random_ports(cls.port,
|
||||
mpl.rcParams['webagg.port_retries']):
|
||||
try:
|
||||
app.listen(port, cls.address)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EADDRINUSE:
|
||||
raise
|
||||
else:
|
||||
cls.port = port
|
||||
break
|
||||
else:
|
||||
raise SystemExit(
|
||||
"The webagg server could not be started because an available "
|
||||
"port could not be found")
|
||||
|
||||
cls.initialized = True
|
||||
|
||||
@classmethod
|
||||
def start(cls):
|
||||
import asyncio
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
pass
|
||||
else:
|
||||
cls.started = True
|
||||
|
||||
if cls.started:
|
||||
return
|
||||
|
||||
"""
|
||||
IOLoop.running() was removed as of Tornado 2.4; see for example
|
||||
https://groups.google.com/forum/#!topic/python-tornado/QLMzkpQBGOY
|
||||
Thus there is no correct way to check if the loop has already been
|
||||
launched. We may end up with two concurrently running loops in that
|
||||
unlucky case with all the expected consequences.
|
||||
"""
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
|
||||
def shutdown():
|
||||
ioloop.stop()
|
||||
print("Server is stopped")
|
||||
sys.stdout.flush()
|
||||
cls.started = False
|
||||
|
||||
@contextmanager
|
||||
def catch_sigint():
|
||||
old_handler = signal.signal(
|
||||
signal.SIGINT,
|
||||
lambda sig, frame: ioloop.add_callback_from_signal(shutdown))
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
signal.signal(signal.SIGINT, old_handler)
|
||||
|
||||
# Set the flag to True *before* blocking on ioloop.start()
|
||||
cls.started = True
|
||||
|
||||
print("Press Ctrl+C to stop WebAgg server")
|
||||
sys.stdout.flush()
|
||||
with catch_sigint():
|
||||
ioloop.start()
|
||||
|
||||
|
||||
def ipython_inline_display(figure):
|
||||
import tornado.template
|
||||
|
||||
WebAggApplication.initialize()
|
||||
import asyncio
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
if not webagg_server_thread.is_alive():
|
||||
webagg_server_thread.start()
|
||||
|
||||
fignum = figure.number
|
||||
tpl = Path(core.FigureManagerWebAgg.get_static_file_path(),
|
||||
"ipython_inline_figure.html").read_text()
|
||||
t = tornado.template.Template(tpl)
|
||||
return t.generate(
|
||||
prefix=WebAggApplication.url_prefix,
|
||||
fig_id=fignum,
|
||||
toolitems=core.NavigationToolbar2WebAgg.toolitems,
|
||||
canvas=figure.canvas,
|
||||
port=WebAggApplication.port).decode('utf-8')
|
||||
|
||||
|
||||
@_Backend.export
|
||||
class _BackendWebAgg(_Backend):
|
||||
FigureCanvas = FigureCanvasWebAgg
|
||||
FigureManager = FigureManagerWebAgg
|
||||
@ -0,0 +1,517 @@
|
||||
"""Displays Agg images in the browser, with interactivity."""
|
||||
# The WebAgg backend is divided into two modules:
|
||||
#
|
||||
# - `backend_webagg_core.py` contains code necessary to embed a WebAgg
|
||||
# plot inside of a web application, and communicate in an abstract
|
||||
# way over a web socket.
|
||||
#
|
||||
# - `backend_webagg.py` contains a concrete implementation of a basic
|
||||
# application, implemented with asyncio.
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
from io import BytesIO, StringIO
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from matplotlib import _api, backend_bases, backend_tools
|
||||
from matplotlib.backends import backend_agg
|
||||
from matplotlib.backend_bases import (
|
||||
_Backend, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
_SPECIAL_KEYS_LUT = {'Alt': 'alt',
|
||||
'AltGraph': 'alt',
|
||||
'CapsLock': 'caps_lock',
|
||||
'Control': 'control',
|
||||
'Meta': 'meta',
|
||||
'NumLock': 'num_lock',
|
||||
'ScrollLock': 'scroll_lock',
|
||||
'Shift': 'shift',
|
||||
'Super': 'super',
|
||||
'Enter': 'enter',
|
||||
'Tab': 'tab',
|
||||
'ArrowDown': 'down',
|
||||
'ArrowLeft': 'left',
|
||||
'ArrowRight': 'right',
|
||||
'ArrowUp': 'up',
|
||||
'End': 'end',
|
||||
'Home': 'home',
|
||||
'PageDown': 'pagedown',
|
||||
'PageUp': 'pageup',
|
||||
'Backspace': 'backspace',
|
||||
'Delete': 'delete',
|
||||
'Insert': 'insert',
|
||||
'Escape': 'escape',
|
||||
'Pause': 'pause',
|
||||
'Select': 'select',
|
||||
'Dead': 'dead',
|
||||
'F1': 'f1',
|
||||
'F2': 'f2',
|
||||
'F3': 'f3',
|
||||
'F4': 'f4',
|
||||
'F5': 'f5',
|
||||
'F6': 'f6',
|
||||
'F7': 'f7',
|
||||
'F8': 'f8',
|
||||
'F9': 'f9',
|
||||
'F10': 'f10',
|
||||
'F11': 'f11',
|
||||
'F12': 'f12'}
|
||||
|
||||
|
||||
def _handle_key(key):
|
||||
"""Handle key values"""
|
||||
value = key[key.index('k') + 1:]
|
||||
if 'shift+' in key:
|
||||
if len(value) == 1:
|
||||
key = key.replace('shift+', '')
|
||||
if value in _SPECIAL_KEYS_LUT:
|
||||
value = _SPECIAL_KEYS_LUT[value]
|
||||
key = key[:key.index('k')] + value
|
||||
return key
|
||||
|
||||
|
||||
class TimerTornado(backend_bases.TimerBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._timer = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _timer_start(self):
|
||||
import tornado
|
||||
|
||||
self._timer_stop()
|
||||
if self._single:
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
self._timer = ioloop.add_timeout(
|
||||
datetime.timedelta(milliseconds=self.interval),
|
||||
self._on_timer)
|
||||
else:
|
||||
self._timer = tornado.ioloop.PeriodicCallback(
|
||||
self._on_timer,
|
||||
max(self.interval, 1e-6))
|
||||
self._timer.start()
|
||||
|
||||
def _timer_stop(self):
|
||||
import tornado
|
||||
|
||||
if self._timer is None:
|
||||
return
|
||||
elif self._single:
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
ioloop.remove_timeout(self._timer)
|
||||
else:
|
||||
self._timer.stop()
|
||||
self._timer = None
|
||||
|
||||
def _timer_set_interval(self):
|
||||
# Only stop and restart it if the timer has already been started
|
||||
if self._timer is not None:
|
||||
self._timer_stop()
|
||||
self._timer_start()
|
||||
|
||||
|
||||
class TimerAsyncio(backend_bases.TimerBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._task = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def _timer_task(self, interval):
|
||||
while True:
|
||||
try:
|
||||
await asyncio.sleep(interval)
|
||||
self._on_timer()
|
||||
|
||||
if self._single:
|
||||
break
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
|
||||
def _timer_start(self):
|
||||
self._timer_stop()
|
||||
|
||||
self._task = asyncio.ensure_future(
|
||||
self._timer_task(max(self.interval / 1_000., 1e-6))
|
||||
)
|
||||
|
||||
def _timer_stop(self):
|
||||
if self._task is not None:
|
||||
self._task.cancel()
|
||||
self._task = None
|
||||
|
||||
def _timer_set_interval(self):
|
||||
# Only stop and restart it if the timer has already been started
|
||||
if self._task is not None:
|
||||
self._timer_stop()
|
||||
self._timer_start()
|
||||
|
||||
|
||||
class FigureCanvasWebAggCore(backend_agg.FigureCanvasAgg):
|
||||
manager_class = _api.classproperty(lambda cls: FigureManagerWebAgg)
|
||||
_timer_cls = TimerAsyncio
|
||||
# Webagg and friends having the right methods, but still
|
||||
# having bugs in practice. Do not advertise that it works until
|
||||
# we can debug this.
|
||||
supports_blit = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Set to True when the renderer contains data that is newer
|
||||
# than the PNG buffer.
|
||||
self._png_is_old = True
|
||||
# Set to True by the `refresh` message so that the next frame
|
||||
# sent to the clients will be a full frame.
|
||||
self._force_full = True
|
||||
# The last buffer, for diff mode.
|
||||
self._last_buff = np.empty((0, 0))
|
||||
# Store the current image mode so that at any point, clients can
|
||||
# request the information. This should be changed by calling
|
||||
# self.set_image_mode(mode) so that the notification can be given
|
||||
# to the connected clients.
|
||||
self._current_image_mode = 'full'
|
||||
# Track mouse events to fill in the x, y position of key events.
|
||||
self._last_mouse_xy = (None, None)
|
||||
|
||||
def show(self):
|
||||
# show the figure window
|
||||
from matplotlib.pyplot import show
|
||||
show()
|
||||
|
||||
def draw(self):
|
||||
self._png_is_old = True
|
||||
try:
|
||||
super().draw()
|
||||
finally:
|
||||
self.manager.refresh_all() # Swap the frames.
|
||||
|
||||
def blit(self, bbox=None):
|
||||
self._png_is_old = True
|
||||
self.manager.refresh_all()
|
||||
|
||||
def draw_idle(self):
|
||||
self.send_event("draw")
|
||||
|
||||
def set_cursor(self, cursor):
|
||||
# docstring inherited
|
||||
cursor = _api.check_getitem({
|
||||
backend_tools.Cursors.HAND: 'pointer',
|
||||
backend_tools.Cursors.POINTER: 'default',
|
||||
backend_tools.Cursors.SELECT_REGION: 'crosshair',
|
||||
backend_tools.Cursors.MOVE: 'move',
|
||||
backend_tools.Cursors.WAIT: 'wait',
|
||||
backend_tools.Cursors.RESIZE_HORIZONTAL: 'ew-resize',
|
||||
backend_tools.Cursors.RESIZE_VERTICAL: 'ns-resize',
|
||||
}, cursor=cursor)
|
||||
self.send_event('cursor', cursor=cursor)
|
||||
|
||||
def set_image_mode(self, mode):
|
||||
"""
|
||||
Set the image mode for any subsequent images which will be sent
|
||||
to the clients. The modes may currently be either 'full' or 'diff'.
|
||||
|
||||
Note: diff images may not contain transparency, therefore upon
|
||||
draw this mode may be changed if the resulting image has any
|
||||
transparent component.
|
||||
"""
|
||||
_api.check_in_list(['full', 'diff'], mode=mode)
|
||||
if self._current_image_mode != mode:
|
||||
self._current_image_mode = mode
|
||||
self.handle_send_image_mode(None)
|
||||
|
||||
def get_diff_image(self):
|
||||
if self._png_is_old:
|
||||
renderer = self.get_renderer()
|
||||
|
||||
pixels = np.asarray(renderer.buffer_rgba())
|
||||
# The buffer is created as type uint32 so that entire
|
||||
# pixels can be compared in one numpy call, rather than
|
||||
# needing to compare each plane separately.
|
||||
buff = pixels.view(np.uint32).squeeze(2)
|
||||
|
||||
if (self._force_full
|
||||
# If the buffer has changed size we need to do a full draw.
|
||||
or buff.shape != self._last_buff.shape
|
||||
# If any pixels have transparency, we need to force a full
|
||||
# draw as we cannot overlay new on top of old.
|
||||
or (pixels[:, :, 3] != 255).any()):
|
||||
self.set_image_mode('full')
|
||||
output = buff
|
||||
else:
|
||||
self.set_image_mode('diff')
|
||||
diff = buff != self._last_buff
|
||||
output = np.where(diff, buff, 0)
|
||||
|
||||
# Store the current buffer so we can compute the next diff.
|
||||
self._last_buff = buff.copy()
|
||||
self._force_full = False
|
||||
self._png_is_old = False
|
||||
|
||||
data = output.view(dtype=np.uint8).reshape((*output.shape, 4))
|
||||
with BytesIO() as png:
|
||||
Image.fromarray(data).save(png, format="png")
|
||||
return png.getvalue()
|
||||
|
||||
def handle_event(self, event):
|
||||
e_type = event['type']
|
||||
handler = getattr(self, f'handle_{e_type}',
|
||||
self.handle_unknown_event)
|
||||
return handler(event)
|
||||
|
||||
def handle_unknown_event(self, event):
|
||||
_log.warning('Unhandled message type %s. %s', event["type"], event)
|
||||
|
||||
def handle_ack(self, event):
|
||||
# Network latency tends to decrease if traffic is flowing
|
||||
# in both directions. Therefore, the browser sends back
|
||||
# an "ack" message after each image frame is received.
|
||||
# This could also be used as a simple sanity check in the
|
||||
# future, but for now the performance increase is enough
|
||||
# to justify it, even if the server does nothing with it.
|
||||
pass
|
||||
|
||||
def handle_draw(self, event):
|
||||
self.draw()
|
||||
|
||||
def _handle_mouse(self, event):
|
||||
x = event['x']
|
||||
y = event['y']
|
||||
y = self.get_renderer().height - y
|
||||
self._last_mouse_xy = x, y
|
||||
# JavaScript button numbers and Matplotlib button numbers are off by 1.
|
||||
button = event['button'] + 1
|
||||
|
||||
e_type = event['type']
|
||||
modifiers = event['modifiers']
|
||||
guiEvent = event.get('guiEvent')
|
||||
if e_type in ['button_press', 'button_release']:
|
||||
MouseEvent(e_type + '_event', self, x, y, button,
|
||||
modifiers=modifiers, guiEvent=guiEvent)._process()
|
||||
elif e_type == 'dblclick':
|
||||
MouseEvent('button_press_event', self, x, y, button, dblclick=True,
|
||||
modifiers=modifiers, guiEvent=guiEvent)._process()
|
||||
elif e_type == 'scroll':
|
||||
MouseEvent('scroll_event', self, x, y, step=event['step'],
|
||||
modifiers=modifiers, guiEvent=guiEvent)._process()
|
||||
elif e_type == 'motion_notify':
|
||||
MouseEvent(e_type + '_event', self, x, y,
|
||||
modifiers=modifiers, guiEvent=guiEvent)._process()
|
||||
elif e_type in ['figure_enter', 'figure_leave']:
|
||||
LocationEvent(e_type + '_event', self, x, y,
|
||||
modifiers=modifiers, guiEvent=guiEvent)._process()
|
||||
handle_button_press = handle_button_release = handle_dblclick = \
|
||||
handle_figure_enter = handle_figure_leave = handle_motion_notify = \
|
||||
handle_scroll = _handle_mouse
|
||||
|
||||
def _handle_key(self, event):
|
||||
KeyEvent(event['type'] + '_event', self,
|
||||
_handle_key(event['key']), *self._last_mouse_xy,
|
||||
guiEvent=event.get('guiEvent'))._process()
|
||||
handle_key_press = handle_key_release = _handle_key
|
||||
|
||||
def handle_toolbar_button(self, event):
|
||||
# TODO: Be more suspicious of the input
|
||||
getattr(self.toolbar, event['name'])()
|
||||
|
||||
def handle_refresh(self, event):
|
||||
figure_label = self.figure.get_label()
|
||||
if not figure_label:
|
||||
figure_label = f"Figure {self.manager.num}"
|
||||
self.send_event('figure_label', label=figure_label)
|
||||
self._force_full = True
|
||||
if self.toolbar:
|
||||
# Normal toolbar init would refresh this, but it happens before the
|
||||
# browser canvas is set up.
|
||||
self.toolbar.set_history_buttons()
|
||||
self.draw_idle()
|
||||
|
||||
def handle_resize(self, event):
|
||||
x = int(event.get('width', 800)) * self.device_pixel_ratio
|
||||
y = int(event.get('height', 800)) * self.device_pixel_ratio
|
||||
fig = self.figure
|
||||
# An attempt at approximating the figure size in pixels.
|
||||
fig.set_size_inches(x / fig.dpi, y / fig.dpi, forward=False)
|
||||
# Acknowledge the resize, and force the viewer to update the
|
||||
# canvas size to the figure's new size (which is hopefully
|
||||
# identical or within a pixel or so).
|
||||
self._png_is_old = True
|
||||
self.manager.resize(*fig.bbox.size, forward=False)
|
||||
ResizeEvent('resize_event', self)._process()
|
||||
self.draw_idle()
|
||||
|
||||
def handle_send_image_mode(self, event):
|
||||
# The client requests notification of what the current image mode is.
|
||||
self.send_event('image_mode', mode=self._current_image_mode)
|
||||
|
||||
def handle_set_device_pixel_ratio(self, event):
|
||||
self._handle_set_device_pixel_ratio(event.get('device_pixel_ratio', 1))
|
||||
|
||||
def handle_set_dpi_ratio(self, event):
|
||||
# This handler is for backwards-compatibility with older ipympl.
|
||||
self._handle_set_device_pixel_ratio(event.get('dpi_ratio', 1))
|
||||
|
||||
def _handle_set_device_pixel_ratio(self, device_pixel_ratio):
|
||||
if self._set_device_pixel_ratio(device_pixel_ratio):
|
||||
self._force_full = True
|
||||
self.draw_idle()
|
||||
|
||||
def send_event(self, event_type, **kwargs):
|
||||
if self.manager:
|
||||
self.manager._send_event(event_type, **kwargs)
|
||||
|
||||
|
||||
_ALLOWED_TOOL_ITEMS = {
|
||||
'home',
|
||||
'back',
|
||||
'forward',
|
||||
'pan',
|
||||
'zoom',
|
||||
'download',
|
||||
None,
|
||||
}
|
||||
|
||||
|
||||
class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2):
|
||||
|
||||
# Use the standard toolbar items + download button
|
||||
toolitems = [
|
||||
(text, tooltip_text, image_file, name_of_method)
|
||||
for text, tooltip_text, image_file, name_of_method
|
||||
in (*backend_bases.NavigationToolbar2.toolitems,
|
||||
('Download', 'Download plot', 'filesave', 'download'))
|
||||
if name_of_method in _ALLOWED_TOOL_ITEMS
|
||||
]
|
||||
|
||||
def __init__(self, canvas):
|
||||
self.message = ''
|
||||
super().__init__(canvas)
|
||||
|
||||
def set_message(self, message):
|
||||
if message != self.message:
|
||||
self.canvas.send_event("message", message=message)
|
||||
self.message = message
|
||||
|
||||
def draw_rubberband(self, event, x0, y0, x1, y1):
|
||||
self.canvas.send_event("rubberband", x0=x0, y0=y0, x1=x1, y1=y1)
|
||||
|
||||
def remove_rubberband(self):
|
||||
self.canvas.send_event("rubberband", x0=-1, y0=-1, x1=-1, y1=-1)
|
||||
|
||||
def save_figure(self, *args):
|
||||
"""Save the current figure"""
|
||||
self.canvas.send_event('save')
|
||||
|
||||
def pan(self):
|
||||
super().pan()
|
||||
self.canvas.send_event('navigate_mode', mode=self.mode.name)
|
||||
|
||||
def zoom(self):
|
||||
super().zoom()
|
||||
self.canvas.send_event('navigate_mode', mode=self.mode.name)
|
||||
|
||||
def set_history_buttons(self):
|
||||
can_backward = self._nav_stack._pos > 0
|
||||
can_forward = self._nav_stack._pos < len(self._nav_stack) - 1
|
||||
self.canvas.send_event('history_buttons',
|
||||
Back=can_backward, Forward=can_forward)
|
||||
|
||||
|
||||
class FigureManagerWebAgg(backend_bases.FigureManagerBase):
|
||||
# This must be None to not break ipympl
|
||||
_toolbar2_class = None
|
||||
ToolbarCls = NavigationToolbar2WebAgg
|
||||
_window_title = "Matplotlib"
|
||||
|
||||
def __init__(self, canvas, num):
|
||||
self.web_sockets = set()
|
||||
super().__init__(canvas, num)
|
||||
|
||||
def show(self):
|
||||
pass
|
||||
|
||||
def resize(self, w, h, forward=True):
|
||||
self._send_event(
|
||||
'resize',
|
||||
size=(w / self.canvas.device_pixel_ratio,
|
||||
h / self.canvas.device_pixel_ratio),
|
||||
forward=forward)
|
||||
|
||||
def set_window_title(self, title):
|
||||
self._send_event('figure_label', label=title)
|
||||
self._window_title = title
|
||||
|
||||
def get_window_title(self):
|
||||
return self._window_title
|
||||
|
||||
# The following methods are specific to FigureManagerWebAgg
|
||||
|
||||
def add_web_socket(self, web_socket):
|
||||
assert hasattr(web_socket, 'send_binary')
|
||||
assert hasattr(web_socket, 'send_json')
|
||||
self.web_sockets.add(web_socket)
|
||||
self.resize(*self.canvas.figure.bbox.size)
|
||||
self._send_event('refresh')
|
||||
|
||||
def remove_web_socket(self, web_socket):
|
||||
self.web_sockets.remove(web_socket)
|
||||
|
||||
def handle_json(self, content):
|
||||
self.canvas.handle_event(content)
|
||||
|
||||
def refresh_all(self):
|
||||
if self.web_sockets:
|
||||
diff = self.canvas.get_diff_image()
|
||||
if diff is not None:
|
||||
for s in self.web_sockets:
|
||||
s.send_binary(diff)
|
||||
|
||||
@classmethod
|
||||
def get_javascript(cls, stream=None):
|
||||
if stream is None:
|
||||
output = StringIO()
|
||||
else:
|
||||
output = stream
|
||||
|
||||
output.write((Path(__file__).parent / "web_backend/js/mpl.js")
|
||||
.read_text(encoding="utf-8"))
|
||||
|
||||
toolitems = []
|
||||
for name, tooltip, image, method in cls.ToolbarCls.toolitems:
|
||||
if name is None:
|
||||
toolitems.append(['', '', '', ''])
|
||||
else:
|
||||
toolitems.append([name, tooltip, image, method])
|
||||
output.write(f"mpl.toolbar_items = {json.dumps(toolitems)};\n\n")
|
||||
|
||||
extensions = []
|
||||
for filetype, ext in sorted(FigureCanvasWebAggCore.
|
||||
get_supported_filetypes_grouped().
|
||||
items()):
|
||||
extensions.append(ext[0])
|
||||
output.write(f"mpl.extensions = {json.dumps(extensions)};\n\n")
|
||||
|
||||
output.write("mpl.default_extension = {};".format(
|
||||
json.dumps(FigureCanvasWebAggCore.get_default_filetype())))
|
||||
|
||||
if stream is None:
|
||||
return output.getvalue()
|
||||
|
||||
@classmethod
|
||||
def get_static_file_path(cls):
|
||||
return os.path.join(os.path.dirname(__file__), 'web_backend')
|
||||
|
||||
def _send_event(self, event_type, **kwargs):
|
||||
payload = {'type': event_type, **kwargs}
|
||||
for s in self.web_sockets:
|
||||
s.send_json(payload)
|
||||
|
||||
|
||||
@_Backend.export
|
||||
class _BackendWebAggCoreAgg(_Backend):
|
||||
FigureCanvas = FigureCanvasWebAggCore
|
||||
FigureManager = FigureManagerWebAgg
|
||||
1364
venv/lib/python3.12/site-packages/matplotlib/backends/backend_wx.py
Normal file
1364
venv/lib/python3.12/site-packages/matplotlib/backends/backend_wx.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,45 @@
|
||||
import wx
|
||||
|
||||
from .backend_agg import FigureCanvasAgg
|
||||
from .backend_wx import _BackendWx, _FigureCanvasWxBase
|
||||
from .backend_wx import ( # noqa: F401 # pylint: disable=W0611
|
||||
NavigationToolbar2Wx as NavigationToolbar2WxAgg)
|
||||
|
||||
|
||||
class FigureCanvasWxAgg(FigureCanvasAgg, _FigureCanvasWxBase):
|
||||
def draw(self, drawDC=None):
|
||||
"""
|
||||
Render the figure using agg.
|
||||
"""
|
||||
FigureCanvasAgg.draw(self)
|
||||
self.bitmap = self._create_bitmap()
|
||||
self._isDrawn = True
|
||||
self.gui_repaint(drawDC=drawDC)
|
||||
|
||||
def blit(self, bbox=None):
|
||||
# docstring inherited
|
||||
bitmap = self._create_bitmap()
|
||||
if bbox is None:
|
||||
self.bitmap = bitmap
|
||||
else:
|
||||
srcDC = wx.MemoryDC(bitmap)
|
||||
destDC = wx.MemoryDC(self.bitmap)
|
||||
x = int(bbox.x0)
|
||||
y = int(self.bitmap.GetHeight() - bbox.y1)
|
||||
destDC.Blit(x, y, int(bbox.width), int(bbox.height), srcDC, x, y)
|
||||
destDC.SelectObject(wx.NullBitmap)
|
||||
srcDC.SelectObject(wx.NullBitmap)
|
||||
self.gui_repaint()
|
||||
|
||||
def _create_bitmap(self):
|
||||
"""Create a wx.Bitmap from the renderer RGBA buffer"""
|
||||
rgba = self.get_renderer().buffer_rgba()
|
||||
h, w, _ = rgba.shape
|
||||
bitmap = wx.Bitmap.FromBufferRGBA(w, h, rgba)
|
||||
bitmap.SetScaleFactor(self.GetDPIScaleFactor())
|
||||
return bitmap
|
||||
|
||||
|
||||
@_BackendWx.export
|
||||
class _BackendWxAgg(_BackendWx):
|
||||
FigureCanvas = FigureCanvasWxAgg
|
||||
@ -0,0 +1,23 @@
|
||||
import wx.lib.wxcairo as wxcairo
|
||||
|
||||
from .backend_cairo import cairo, FigureCanvasCairo
|
||||
from .backend_wx import _BackendWx, _FigureCanvasWxBase
|
||||
from .backend_wx import ( # noqa: F401 # pylint: disable=W0611
|
||||
NavigationToolbar2Wx as NavigationToolbar2WxCairo)
|
||||
|
||||
|
||||
class FigureCanvasWxCairo(FigureCanvasCairo, _FigureCanvasWxBase):
|
||||
def draw(self, drawDC=None):
|
||||
size = self.figure.bbox.size.astype(int)
|
||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, *size)
|
||||
self._renderer.set_context(cairo.Context(surface))
|
||||
self._renderer.dpi = self.figure.dpi
|
||||
self.figure.draw(self._renderer)
|
||||
self.bitmap = wxcairo.BitmapFromImageSurface(surface)
|
||||
self._isDrawn = True
|
||||
self.gui_repaint(drawDC=drawDC)
|
||||
|
||||
|
||||
@_BackendWx.export
|
||||
class _BackendWxCairo(_BackendWx):
|
||||
FigureCanvas = FigureCanvasWxCairo
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user