Initial import: grid-bot — grid trading bot for BTC-USDT on Cifra Markets
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,124 @@
|
||||
__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, Literal, NamedTuple, overload
|
||||
|
||||
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 _update_raw(self, other_params: dict | RcParams) -> None: ...
|
||||
|
||||
def _ensure_has_backend(self) -> None: ...
|
||||
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: ...
|
||||
@overload
|
||||
def get_backend(*, auto_select: Literal[True] = True) -> str: ...
|
||||
@overload
|
||||
def get_backend(*, auto_select: Literal[False]) -> str | None: ...
|
||||
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 # noqa: E402
|
||||
from matplotlib.cm import _multivar_colormaps as multivar_colormaps # noqa: E402
|
||||
from matplotlib.cm import _bivar_colormaps as bivar_colormaps # noqa: E402
|
||||
from matplotlib.colors import _color_sequences as color_sequences # noqa: E402
|
||||
@@ -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)
|
||||
@@ -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}";
|
||||
}}
|
||||
"""
|
||||
@@ -0,0 +1,405 @@
|
||||
"""
|
||||
Helper functions for managing the Matplotlib API.
|
||||
|
||||
This documentation is only relevant for Matplotlib developers, not for users.
|
||||
|
||||
.. warning::
|
||||
|
||||
This module and its submodules are for internal use only. Do not use them
|
||||
in your own code. We may change the API at any time with no warning.
|
||||
|
||||
"""
|
||||
|
||||
import functools
|
||||
import itertools
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from .deprecation import ( # noqa: F401
|
||||
deprecated, warn_deprecated,
|
||||
rename_parameter, delete_parameter, make_keyword_only,
|
||||
deprecate_method_override, deprecate_privatize_attribute,
|
||||
suppress_matplotlib_deprecation_warning,
|
||||
MatplotlibDeprecationWarning)
|
||||
|
||||
|
||||
class classproperty:
|
||||
"""
|
||||
Like `property`, but also triggers on access via the class, and it is the
|
||||
*class* that's passed as argument.
|
||||
|
||||
Examples
|
||||
--------
|
||||
::
|
||||
|
||||
class C:
|
||||
@classproperty
|
||||
def foo(cls):
|
||||
return cls.__name__
|
||||
|
||||
assert C.foo == "C"
|
||||
"""
|
||||
|
||||
def __init__(self, fget, fset=None, fdel=None, doc=None):
|
||||
self._fget = fget
|
||||
if fset is not None or fdel is not None:
|
||||
raise ValueError('classproperty only implements fget.')
|
||||
self.fset = fset
|
||||
self.fdel = fdel
|
||||
# docs are ignored for now
|
||||
self._doc = doc
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
return self._fget(owner)
|
||||
|
||||
@property
|
||||
def fget(self):
|
||||
return self._fget
|
||||
|
||||
|
||||
# In the following check_foo() functions, the first parameter is positional-only to make
|
||||
# e.g. `_api.check_isinstance([...], types=foo)` work.
|
||||
|
||||
def check_isinstance(types, /, **kwargs):
|
||||
"""
|
||||
For each *key, value* pair in *kwargs*, check that *value* is an instance
|
||||
of one of *types*; if not, raise an appropriate TypeError.
|
||||
|
||||
As a special case, a ``None`` entry in *types* is treated as NoneType.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> _api.check_isinstance((SomeClass, None), arg=arg)
|
||||
"""
|
||||
none_type = type(None)
|
||||
types = ((types,) if isinstance(types, type) else
|
||||
(none_type,) if types is None else
|
||||
tuple(none_type if tp is None else tp for tp in types))
|
||||
|
||||
def type_name(tp):
|
||||
return ("None" if tp is none_type
|
||||
else tp.__qualname__ if tp.__module__ == "builtins"
|
||||
else f"{tp.__module__}.{tp.__qualname__}")
|
||||
|
||||
for k, v in kwargs.items():
|
||||
if not isinstance(v, types):
|
||||
names = [*map(type_name, types)]
|
||||
if "None" in names: # Move it to the end for better wording.
|
||||
names.remove("None")
|
||||
names.append("None")
|
||||
raise TypeError(
|
||||
"{!r} must be an instance of {}, not a {}".format(
|
||||
k,
|
||||
", ".join(names[:-1]) + " or " + names[-1]
|
||||
if len(names) > 1 else names[0],
|
||||
type_name(type(v))))
|
||||
|
||||
|
||||
def check_in_list(values, /, *, _print_supported_values=True, **kwargs):
|
||||
"""
|
||||
For each *key, value* pair in *kwargs*, check that *value* is in *values*;
|
||||
if not, raise an appropriate ValueError.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
values : iterable
|
||||
Sequence of values to check on.
|
||||
|
||||
Note: All values must support == comparisons.
|
||||
This means in particular the entries must not be numpy arrays.
|
||||
_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():
|
||||
try:
|
||||
exists = val in values
|
||||
except ValueError:
|
||||
# `in` internally uses `val == values[i]`. There are some objects
|
||||
# that do not support == to arbitrary other objects, in particular
|
||||
# numpy arrays.
|
||||
# Since such objects are not allowed in values, we can gracefully
|
||||
# handle the case that val (typically provided by users) is of such
|
||||
# type and directly state it's not in the list instead of letting
|
||||
# the individual `val == values[i]` ValueError surface.
|
||||
exists = False
|
||||
if not exists:
|
||||
msg = f"{val!r} is not a valid value for {key}"
|
||||
if _print_supported_values:
|
||||
msg += f"; supported values are {', '.join(map(repr, values))}"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def check_shape(shape, /, **kwargs):
|
||||
"""
|
||||
For each *key, value* pair in *kwargs*, check that *value* has the shape *shape*;
|
||||
if not, raise an appropriate ValueError.
|
||||
|
||||
*None* in the shape is treated as a "free" size that can have any length.
|
||||
e.g. (None, 2) -> (N, 2)
|
||||
|
||||
The values checked must be numpy arrays.
|
||||
|
||||
Examples
|
||||
--------
|
||||
To check for (N, 2) shaped arrays
|
||||
|
||||
>>> _api.check_shape((None, 2), arg=arg, other_arg=other_arg)
|
||||
"""
|
||||
for k, v in kwargs.items():
|
||||
data_shape = v.shape
|
||||
|
||||
if (len(data_shape) != len(shape)
|
||||
or any(s != t and t is not None for s, t in zip(data_shape, shape))):
|
||||
dim_labels = iter(itertools.chain(
|
||||
'NMLKJIH',
|
||||
(f"D{i}" for i in itertools.count())))
|
||||
text_shape = ", ".join([str(n) if n is not None else next(dim_labels)
|
||||
for n in shape[::-1]][::-1])
|
||||
if len(shape) == 1:
|
||||
text_shape += ","
|
||||
|
||||
raise ValueError(
|
||||
f"{k!r} must be {len(shape)}D with shape ({text_shape}), "
|
||||
f"but your input has shape {v.shape}"
|
||||
)
|
||||
|
||||
|
||||
def check_getitem(mapping, /, **kwargs):
|
||||
"""
|
||||
*kwargs* must consist of a single *key, value* pair. If *key* is in
|
||||
*mapping*, return ``mapping[value]``; else, raise an appropriate
|
||||
ValueError.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> _api.check_getitem({"foo": "bar"}, arg=arg)
|
||||
"""
|
||||
if len(kwargs) != 1:
|
||||
raise ValueError("check_getitem takes a single keyword argument")
|
||||
(k, v), = kwargs.items()
|
||||
try:
|
||||
return mapping[v]
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
f"{v!r} is not a valid value for {k}; supported values are "
|
||||
f"{', '.join(map(repr, mapping))}") from None
|
||||
|
||||
|
||||
def caching_module_getattr(cls):
|
||||
"""
|
||||
Helper decorator for implementing module-level ``__getattr__`` as a class.
|
||||
|
||||
This decorator must be used at the module toplevel as follows::
|
||||
|
||||
@caching_module_getattr
|
||||
class __getattr__: # The class *must* be named ``__getattr__``.
|
||||
@property # Only properties are taken into account.
|
||||
def name(self): ...
|
||||
|
||||
The ``__getattr__`` class will be replaced by a ``__getattr__``
|
||||
function such that trying to access ``name`` on the module will
|
||||
resolve the corresponding property (which may be decorated e.g. with
|
||||
``_api.deprecated`` for deprecating module globals). The properties are
|
||||
all implicitly cached. Moreover, a suitable AttributeError is generated
|
||||
and raised if no property with the given name exists.
|
||||
"""
|
||||
|
||||
assert cls.__name__ == "__getattr__"
|
||||
# Don't accidentally export cls dunders.
|
||||
props = {name: prop for name, prop in vars(cls).items()
|
||||
if isinstance(prop, property)}
|
||||
instance = cls()
|
||||
|
||||
@functools.cache
|
||||
def __getattr__(name):
|
||||
if name in props:
|
||||
return props[name].__get__(instance)
|
||||
raise AttributeError(
|
||||
f"module {cls.__module__!r} has no attribute {name!r}")
|
||||
|
||||
return __getattr__
|
||||
|
||||
|
||||
def define_aliases(alias_d, cls=None):
|
||||
"""
|
||||
Class decorator for defining property aliases.
|
||||
|
||||
Use as ::
|
||||
|
||||
@_api.define_aliases({"property": ["alias", ...], ...})
|
||||
class C: ...
|
||||
|
||||
For each property, if the corresponding ``get_property`` is defined in the
|
||||
class so far, an alias named ``get_alias`` will be defined; the same will
|
||||
be done for setters. If neither the getter nor the setter exists, an
|
||||
exception will be raised.
|
||||
|
||||
The alias map is stored as the ``_alias_map`` attribute on the class and
|
||||
can be used by `.normalize_kwargs` (which assumes that higher priority
|
||||
aliases come last).
|
||||
"""
|
||||
if cls is None: # Return the actual class decorator.
|
||||
return functools.partial(define_aliases, alias_d)
|
||||
|
||||
def make_alias(name): # Enforce a closure over *name*.
|
||||
@functools.wraps(getattr(cls, name))
|
||||
def method(self, *args, **kwargs):
|
||||
return getattr(self, name)(*args, **kwargs)
|
||||
return method
|
||||
|
||||
for prop, aliases in alias_d.items():
|
||||
exists = False
|
||||
for prefix in ["get_", "set_"]:
|
||||
if prefix + prop in vars(cls):
|
||||
exists = True
|
||||
for alias in aliases:
|
||||
method = make_alias(prefix + prop)
|
||||
method.__name__ = prefix + alias
|
||||
method.__doc__ = f"Alias for `{prefix + prop}`."
|
||||
setattr(cls, prefix + alias, method)
|
||||
if not exists:
|
||||
raise ValueError(
|
||||
f"Neither getter nor setter exists for {prop!r}")
|
||||
|
||||
def get_aliased_and_aliases(d):
|
||||
return {*d, *(alias for aliases in d.values() for alias in aliases)}
|
||||
|
||||
preexisting_aliases = getattr(cls, "_alias_map", {})
|
||||
conflicting = (get_aliased_and_aliases(preexisting_aliases)
|
||||
& get_aliased_and_aliases(alias_d))
|
||||
if conflicting:
|
||||
# Need to decide on conflict resolution policy.
|
||||
raise NotImplementedError(
|
||||
f"Parent class already defines conflicting aliases: {conflicting}")
|
||||
cls._alias_map = {**preexisting_aliases, **alias_d}
|
||||
return cls
|
||||
|
||||
|
||||
def select_matching_signature(funcs, *args, **kwargs):
|
||||
"""
|
||||
Select and call the function that accepts ``*args, **kwargs``.
|
||||
|
||||
*funcs* is a list of functions which should not raise any exception (other
|
||||
than `TypeError` if the arguments passed do not match their signature).
|
||||
|
||||
`select_matching_signature` tries to call each of the functions in *funcs*
|
||||
with ``*args, **kwargs`` (in the order in which they are given). Calls
|
||||
that fail with a `TypeError` are silently skipped. As soon as a call
|
||||
succeeds, `select_matching_signature` returns its return value. If no
|
||||
function accepts ``*args, **kwargs``, then the `TypeError` raised by the
|
||||
last failing call is re-raised.
|
||||
|
||||
Callers should normally make sure that any ``*args, **kwargs`` can only
|
||||
bind a single *func* (to avoid any ambiguity), although this is not checked
|
||||
by `select_matching_signature`.
|
||||
|
||||
Notes
|
||||
-----
|
||||
`select_matching_signature` is intended to help implementing
|
||||
signature-overloaded functions. In general, such functions should be
|
||||
avoided, except for back-compatibility concerns. A typical use pattern is
|
||||
::
|
||||
|
||||
def my_func(*args, **kwargs):
|
||||
params = select_matching_signature(
|
||||
[lambda old1, old2: locals(), lambda new: locals()],
|
||||
*args, **kwargs)
|
||||
if "old1" in params:
|
||||
warn_deprecated(...)
|
||||
old1, old2 = params.values() # note that locals() is ordered.
|
||||
else:
|
||||
new, = params.values()
|
||||
# do things with params
|
||||
|
||||
which allows *my_func* to be called either with two parameters (*old1* and
|
||||
*old2*) or a single one (*new*). Note that the new signature is given
|
||||
last, so that callers get a `TypeError` corresponding to the new signature
|
||||
if the arguments they passed in do not match any signature.
|
||||
"""
|
||||
# Rather than relying on locals() ordering, one could have just used func's
|
||||
# signature (``bound = inspect.signature(func).bind(*args, **kwargs);
|
||||
# bound.apply_defaults(); return bound``) but that is significantly slower.
|
||||
for i, func in enumerate(funcs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except TypeError:
|
||||
if i == len(funcs) - 1:
|
||||
raise
|
||||
|
||||
|
||||
def nargs_error(name, takes, given):
|
||||
"""Generate a TypeError to be raised by function calls with wrong arity."""
|
||||
return TypeError(f"{name}() takes {takes} positional arguments but "
|
||||
f"{given} were given")
|
||||
|
||||
|
||||
def kwarg_error(name, kw):
|
||||
"""
|
||||
Generate a TypeError to be raised by function calls with wrong kwarg.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The name of the calling function.
|
||||
kw : str or Iterable[str]
|
||||
Either the invalid keyword argument name, or an iterable yielding
|
||||
invalid keyword arguments (e.g., a ``kwargs`` dict).
|
||||
"""
|
||||
if not isinstance(kw, str):
|
||||
kw = next(iter(kw))
|
||||
return TypeError(f"{name}() got an unexpected keyword argument '{kw}'")
|
||||
|
||||
|
||||
def recursive_subclasses(cls):
|
||||
"""Yield *cls* and direct and indirect subclasses of *cls*."""
|
||||
yield cls
|
||||
for subcls in cls.__subclasses__():
|
||||
yield from recursive_subclasses(subcls)
|
||||
|
||||
|
||||
def warn_external(message, category=None):
|
||||
"""
|
||||
`warnings.warn` wrapper that sets *stacklevel* to "outside Matplotlib".
|
||||
|
||||
The original emitter of the warning can be obtained by patching this
|
||||
function back to `warnings.warn`, i.e. ``_api.warn_external =
|
||||
warnings.warn`` (or ``functools.partial(warnings.warn, stacklevel=2)``,
|
||||
etc.).
|
||||
"""
|
||||
kwargs = {}
|
||||
if sys.version_info[:2] >= (3, 12):
|
||||
# Go to Python's `site-packages` or `lib` from an editable install.
|
||||
basedir = pathlib.Path(__file__).parents[2]
|
||||
kwargs['skip_file_prefixes'] = (str(basedir / 'matplotlib'),
|
||||
str(basedir / 'mpl_toolkits'))
|
||||
else:
|
||||
frame = sys._getframe()
|
||||
for stacklevel in itertools.count(1):
|
||||
if frame is None:
|
||||
# when called in embedded context may hit frame is None
|
||||
kwargs['stacklevel'] = stacklevel
|
||||
break
|
||||
if not re.match(r"\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))",
|
||||
# Work around sphinx-gallery not setting __name__.
|
||||
frame.f_globals.get("__name__", "")):
|
||||
kwargs['stacklevel'] = stacklevel
|
||||
break
|
||||
frame = frame.f_back
|
||||
# preemptively break reference cycle between locals and the frame
|
||||
del frame
|
||||
warnings.warn(message, category, **kwargs)
|
||||
@@ -0,0 +1,59 @@
|
||||
from collections.abc import Callable, Generator, Iterable, Mapping, Sequence
|
||||
from typing import Any, TypeVar, overload
|
||||
from typing_extensions import Self # < Py 3.11
|
||||
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from .deprecation import ( # noqa: F401, re-exported API
|
||||
deprecated as deprecated,
|
||||
warn_deprecated as warn_deprecated,
|
||||
rename_parameter as rename_parameter,
|
||||
delete_parameter as delete_parameter,
|
||||
make_keyword_only as make_keyword_only,
|
||||
deprecate_method_override as deprecate_method_override,
|
||||
deprecate_privatize_attribute as deprecate_privatize_attribute,
|
||||
suppress_matplotlib_deprecation_warning as suppress_matplotlib_deprecation_warning,
|
||||
MatplotlibDeprecationWarning as MatplotlibDeprecationWarning,
|
||||
)
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
class classproperty(Any):
|
||||
def __init__(
|
||||
self,
|
||||
fget: Callable[[_T], Any],
|
||||
fset: None = ...,
|
||||
fdel: None = ...,
|
||||
doc: str | None = None,
|
||||
): ...
|
||||
@overload
|
||||
def __get__(self, instance: None, owner: None) -> Self: ...
|
||||
@overload
|
||||
def __get__(self, instance: object, owner: type[object]) -> Any: ...
|
||||
@property
|
||||
def fget(self) -> Callable[[_T], Any]: ...
|
||||
|
||||
def check_isinstance(
|
||||
types: type | tuple[type | None, ...], /, **kwargs: Any
|
||||
) -> None: ...
|
||||
def check_in_list(
|
||||
values: Sequence[Any], /, *, _print_supported_values: bool = ..., **kwargs: Any
|
||||
) -> None: ...
|
||||
def check_shape(shape: tuple[int | None, ...], /, **kwargs: NDArray) -> None: ...
|
||||
def check_getitem(mapping: Mapping[Any, Any], /, **kwargs: Any) -> Any: ...
|
||||
def caching_module_getattr(cls: type) -> Callable[[str], Any]: ...
|
||||
@overload
|
||||
def define_aliases(
|
||||
alias_d: dict[str, list[str]], cls: None = ...
|
||||
) -> Callable[[type[_T]], type[_T]]: ...
|
||||
@overload
|
||||
def define_aliases(alias_d: dict[str, list[str]], cls: type[_T]) -> type[_T]: ...
|
||||
def select_matching_signature(
|
||||
funcs: list[Callable], *args: Any, **kwargs: Any
|
||||
) -> Any: ...
|
||||
def nargs_error(name: str, takes: int | str, given: int) -> TypeError: ...
|
||||
def kwarg_error(name: str, kw: str | Iterable[str]) -> TypeError: ...
|
||||
def recursive_subclasses(cls: type) -> Generator[type, None, None]: ...
|
||||
def warn_external(
|
||||
message: str | Warning, category: type[Warning] | None = ...
|
||||
) -> None: ...
|
||||
@@ -0,0 +1,509 @@
|
||||
"""
|
||||
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")
|
||||
elif removal == '':
|
||||
macro, meso, *_ = since.split('.')
|
||||
removal = f'{macro}.{int(meso) + 2}'
|
||||
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 in %(removal)s" if removal else ""))) +
|
||||
"." +
|
||||
(" Use %(alternative)s instead." if alternative else "") +
|
||||
(" %(addendum)s" if addendum else ""))
|
||||
warning_cls = PendingDeprecationWarning if pending else MatplotlibDeprecationWarning
|
||||
return warning_cls(message % dict(
|
||||
func=name, name=name, obj_type=obj_type, since=since, removal=removal,
|
||||
alternative=alternative, addendum=addendum))
|
||||
|
||||
|
||||
def warn_deprecated(
|
||||
since, *, message='', name='', alternative='', pending=False,
|
||||
obj_type='', addendum='', removal=''):
|
||||
"""
|
||||
Display a standardized deprecation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
since : str
|
||||
The release at which this API became deprecated.
|
||||
message : str, optional
|
||||
Override the default deprecation message. The ``%(since)s``,
|
||||
``%(name)s``, ``%(alternative)s``, ``%(obj_type)s``, ``%(addendum)s``,
|
||||
and ``%(removal)s`` format specifiers will be replaced by the values
|
||||
of the respective arguments passed to this function.
|
||||
name : str, optional
|
||||
The name of the deprecated object.
|
||||
alternative : str, optional
|
||||
An alternative API that the user may use in place of the deprecated
|
||||
API. The deprecation warning will tell the user about this alternative
|
||||
if provided.
|
||||
pending : bool, optional
|
||||
If True, uses a PendingDeprecationWarning instead of a
|
||||
DeprecationWarning. Cannot be used together with *removal*.
|
||||
obj_type : str, optional
|
||||
The object type being deprecated.
|
||||
addendum : str, optional
|
||||
Additional text appended directly to the final message.
|
||||
removal : str, optional
|
||||
The expected removal version. With the default (an empty string), a
|
||||
removal version is automatically computed from *since*. Set to other
|
||||
Falsy values to not schedule a removal date. Cannot be used together
|
||||
with *pending*.
|
||||
|
||||
Examples
|
||||
--------
|
||||
::
|
||||
|
||||
# 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 in %(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 in %(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 in %(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__}(). If this error happens on a function with a "
|
||||
f"pyplot wrapper, make sure make_keyword_only() is the outermost decorator.")
|
||||
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 in %(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,75 @@
|
||||
from collections.abc import Callable
|
||||
import contextlib
|
||||
from typing import Any, Literal, ParamSpec, TypedDict, TypeVar, overload
|
||||
from typing_extensions import (
|
||||
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 | Literal[False]
|
||||
|
||||
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)
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
def display_is_valid() -> bool: ...
|
||||
def xdisplay_is_valid() -> bool: ...
|
||||
|
||||
def Win32_GetForegroundWindow() -> int | None: ...
|
||||
def Win32_SetForegroundWindow(hwnd: int) -> None: ...
|
||||
def Win32_SetProcessDpiAwareness_max() -> None: ...
|
||||
def Win32_SetCurrentProcessExplicitAppUserModelID(appid: str) -> None: ...
|
||||
def Win32_GetCurrentProcessExplicitAppUserModelID() -> str | None: ...
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
# auto-generated by https://github.com/trygvrad/multivariate_colormaps
|
||||
# date: 2024-05-28
|
||||
|
||||
from .colors import LinearSegmentedColormap, MultivarColormap
|
||||
import matplotlib as mpl
|
||||
_LUTSIZE = mpl.rcParams['image.lut']
|
||||
|
||||
_2VarAddA0_data = [[0.000, 0.000, 0.000],
|
||||
[0.020, 0.026, 0.031],
|
||||
[0.049, 0.068, 0.085],
|
||||
[0.075, 0.107, 0.135],
|
||||
[0.097, 0.144, 0.183],
|
||||
[0.116, 0.178, 0.231],
|
||||
[0.133, 0.212, 0.279],
|
||||
[0.148, 0.244, 0.326],
|
||||
[0.161, 0.276, 0.374],
|
||||
[0.173, 0.308, 0.422],
|
||||
[0.182, 0.339, 0.471],
|
||||
[0.190, 0.370, 0.521],
|
||||
[0.197, 0.400, 0.572],
|
||||
[0.201, 0.431, 0.623],
|
||||
[0.204, 0.461, 0.675],
|
||||
[0.204, 0.491, 0.728],
|
||||
[0.202, 0.520, 0.783],
|
||||
[0.197, 0.549, 0.838],
|
||||
[0.187, 0.577, 0.895]]
|
||||
|
||||
_2VarAddA1_data = [[0.000, 0.000, 0.000],
|
||||
[0.030, 0.023, 0.018],
|
||||
[0.079, 0.060, 0.043],
|
||||
[0.125, 0.093, 0.065],
|
||||
[0.170, 0.123, 0.083],
|
||||
[0.213, 0.151, 0.098],
|
||||
[0.255, 0.177, 0.110],
|
||||
[0.298, 0.202, 0.120],
|
||||
[0.341, 0.226, 0.128],
|
||||
[0.384, 0.249, 0.134],
|
||||
[0.427, 0.271, 0.138],
|
||||
[0.472, 0.292, 0.141],
|
||||
[0.517, 0.313, 0.142],
|
||||
[0.563, 0.333, 0.141],
|
||||
[0.610, 0.353, 0.139],
|
||||
[0.658, 0.372, 0.134],
|
||||
[0.708, 0.390, 0.127],
|
||||
[0.759, 0.407, 0.118],
|
||||
[0.813, 0.423, 0.105]]
|
||||
|
||||
_2VarSubA0_data = [[1.000, 1.000, 1.000],
|
||||
[0.959, 0.973, 0.986],
|
||||
[0.916, 0.948, 0.974],
|
||||
[0.874, 0.923, 0.965],
|
||||
[0.832, 0.899, 0.956],
|
||||
[0.790, 0.875, 0.948],
|
||||
[0.748, 0.852, 0.940],
|
||||
[0.707, 0.829, 0.934],
|
||||
[0.665, 0.806, 0.927],
|
||||
[0.624, 0.784, 0.921],
|
||||
[0.583, 0.762, 0.916],
|
||||
[0.541, 0.740, 0.910],
|
||||
[0.500, 0.718, 0.905],
|
||||
[0.457, 0.697, 0.901],
|
||||
[0.414, 0.675, 0.896],
|
||||
[0.369, 0.652, 0.892],
|
||||
[0.320, 0.629, 0.888],
|
||||
[0.266, 0.604, 0.884],
|
||||
[0.199, 0.574, 0.881]]
|
||||
|
||||
_2VarSubA1_data = [[1.000, 1.000, 1.000],
|
||||
[0.982, 0.967, 0.955],
|
||||
[0.966, 0.935, 0.908],
|
||||
[0.951, 0.902, 0.860],
|
||||
[0.937, 0.870, 0.813],
|
||||
[0.923, 0.838, 0.765],
|
||||
[0.910, 0.807, 0.718],
|
||||
[0.898, 0.776, 0.671],
|
||||
[0.886, 0.745, 0.624],
|
||||
[0.874, 0.714, 0.577],
|
||||
[0.862, 0.683, 0.530],
|
||||
[0.851, 0.653, 0.483],
|
||||
[0.841, 0.622, 0.435],
|
||||
[0.831, 0.592, 0.388],
|
||||
[0.822, 0.561, 0.340],
|
||||
[0.813, 0.530, 0.290],
|
||||
[0.806, 0.498, 0.239],
|
||||
[0.802, 0.464, 0.184],
|
||||
[0.801, 0.426, 0.119]]
|
||||
|
||||
_3VarAddA0_data = [[0.000, 0.000, 0.000],
|
||||
[0.018, 0.023, 0.028],
|
||||
[0.040, 0.056, 0.071],
|
||||
[0.059, 0.087, 0.110],
|
||||
[0.074, 0.114, 0.147],
|
||||
[0.086, 0.139, 0.183],
|
||||
[0.095, 0.163, 0.219],
|
||||
[0.101, 0.187, 0.255],
|
||||
[0.105, 0.209, 0.290],
|
||||
[0.107, 0.230, 0.326],
|
||||
[0.105, 0.251, 0.362],
|
||||
[0.101, 0.271, 0.398],
|
||||
[0.091, 0.291, 0.434],
|
||||
[0.075, 0.309, 0.471],
|
||||
[0.046, 0.325, 0.507],
|
||||
[0.021, 0.341, 0.546],
|
||||
[0.021, 0.363, 0.584],
|
||||
[0.022, 0.385, 0.622],
|
||||
[0.023, 0.408, 0.661]]
|
||||
|
||||
_3VarAddA1_data = [[0.000, 0.000, 0.000],
|
||||
[0.020, 0.024, 0.016],
|
||||
[0.047, 0.058, 0.034],
|
||||
[0.072, 0.088, 0.048],
|
||||
[0.093, 0.116, 0.059],
|
||||
[0.113, 0.142, 0.067],
|
||||
[0.131, 0.167, 0.071],
|
||||
[0.149, 0.190, 0.074],
|
||||
[0.166, 0.213, 0.074],
|
||||
[0.182, 0.235, 0.072],
|
||||
[0.198, 0.256, 0.068],
|
||||
[0.215, 0.276, 0.061],
|
||||
[0.232, 0.296, 0.051],
|
||||
[0.249, 0.314, 0.037],
|
||||
[0.270, 0.330, 0.018],
|
||||
[0.288, 0.347, 0.000],
|
||||
[0.302, 0.369, 0.000],
|
||||
[0.315, 0.391, 0.000],
|
||||
[0.328, 0.414, 0.000]]
|
||||
|
||||
_3VarAddA2_data = [[0.000, 0.000, 0.000],
|
||||
[0.029, 0.020, 0.023],
|
||||
[0.072, 0.045, 0.055],
|
||||
[0.111, 0.067, 0.084],
|
||||
[0.148, 0.085, 0.109],
|
||||
[0.184, 0.101, 0.133],
|
||||
[0.219, 0.115, 0.155],
|
||||
[0.254, 0.127, 0.176],
|
||||
[0.289, 0.138, 0.195],
|
||||
[0.323, 0.147, 0.214],
|
||||
[0.358, 0.155, 0.232],
|
||||
[0.393, 0.161, 0.250],
|
||||
[0.429, 0.166, 0.267],
|
||||
[0.467, 0.169, 0.283],
|
||||
[0.507, 0.168, 0.298],
|
||||
[0.546, 0.168, 0.313],
|
||||
[0.580, 0.172, 0.328],
|
||||
[0.615, 0.175, 0.341],
|
||||
[0.649, 0.178, 0.355]]
|
||||
|
||||
cmaps = {
|
||||
name: LinearSegmentedColormap.from_list(name, data, _LUTSIZE) for name, data in [
|
||||
('2VarAddA0', _2VarAddA0_data),
|
||||
('2VarAddA1', _2VarAddA1_data),
|
||||
('2VarSubA0', _2VarSubA0_data),
|
||||
('2VarSubA1', _2VarSubA1_data),
|
||||
('3VarAddA0', _3VarAddA0_data),
|
||||
('3VarAddA1', _3VarAddA1_data),
|
||||
('3VarAddA2', _3VarAddA2_data),
|
||||
]}
|
||||
|
||||
cmap_families = {
|
||||
'2VarAddA': MultivarColormap([cmaps[f'2VarAddA{i}'] for i in range(2)],
|
||||
'sRGB_add', name='2VarAddA'),
|
||||
'2VarSubA': MultivarColormap([cmaps[f'2VarSubA{i}'] for i in range(2)],
|
||||
'sRGB_sub', name='2VarSubA'),
|
||||
'3VarAddA': MultivarColormap([cmaps[f'3VarAddA{i}'] for i in range(3)],
|
||||
'sRGB_add', name='3VarAddA'),
|
||||
}
|
||||
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,805 @@
|
||||
"""
|
||||
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)
|
||||
|
||||
if ((suptitle := fig._suptitle) is not None and
|
||||
suptitle.get_in_layout() and suptitle._autopos):
|
||||
x, _ = suptitle.get_position()
|
||||
suptitle.set_position(
|
||||
(x, layoutgrids[fig].get_inner_bbox().y1 + h_pad))
|
||||
suptitle.set_verticalalignment('bottom')
|
||||
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.
|
||||
"""
|
||||
|
||||
axsdone = []
|
||||
for sfig in fig.subfigs:
|
||||
axsdone += 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() and
|
||||
a not in axsdone)]
|
||||
|
||||
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)
|
||||
|
||||
return axs
|
||||
|
||||
|
||||
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.get_figure(root=False)
|
||||
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.get_figure(root=False)
|
||||
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
|
||||
@@ -0,0 +1,141 @@
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
A class to substitute formatted placeholders in docstrings.
|
||||
|
||||
This is realized in a single instance ``_docstring.interpd``.
|
||||
|
||||
Use `~._ArtistPropertiesSubstition.register` to define placeholders and
|
||||
their substitution, e.g. ``_docstring.interpd.register(name="some value")``.
|
||||
|
||||
Use this as a decorator to apply the substitution::
|
||||
|
||||
@_docstring.interpd
|
||||
def some_func():
|
||||
'''Replace %(name)s.'''
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.params = _ArtistKwdocLoader()
|
||||
|
||||
def register(self, **kwargs):
|
||||
"""
|
||||
Register substitutions.
|
||||
|
||||
``_docstring.interpd.register(name="some value")`` makes "name" available
|
||||
as a named parameter that will be replaced by "some value".
|
||||
"""
|
||||
self.params.update(**kwargs)
|
||||
|
||||
def __call__(self, obj):
|
||||
if obj.__doc__:
|
||||
obj.__doc__ = inspect.cleandoc(obj.__doc__) % self.params
|
||||
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.
|
||||
interpd = _ArtistPropertiesSubstitution()
|
||||
@@ -0,0 +1,34 @@
|
||||
from collections.abc import Callable
|
||||
from typing import Any, 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:
|
||||
def __init__(self) -> None: ...
|
||||
def register(self, **kwargs) -> None: ...
|
||||
def __call__(self, obj: _T) -> _T: ...
|
||||
|
||||
|
||||
def copy(source: Any) -> Callable[[_T], _T]: ...
|
||||
|
||||
|
||||
dedent_interpd: _ArtistPropertiesSubstitution
|
||||
interpd: _ArtistPropertiesSubstitution
|
||||
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
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
|
||||
from matplotlib import _docstring
|
||||
|
||||
|
||||
class JoinStyle(str, Enum):
|
||||
"""
|
||||
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 = "miter"
|
||||
round = "round"
|
||||
bevel = "bevel"
|
||||
|
||||
@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, Enum):
|
||||
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 = "butt"
|
||||
projecting = "projecting"
|
||||
round = "round"
|
||||
|
||||
@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.register(
|
||||
JoinStyle=JoinStyle.input_description,
|
||||
CapStyle=CapStyle.input_description,
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
from typing import cast
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class JoinStyle(str, Enum):
|
||||
miter = "miter"
|
||||
round = "round"
|
||||
bevel = "bevel"
|
||||
@staticmethod
|
||||
def demo() -> None: ...
|
||||
|
||||
|
||||
class CapStyle(str, Enum):
|
||||
butt = "butt"
|
||||
projecting = "projecting"
|
||||
round = "round"
|
||||
|
||||
@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, one_of)
|
||||
|
||||
|
||||
_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)) | one_of(_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.parse_string(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.reset_cache()
|
||||
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))
|
||||
Executable
BIN
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)
|
||||
@@ -0,0 +1,547 @@
|
||||
"""
|
||||
A layoutgrid is a nrows by ncols set of boxes, meant to be used by
|
||||
`._constrained_layout`, each box is analogous to a subplotspec element of
|
||||
a gridspec.
|
||||
|
||||
Each box is defined by left[ncols], right[ncols], bottom[nrows] and top[nrows],
|
||||
and by two editable margins for each side. The main margin gets its value
|
||||
set by the size of ticklabels, titles, etc on each Axes that is in the figure.
|
||||
The outer margin is the padding around the Axes, and space for any
|
||||
colorbars.
|
||||
|
||||
The "inner" widths and heights of these boxes are then constrained to be the
|
||||
same (relative the values of `width_ratios[ncols]` and `height_ratios[nrows]`).
|
||||
|
||||
The layoutgrid is then constrained to be contained within a parent layoutgrid,
|
||||
its column(s) and row(s) specified when it is created.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import kiwisolver as kiwi
|
||||
import logging
|
||||
import numpy as np
|
||||
|
||||
import matplotlib as mpl
|
||||
import matplotlib.patches as mpatches
|
||||
from matplotlib.transforms import Bbox
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LayoutGrid:
|
||||
"""
|
||||
Analogous to a gridspec, and contained in another LayoutGrid.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, parent_pos=(0, 0),
|
||||
parent_inner=False, name='', ncols=1, nrows=1,
|
||||
h_pad=None, w_pad=None, width_ratios=None,
|
||||
height_ratios=None):
|
||||
Variable = kiwi.Variable
|
||||
self.parent_pos = parent_pos
|
||||
self.parent_inner = parent_inner
|
||||
self.name = name + seq_id()
|
||||
if isinstance(parent, LayoutGrid):
|
||||
self.name = f'{parent.name}.{self.name}'
|
||||
self.nrows = nrows
|
||||
self.ncols = ncols
|
||||
self.height_ratios = np.atleast_1d(height_ratios)
|
||||
if height_ratios is None:
|
||||
self.height_ratios = np.ones(nrows)
|
||||
self.width_ratios = np.atleast_1d(width_ratios)
|
||||
if width_ratios is None:
|
||||
self.width_ratios = np.ones(ncols)
|
||||
|
||||
sn = self.name + '_'
|
||||
if not isinstance(parent, LayoutGrid):
|
||||
# parent can be a rect if not a LayoutGrid
|
||||
# allows specifying a rectangle to contain the layout.
|
||||
self.solver = kiwi.Solver()
|
||||
else:
|
||||
parent.add_child(self, *parent_pos)
|
||||
self.solver = parent.solver
|
||||
# keep track of artist associated w/ this layout. Can be none
|
||||
self.artists = np.empty((nrows, ncols), dtype=object)
|
||||
self.children = np.empty((nrows, ncols), dtype=object)
|
||||
|
||||
self.margins = {}
|
||||
self.margin_vals = {}
|
||||
# all the boxes in each column share the same left/right margins:
|
||||
for todo in ['left', 'right', 'leftcb', 'rightcb']:
|
||||
# track the value so we can change only if a margin is larger
|
||||
# than the current value
|
||||
self.margin_vals[todo] = np.zeros(ncols)
|
||||
|
||||
sol = self.solver
|
||||
|
||||
self.lefts = [Variable(f'{sn}lefts[{i}]') for i in range(ncols)]
|
||||
self.rights = [Variable(f'{sn}rights[{i}]') for i in range(ncols)]
|
||||
for todo in ['left', 'right', 'leftcb', 'rightcb']:
|
||||
self.margins[todo] = [Variable(f'{sn}margins[{todo}][{i}]')
|
||||
for i in range(ncols)]
|
||||
for i in range(ncols):
|
||||
sol.addEditVariable(self.margins[todo][i], 'strong')
|
||||
|
||||
for todo in ['bottom', 'top', 'bottomcb', 'topcb']:
|
||||
self.margins[todo] = np.empty((nrows), dtype=object)
|
||||
self.margin_vals[todo] = np.zeros(nrows)
|
||||
|
||||
self.bottoms = [Variable(f'{sn}bottoms[{i}]') for i in range(nrows)]
|
||||
self.tops = [Variable(f'{sn}tops[{i}]') for i in range(nrows)]
|
||||
for todo in ['bottom', 'top', 'bottomcb', 'topcb']:
|
||||
self.margins[todo] = [Variable(f'{sn}margins[{todo}][{i}]')
|
||||
for i in range(nrows)]
|
||||
for i in range(nrows):
|
||||
sol.addEditVariable(self.margins[todo][i], 'strong')
|
||||
|
||||
# set these margins to zero by default. They will be edited as
|
||||
# children are filled.
|
||||
self.reset_margins()
|
||||
self.add_constraints(parent)
|
||||
|
||||
self.h_pad = h_pad
|
||||
self.w_pad = w_pad
|
||||
|
||||
def __repr__(self):
|
||||
str = f'LayoutBox: {self.name:25s} {self.nrows}x{self.ncols},\n'
|
||||
for i in range(self.nrows):
|
||||
for j in range(self.ncols):
|
||||
str += f'{i}, {j}: '\
|
||||
f'L{self.lefts[j].value():1.3f}, ' \
|
||||
f'B{self.bottoms[i].value():1.3f}, ' \
|
||||
f'R{self.rights[j].value():1.3f}, ' \
|
||||
f'T{self.tops[i].value():1.3f}, ' \
|
||||
f'ML{self.margins["left"][j].value():1.3f}, ' \
|
||||
f'MR{self.margins["right"][j].value():1.3f}, ' \
|
||||
f'MB{self.margins["bottom"][i].value():1.3f}, ' \
|
||||
f'MT{self.margins["top"][i].value():1.3f}, \n'
|
||||
return str
|
||||
|
||||
def reset_margins(self):
|
||||
"""
|
||||
Reset all the margins to zero. Must do this after changing
|
||||
figure size, for instance, because the relative size of the
|
||||
axes labels etc changes.
|
||||
"""
|
||||
for todo in ['left', 'right', 'bottom', 'top',
|
||||
'leftcb', 'rightcb', 'bottomcb', 'topcb']:
|
||||
self.edit_margins(todo, 0.0)
|
||||
|
||||
def add_constraints(self, parent):
|
||||
# define self-consistent constraints
|
||||
self.hard_constraints()
|
||||
# define relationship with parent layoutgrid:
|
||||
self.parent_constraints(parent)
|
||||
# define relative widths of the grid cells to each other
|
||||
# and stack horizontally and vertically.
|
||||
self.grid_constraints()
|
||||
|
||||
def hard_constraints(self):
|
||||
"""
|
||||
These are the redundant constraints, plus ones that make the
|
||||
rest of the code easier.
|
||||
"""
|
||||
for i in range(self.ncols):
|
||||
hc = [self.rights[i] >= self.lefts[i],
|
||||
(self.rights[i] - self.margins['right'][i] -
|
||||
self.margins['rightcb'][i] >=
|
||||
self.lefts[i] - self.margins['left'][i] -
|
||||
self.margins['leftcb'][i])
|
||||
]
|
||||
for c in hc:
|
||||
self.solver.addConstraint(c | 'required')
|
||||
|
||||
for i in range(self.nrows):
|
||||
hc = [self.tops[i] >= self.bottoms[i],
|
||||
(self.tops[i] - self.margins['top'][i] -
|
||||
self.margins['topcb'][i] >=
|
||||
self.bottoms[i] - self.margins['bottom'][i] -
|
||||
self.margins['bottomcb'][i])
|
||||
]
|
||||
for c in hc:
|
||||
self.solver.addConstraint(c | 'required')
|
||||
|
||||
def add_child(self, child, i=0, j=0):
|
||||
# np.ix_ returns the cross product of i and j indices
|
||||
self.children[np.ix_(np.atleast_1d(i), np.atleast_1d(j))] = child
|
||||
|
||||
def parent_constraints(self, parent):
|
||||
# constraints that are due to the parent...
|
||||
# i.e. the first column's left is equal to the
|
||||
# parent's left, the last column right equal to the
|
||||
# parent's right...
|
||||
if not isinstance(parent, LayoutGrid):
|
||||
# specify a rectangle in figure coordinates
|
||||
hc = [self.lefts[0] == parent[0],
|
||||
self.rights[-1] == parent[0] + parent[2],
|
||||
# top and bottom reversed order...
|
||||
self.tops[0] == parent[1] + parent[3],
|
||||
self.bottoms[-1] == parent[1]]
|
||||
else:
|
||||
rows, cols = self.parent_pos
|
||||
rows = np.atleast_1d(rows)
|
||||
cols = np.atleast_1d(cols)
|
||||
|
||||
left = parent.lefts[cols[0]]
|
||||
right = parent.rights[cols[-1]]
|
||||
top = parent.tops[rows[0]]
|
||||
bottom = parent.bottoms[rows[-1]]
|
||||
if self.parent_inner:
|
||||
# the layout grid is contained inside the inner
|
||||
# grid of the parent.
|
||||
left += parent.margins['left'][cols[0]]
|
||||
left += parent.margins['leftcb'][cols[0]]
|
||||
right -= parent.margins['right'][cols[-1]]
|
||||
right -= parent.margins['rightcb'][cols[-1]]
|
||||
top -= parent.margins['top'][rows[0]]
|
||||
top -= parent.margins['topcb'][rows[0]]
|
||||
bottom += parent.margins['bottom'][rows[-1]]
|
||||
bottom += parent.margins['bottomcb'][rows[-1]]
|
||||
hc = [self.lefts[0] == left,
|
||||
self.rights[-1] == right,
|
||||
# from top to bottom
|
||||
self.tops[0] == top,
|
||||
self.bottoms[-1] == bottom]
|
||||
for c in hc:
|
||||
self.solver.addConstraint(c | 'required')
|
||||
|
||||
def grid_constraints(self):
|
||||
# constrain the ratio of the inner part of the grids
|
||||
# to be the same (relative to width_ratios)
|
||||
|
||||
# constrain widths:
|
||||
w = (self.rights[0] - self.margins['right'][0] -
|
||||
self.margins['rightcb'][0])
|
||||
w = (w - self.lefts[0] - self.margins['left'][0] -
|
||||
self.margins['leftcb'][0])
|
||||
w0 = w / self.width_ratios[0]
|
||||
# from left to right
|
||||
for i in range(1, self.ncols):
|
||||
w = (self.rights[i] - self.margins['right'][i] -
|
||||
self.margins['rightcb'][i])
|
||||
w = (w - self.lefts[i] - self.margins['left'][i] -
|
||||
self.margins['leftcb'][i])
|
||||
c = (w == w0 * self.width_ratios[i])
|
||||
self.solver.addConstraint(c | 'strong')
|
||||
# constrain the grid cells to be directly next to each other.
|
||||
c = (self.rights[i - 1] == self.lefts[i])
|
||||
self.solver.addConstraint(c | 'strong')
|
||||
|
||||
# constrain heights:
|
||||
h = self.tops[0] - self.margins['top'][0] - self.margins['topcb'][0]
|
||||
h = (h - self.bottoms[0] - self.margins['bottom'][0] -
|
||||
self.margins['bottomcb'][0])
|
||||
h0 = h / self.height_ratios[0]
|
||||
# from top to bottom:
|
||||
for i in range(1, self.nrows):
|
||||
h = (self.tops[i] - self.margins['top'][i] -
|
||||
self.margins['topcb'][i])
|
||||
h = (h - self.bottoms[i] - self.margins['bottom'][i] -
|
||||
self.margins['bottomcb'][i])
|
||||
c = (h == h0 * self.height_ratios[i])
|
||||
self.solver.addConstraint(c | 'strong')
|
||||
# constrain the grid cells to be directly above each other.
|
||||
c = (self.bottoms[i - 1] == self.tops[i])
|
||||
self.solver.addConstraint(c | 'strong')
|
||||
|
||||
# Margin editing: The margins are variable and meant to
|
||||
# contain things of a fixed size like axes labels, tick labels, titles
|
||||
# etc
|
||||
def edit_margin(self, todo, size, cell):
|
||||
"""
|
||||
Change the size of the margin for one cell.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
todo : string (one of 'left', 'right', 'bottom', 'top')
|
||||
margin to alter.
|
||||
|
||||
size : float
|
||||
Size of the margin. If it is larger than the existing minimum it
|
||||
updates the margin size. Fraction of figure size.
|
||||
|
||||
cell : int
|
||||
Cell column or row to edit.
|
||||
"""
|
||||
self.solver.suggestValue(self.margins[todo][cell], size)
|
||||
self.margin_vals[todo][cell] = size
|
||||
|
||||
def edit_margin_min(self, todo, size, cell=0):
|
||||
"""
|
||||
Change the minimum size of the margin for one cell.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
todo : string (one of 'left', 'right', 'bottom', 'top')
|
||||
margin to alter.
|
||||
|
||||
size : float
|
||||
Minimum size of the margin . If it is larger than the
|
||||
existing minimum it updates the margin size. Fraction of
|
||||
figure size.
|
||||
|
||||
cell : int
|
||||
Cell column or row to edit.
|
||||
"""
|
||||
|
||||
if size > self.margin_vals[todo][cell]:
|
||||
self.edit_margin(todo, size, cell)
|
||||
|
||||
def edit_margins(self, todo, size):
|
||||
"""
|
||||
Change the size of all the margin of all the cells in the layout grid.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
todo : string (one of 'left', 'right', 'bottom', 'top')
|
||||
margin to alter.
|
||||
|
||||
size : float
|
||||
Size to set the margins. Fraction of figure size.
|
||||
"""
|
||||
|
||||
for i in range(len(self.margin_vals[todo])):
|
||||
self.edit_margin(todo, size, i)
|
||||
|
||||
def edit_all_margins_min(self, todo, size):
|
||||
"""
|
||||
Change the minimum size of all the margin of all
|
||||
the cells in the layout grid.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
todo : {'left', 'right', 'bottom', 'top'}
|
||||
The margin to alter.
|
||||
|
||||
size : float
|
||||
Minimum size of the margin. If it is larger than the
|
||||
existing minimum it updates the margin size. Fraction of
|
||||
figure size.
|
||||
"""
|
||||
|
||||
for i in range(len(self.margin_vals[todo])):
|
||||
self.edit_margin_min(todo, size, i)
|
||||
|
||||
def edit_outer_margin_mins(self, margin, ss):
|
||||
"""
|
||||
Edit all four margin minimums in one statement.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
margin : dict
|
||||
size of margins in a dict with keys 'left', 'right', 'bottom',
|
||||
'top'
|
||||
|
||||
ss : SubplotSpec
|
||||
defines the subplotspec these margins should be applied to
|
||||
"""
|
||||
|
||||
self.edit_margin_min('left', margin['left'], ss.colspan.start)
|
||||
self.edit_margin_min('leftcb', margin['leftcb'], ss.colspan.start)
|
||||
self.edit_margin_min('right', margin['right'], ss.colspan.stop - 1)
|
||||
self.edit_margin_min('rightcb', margin['rightcb'], ss.colspan.stop - 1)
|
||||
# rows are from the top down:
|
||||
self.edit_margin_min('top', margin['top'], ss.rowspan.start)
|
||||
self.edit_margin_min('topcb', margin['topcb'], ss.rowspan.start)
|
||||
self.edit_margin_min('bottom', margin['bottom'], ss.rowspan.stop - 1)
|
||||
self.edit_margin_min('bottomcb', margin['bottomcb'],
|
||||
ss.rowspan.stop - 1)
|
||||
|
||||
def get_margins(self, todo, col):
|
||||
"""Return the margin at this position"""
|
||||
return self.margin_vals[todo][col]
|
||||
|
||||
def get_outer_bbox(self, rows=0, cols=0):
|
||||
"""
|
||||
Return the outer bounding box of the subplot specs
|
||||
given by rows and cols. rows and cols can be spans.
|
||||
"""
|
||||
rows = np.atleast_1d(rows)
|
||||
cols = np.atleast_1d(cols)
|
||||
|
||||
bbox = Bbox.from_extents(
|
||||
self.lefts[cols[0]].value(),
|
||||
self.bottoms[rows[-1]].value(),
|
||||
self.rights[cols[-1]].value(),
|
||||
self.tops[rows[0]].value())
|
||||
return bbox
|
||||
|
||||
def get_inner_bbox(self, rows=0, cols=0):
|
||||
"""
|
||||
Return the inner bounding box of the subplot specs
|
||||
given by rows and cols. rows and cols can be spans.
|
||||
"""
|
||||
rows = np.atleast_1d(rows)
|
||||
cols = np.atleast_1d(cols)
|
||||
|
||||
bbox = Bbox.from_extents(
|
||||
(self.lefts[cols[0]].value() +
|
||||
self.margins['left'][cols[0]].value() +
|
||||
self.margins['leftcb'][cols[0]].value()),
|
||||
(self.bottoms[rows[-1]].value() +
|
||||
self.margins['bottom'][rows[-1]].value() +
|
||||
self.margins['bottomcb'][rows[-1]].value()),
|
||||
(self.rights[cols[-1]].value() -
|
||||
self.margins['right'][cols[-1]].value() -
|
||||
self.margins['rightcb'][cols[-1]].value()),
|
||||
(self.tops[rows[0]].value() -
|
||||
self.margins['top'][rows[0]].value() -
|
||||
self.margins['topcb'][rows[0]].value())
|
||||
)
|
||||
return bbox
|
||||
|
||||
def get_bbox_for_cb(self, rows=0, cols=0):
|
||||
"""
|
||||
Return the bounding box that includes the
|
||||
decorations but, *not* the colorbar...
|
||||
"""
|
||||
rows = np.atleast_1d(rows)
|
||||
cols = np.atleast_1d(cols)
|
||||
|
||||
bbox = Bbox.from_extents(
|
||||
(self.lefts[cols[0]].value() +
|
||||
self.margins['leftcb'][cols[0]].value()),
|
||||
(self.bottoms[rows[-1]].value() +
|
||||
self.margins['bottomcb'][rows[-1]].value()),
|
||||
(self.rights[cols[-1]].value() -
|
||||
self.margins['rightcb'][cols[-1]].value()),
|
||||
(self.tops[rows[0]].value() -
|
||||
self.margins['topcb'][rows[0]].value())
|
||||
)
|
||||
return bbox
|
||||
|
||||
def get_left_margin_bbox(self, rows=0, cols=0):
|
||||
"""
|
||||
Return the left margin bounding box of the subplot specs
|
||||
given by rows and cols. rows and cols can be spans.
|
||||
"""
|
||||
rows = np.atleast_1d(rows)
|
||||
cols = np.atleast_1d(cols)
|
||||
|
||||
bbox = Bbox.from_extents(
|
||||
(self.lefts[cols[0]].value() +
|
||||
self.margins['leftcb'][cols[0]].value()),
|
||||
(self.bottoms[rows[-1]].value()),
|
||||
(self.lefts[cols[0]].value() +
|
||||
self.margins['leftcb'][cols[0]].value() +
|
||||
self.margins['left'][cols[0]].value()),
|
||||
(self.tops[rows[0]].value()))
|
||||
return bbox
|
||||
|
||||
def get_bottom_margin_bbox(self, rows=0, cols=0):
|
||||
"""
|
||||
Return the left margin bounding box of the subplot specs
|
||||
given by rows and cols. rows and cols can be spans.
|
||||
"""
|
||||
rows = np.atleast_1d(rows)
|
||||
cols = np.atleast_1d(cols)
|
||||
|
||||
bbox = Bbox.from_extents(
|
||||
(self.lefts[cols[0]].value()),
|
||||
(self.bottoms[rows[-1]].value() +
|
||||
self.margins['bottomcb'][rows[-1]].value()),
|
||||
(self.rights[cols[-1]].value()),
|
||||
(self.bottoms[rows[-1]].value() +
|
||||
self.margins['bottom'][rows[-1]].value() +
|
||||
self.margins['bottomcb'][rows[-1]].value()
|
||||
))
|
||||
return bbox
|
||||
|
||||
def get_right_margin_bbox(self, rows=0, cols=0):
|
||||
"""
|
||||
Return the left margin bounding box of the subplot specs
|
||||
given by rows and cols. rows and cols can be spans.
|
||||
"""
|
||||
rows = np.atleast_1d(rows)
|
||||
cols = np.atleast_1d(cols)
|
||||
|
||||
bbox = Bbox.from_extents(
|
||||
(self.rights[cols[-1]].value() -
|
||||
self.margins['right'][cols[-1]].value() -
|
||||
self.margins['rightcb'][cols[-1]].value()),
|
||||
(self.bottoms[rows[-1]].value()),
|
||||
(self.rights[cols[-1]].value() -
|
||||
self.margins['rightcb'][cols[-1]].value()),
|
||||
(self.tops[rows[0]].value()))
|
||||
return bbox
|
||||
|
||||
def get_top_margin_bbox(self, rows=0, cols=0):
|
||||
"""
|
||||
Return the left margin bounding box of the subplot specs
|
||||
given by rows and cols. rows and cols can be spans.
|
||||
"""
|
||||
rows = np.atleast_1d(rows)
|
||||
cols = np.atleast_1d(cols)
|
||||
|
||||
bbox = Bbox.from_extents(
|
||||
(self.lefts[cols[0]].value()),
|
||||
(self.tops[rows[0]].value() -
|
||||
self.margins['topcb'][rows[0]].value()),
|
||||
(self.rights[cols[-1]].value()),
|
||||
(self.tops[rows[0]].value() -
|
||||
self.margins['topcb'][rows[0]].value() -
|
||||
self.margins['top'][rows[0]].value()))
|
||||
return bbox
|
||||
|
||||
def update_variables(self):
|
||||
"""
|
||||
Update the variables for the solver attached to this layoutgrid.
|
||||
"""
|
||||
self.solver.updateVariables()
|
||||
|
||||
_layoutboxobjnum = itertools.count()
|
||||
|
||||
|
||||
def seq_id():
|
||||
"""Generate a short sequential id for layoutbox objects."""
|
||||
return '%06d' % next(_layoutboxobjnum)
|
||||
|
||||
|
||||
def plot_children(fig, lg=None, level=0):
|
||||
"""Simple plotting to show where boxes are."""
|
||||
if lg is None:
|
||||
_layoutgrids = fig.get_layout_engine().execute(fig)
|
||||
lg = _layoutgrids[fig]
|
||||
colors = mpl.rcParams["axes.prop_cycle"].by_key()["color"]
|
||||
col = colors[level]
|
||||
for i in range(lg.nrows):
|
||||
for j in range(lg.ncols):
|
||||
bb = lg.get_outer_bbox(rows=i, cols=j)
|
||||
fig.add_artist(
|
||||
mpatches.Rectangle(bb.p0, bb.width, bb.height, linewidth=1,
|
||||
edgecolor='0.7', facecolor='0.7',
|
||||
alpha=0.2, transform=fig.transFigure,
|
||||
zorder=-3))
|
||||
bbi = lg.get_inner_bbox(rows=i, cols=j)
|
||||
fig.add_artist(
|
||||
mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=2,
|
||||
edgecolor=col, facecolor='none',
|
||||
transform=fig.transFigure, zorder=-2))
|
||||
|
||||
bbi = lg.get_left_margin_bbox(rows=i, cols=j)
|
||||
fig.add_artist(
|
||||
mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=0,
|
||||
edgecolor='none', alpha=0.2,
|
||||
facecolor=[0.5, 0.7, 0.5],
|
||||
transform=fig.transFigure, zorder=-2))
|
||||
bbi = lg.get_right_margin_bbox(rows=i, cols=j)
|
||||
fig.add_artist(
|
||||
mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=0,
|
||||
edgecolor='none', alpha=0.2,
|
||||
facecolor=[0.7, 0.5, 0.5],
|
||||
transform=fig.transFigure, zorder=-2))
|
||||
bbi = lg.get_bottom_margin_bbox(rows=i, cols=j)
|
||||
fig.add_artist(
|
||||
mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=0,
|
||||
edgecolor='none', alpha=0.2,
|
||||
facecolor=[0.5, 0.5, 0.7],
|
||||
transform=fig.transFigure, zorder=-2))
|
||||
bbi = lg.get_top_margin_bbox(rows=i, cols=j)
|
||||
fig.add_artist(
|
||||
mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=0,
|
||||
edgecolor='none', alpha=0.2,
|
||||
facecolor=[0.7, 0.2, 0.7],
|
||||
transform=fig.transFigure, zorder=-2))
|
||||
for ch in lg.children.flat:
|
||||
if ch is not None:
|
||||
plot_children(fig, ch, level=level+1)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Executable
BIN
Binary file not shown.
@@ -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): ...
|
||||
@@ -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: ...
|
||||
Executable
BIN
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 FT2Font, Kerning, LoadFlags
|
||||
|
||||
|
||||
@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 : Kerning
|
||||
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=LoadFlags.NO_HINTING)
|
||||
yield LayoutItem(font, char, glyph_idx, x, kern)
|
||||
x += glyph.linearHoriAdvance / 65536
|
||||
prev_glyph_idx = glyph_idx
|
||||
@@ -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
|
||||
@@ -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
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
# This is a private module implemented in C++
|
||||
from typing import final
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
@final
|
||||
class TrapezoidMapTriFinder:
|
||||
def __init__(self, triangulation: Triangulation): ...
|
||||
def find_many(self, x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]) -> npt.NDArray[np.int_]: ...
|
||||
def get_tree_stats(self) -> list[int | float]: ...
|
||||
def initialize(self) -> None: ...
|
||||
def print_tree(self) -> None: ...
|
||||
|
||||
@final
|
||||
class TriContourGenerator:
|
||||
def __init__(self, triangulation: Triangulation, z: npt.NDArray[np.float64]): ...
|
||||
def create_contour(self, level: float) -> tuple[list[float], list[int]]: ...
|
||||
def create_filled_contour(self, lower_level: float, upper_level: float) -> tuple[list[float], list[int]]: ...
|
||||
|
||||
@final
|
||||
class Triangulation:
|
||||
def __init__(
|
||||
self,
|
||||
x: npt.NDArray[np.float64],
|
||||
y: npt.NDArray[np.float64],
|
||||
triangles: npt.NDArray[np.int_],
|
||||
mask: npt.NDArray[np.bool_] | tuple[()],
|
||||
edges: npt.NDArray[np.int_] | tuple[()],
|
||||
neighbors: npt.NDArray[np.int_] | tuple[()],
|
||||
correct_triangle_orientation: bool,
|
||||
): ...
|
||||
def calculate_plane_coefficients(self, z: npt.ArrayLike) -> npt.NDArray[np.float64]: ...
|
||||
def get_edges(self) -> npt.NDArray[np.int_]: ...
|
||||
def get_neighbors(self) -> npt.NDArray[np.int_]: ...
|
||||
def set_mask(self, mask: npt.NDArray[np.bool_] | tuple[()]) -> None: ...
|
||||
@@ -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',
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
version = "3.10.9"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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: ...
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,199 @@
|
||||
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, Literal, NamedTuple, TextIO, overload, TypeVar
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
_T_Artist = TypeVar("_T_Artist", bound=Artist)
|
||||
|
||||
def allow_rasterization(draw): ...
|
||||
|
||||
class _XYPair(NamedTuple):
|
||||
x: ArrayLike
|
||||
y: ArrayLike
|
||||
|
||||
class _Unset: ...
|
||||
|
||||
class Artist:
|
||||
zorder: float
|
||||
stale_callback: Callable[[Artist, bool], None] | None
|
||||
@property
|
||||
def figure(self) -> Figure | SubFigure: ...
|
||||
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]: ...
|
||||
@overload
|
||||
def get_figure(self, root: Literal[True]) -> Figure | None: ...
|
||||
@overload
|
||||
def get_figure(self, root: Literal[False]) -> Figure | SubFigure | None: ...
|
||||
@overload
|
||||
def get_figure(self, root: bool = ...) -> Figure | SubFigure | None: ...
|
||||
def set_figure(self, fig: Figure | SubFigure) -> 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]: ...
|
||||
|
||||
@overload
|
||||
def findobj(
|
||||
self,
|
||||
match: None | Callable[[Artist], bool] = ...,
|
||||
include_self: bool = ...,
|
||||
) -> list[Artist]: ...
|
||||
|
||||
@overload
|
||||
def findobj(
|
||||
self,
|
||||
match: type[_T_Artist],
|
||||
include_self: bool = ...,
|
||||
) -> list[_T_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
|
||||
|
||||
# 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]: ...
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,780 @@
|
||||
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,
|
||||
FillBetweenPolyCollection,
|
||||
LineCollection,
|
||||
PathCollection,
|
||||
PolyCollection,
|
||||
EventCollection,
|
||||
QuadMesh,
|
||||
)
|
||||
from matplotlib.colorizer import Colorizer
|
||||
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.inset import InsetIndicator
|
||||
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
|
||||
from matplotlib.typing import CoordsType
|
||||
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] | None = ...,
|
||||
inset_ax: Axes | None = ...,
|
||||
*,
|
||||
transform: Transform | None = ...,
|
||||
facecolor: ColorType = ...,
|
||||
edgecolor: ColorType = ...,
|
||||
alpha: float = ...,
|
||||
zorder: float | None = ...,
|
||||
**kwargs
|
||||
) -> InsetIndicator: ...
|
||||
def indicate_inset_zoom(self, inset_ax: Axes, **kwargs) -> InsetIndicator: ...
|
||||
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: CoordsType = ...,
|
||||
textcoords: CoordsType | 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 = ...,
|
||||
orientation: Literal["vertical", "horizontal"] = ...,
|
||||
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 | None = ...,
|
||||
orientation: Literal["vertical", "horizontal"] = ...,
|
||||
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 = ...,
|
||||
colorizer: Colorizer | 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 = ...,
|
||||
colorizer: Colorizer | None = ...,
|
||||
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
|
||||
) -> FillBetweenPolyCollection: ...
|
||||
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
|
||||
) -> FillBetweenPolyCollection: ...
|
||||
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 = ...,
|
||||
colorizer: Colorizer | None = ...,
|
||||
origin: Literal["upper", "lower"] | None = ...,
|
||||
extent: tuple[float, float, float, float] | None = ...,
|
||||
interpolation_stage: Literal["data", "rgba", "auto"] | 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 = ...,
|
||||
colorizer: Colorizer | 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 = ...,
|
||||
colorizer: Colorizer | 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 = ...,
|
||||
colorizer: Colorizer | 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", "horizontal"]=...,
|
||||
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 | None = ...,
|
||||
orientation: Literal["vertical", "horizontal"] = ...,
|
||||
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 | None = ...,
|
||||
orientation: Literal["vertical", "horizontal"] = ...,
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.colorizer import ColorizingArtist
|
||||
from matplotlib.legend import Legend
|
||||
from matplotlib.lines import Line2D
|
||||
from matplotlib.gridspec import SubplotSpec, GridSpec
|
||||
from matplotlib.figure import Figure, SubFigure
|
||||
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 | SubFigure) -> 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) -> 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 | None: ...
|
||||
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: ColorizingArtist) -> 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,322 @@
|
||||
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
|
||||
|
||||
fig = self._parent.get_figure(root=False)
|
||||
if self._orientation == 'x':
|
||||
super().__init__(fig, [0, 1., 1, 0.0001], **kwargs)
|
||||
self._axis = self.xaxis
|
||||
self._locstrings = ['top', 'bottom']
|
||||
self._otherstrings = ['left', 'right']
|
||||
else: # 'y'
|
||||
super().__init__(fig, [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.register(_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: ...
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,280 @@
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
import datetime
|
||||
from typing import Any, Literal, overload
|
||||
from typing_extensions import Self # < Py 3.11
|
||||
|
||||
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
|
||||
from matplotlib.units import ConversionInterface
|
||||
|
||||
|
||||
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_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: ...
|
||||
@overload
|
||||
def __get__(self, instance: None, owner: None) -> Self: ...
|
||||
@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 get_converter(self) -> ConversionInterface | None: ...
|
||||
def set_converter(self, converter: ConversionInterface) -> None: ...
|
||||
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: ...
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,482 @@
|
||||
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 set_hatch_linewidth(self, hatch_linewidth: float) -> 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: ...
|
||||
|
||||
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
|
||||
guiEvent: Any
|
||||
def __init__(
|
||||
self, name: str, canvas: FigureCanvasBase, guiEvent: Any | None = ...
|
||||
) -> None: ...
|
||||
|
||||
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):
|
||||
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 = 1
|
||||
MIDDLE = 2
|
||||
RIGHT = 3
|
||||
BACK = 8
|
||||
FORWARD = 9
|
||||
|
||||
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 = ...,
|
||||
*,
|
||||
buttons: Iterable[MouseButton] | 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 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 = ""
|
||||
PAN = "pan/zoom"
|
||||
ZOOM = "zoom rect"
|
||||
|
||||
class NavigationToolbar2:
|
||||
toolitems: tuple[tuple[str, ...] | tuple[None, ...], ...]
|
||||
UNKNOWN_SAVED_STATUS: object
|
||||
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) -> str | None | object: ...
|
||||
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: ...
|
||||
@@ -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: ...
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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, cast
|
||||
|
||||
class Cursors(enum.IntEnum):
|
||||
POINTER = cast(int, ...)
|
||||
HAND = cast(int, ...)
|
||||
SELECT_REGION = cast(int, ...)
|
||||
MOVE = cast(int, ...)
|
||||
WAIT = cast(int, ...)
|
||||
RESIZE_HORIZONTAL = cast(int, ...)
|
||||
RESIZE_VERTICAL = cast(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, type[ToolBase]]
|
||||
default_toolbar_tools: list[list[str | list[str]]]
|
||||
|
||||
def add_tools_to_manager(
|
||||
toolmanager: ToolManager, tools: dict[str, type[ToolBase]] | None = ...
|
||||
) -> None: ...
|
||||
def add_tools_to_container(container: ToolContainerBase, tools: list[Any] | None = ...) -> 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
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
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:
|
||||
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,189 @@
|
||||
"""
|
||||
Common functionality between the PDF and PS backends.
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
import functools
|
||||
import logging
|
||||
|
||||
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.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fontfile : str
|
||||
Path to the font file
|
||||
characters : str
|
||||
Continuous set of characters to include in subset
|
||||
|
||||
Returns
|
||||
-------
|
||||
fontTools.ttLib.ttFont.TTFont
|
||||
An open font object representing the subset, which needs to
|
||||
be closed by the caller.
|
||||
"""
|
||||
|
||||
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).
|
||||
'MERG', # Merge Table.
|
||||
'TSIV', # Microsoft Visual TrueType extension.
|
||||
'Zapf', # Information about the individual glyphs in the font.
|
||||
'bdat', # The bitmap data table.
|
||||
'bloc', # The bitmap location table.
|
||||
'cidg', # CID to Glyph ID table (Apple Advanced Typography).
|
||||
'fdsc', # The font descriptors table.
|
||||
'feat', # Feature name table (Apple Advanced Typography).
|
||||
'fmtx', # The Font Metrics Table.
|
||||
'fond', # Data-fork font information (Apple Advanced Typography).
|
||||
'just', # The justification table (Apple Advanced Typography).
|
||||
'kerx', # An extended kerning table (Apple Advanced Typography).
|
||||
'ltag', # Language Tag.
|
||||
'morx', # Extended Glyph Metamorphosis Table.
|
||||
'trak', # Tracking table.
|
||||
'xref', # The cross-reference table (some Apple font tooling information).
|
||||
]
|
||||
# if fontfile is a ttc, specify font number
|
||||
if fontfile.endswith(".ttc"):
|
||||
options.font_number = 0
|
||||
|
||||
font = subset.load_font(fontfile, options)
|
||||
subsetter = subset.Subsetter(options=options)
|
||||
subsetter.populate(text=characters)
|
||||
subsetter.subset(font)
|
||||
return font
|
||||
|
||||
|
||||
def font_as_file(font):
|
||||
"""
|
||||
Convert a TTFont object into a file-like object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
font : fontTools.ttLib.ttFont.TTFont
|
||||
A font object
|
||||
|
||||
Returns
|
||||
-------
|
||||
BytesIO
|
||||
A file object with the font saved into it
|
||||
"""
|
||||
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.LoadFlags.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)
|
||||
try:
|
||||
font = font_manager.get_font(fnames)
|
||||
font.clear()
|
||||
font.set_size(prop.get_size_in_points(), 72)
|
||||
return font
|
||||
except RuntimeError:
|
||||
logging.getLogger(__name__).warning(
|
||||
"The PostScript/PDF backend does not currently "
|
||||
"support the selected font (%s).", fnames)
|
||||
raise
|
||||
File diff suppressed because it is too large
Load Diff
Executable
BIN
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,528 @@
|
||||
"""
|
||||
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 LoadFlags
|
||||
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': LoadFlags.DEFAULT,
|
||||
'no_autohint': LoadFlags.NO_AUTOHINT,
|
||||
'force_autohint': LoadFlags.FORCE_AUTOHINT,
|
||||
'no_hinting': LoadFlags.NO_HINTING,
|
||||
True: LoadFlags.FORCE_AUTOHINT,
|
||||
False: LoadFlags.NO_HINTING,
|
||||
'either': LoadFlags.DEFAULT,
|
||||
'native': LoadFlags.NO_AUTOHINT,
|
||||
'auto': LoadFlags.FORCE_AUTOHINT,
|
||||
'none': LoadFlags.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()
|
||||
|
||||
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
|
||||
|
||||
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,596 @@
|
||||
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, MouseButton,
|
||||
CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
|
||||
|
||||
try:
|
||||
from gi import require_version as gi_require_version
|
||||
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),
|
||||
buttons=self._mpl_buttons(event.state),
|
||||
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_buttons(event_state):
|
||||
modifiers = [
|
||||
(MouseButton.LEFT, Gdk.ModifierType.BUTTON1_MASK),
|
||||
(MouseButton.MIDDLE, Gdk.ModifierType.BUTTON2_MASK),
|
||||
(MouseButton.RIGHT, Gdk.ModifierType.BUTTON3_MASK),
|
||||
(MouseButton.BACK, Gdk.ModifierType.BUTTON4_MASK),
|
||||
(MouseButton.FORWARD, Gdk.ModifierType.BUTTON5_MASK),
|
||||
]
|
||||
# State *before* press/release.
|
||||
return [name for name, mask in modifiers if event_state & mask]
|
||||
|
||||
@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",
|
||||
transient_for=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 None
|
||||
# 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)
|
||||
return fname
|
||||
except Exception as e:
|
||||
dialog = Gtk.MessageDialog(
|
||||
transient_for=self.canvas.get_toplevel(), text=str(e),
|
||||
message_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 _BackendGTK3Agg(_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,642 @@
|
||||
import functools
|
||||
import io
|
||||
import os
|
||||
|
||||
import matplotlib as mpl
|
||||
from matplotlib import _api, backend_tools, cbook
|
||||
from matplotlib.backend_bases import (
|
||||
ToolContainerBase, MouseButton,
|
||||
KeyEvent, LocationEvent, MouseEvent, ResizeEvent, CloseEvent)
|
||||
|
||||
try:
|
||||
from gi import require_version as gi_require_version
|
||||
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
|
||||
|
||||
import gi
|
||||
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,
|
||||
)
|
||||
|
||||
_GOBJECT_GE_3_47 = gi.version_info >= (3, 47, 0)
|
||||
_GTK_GE_4_12 = Gtk.check_version(4, 12, 0) is None
|
||||
|
||||
|
||||
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)
|
||||
if _GTK_GE_4_12:
|
||||
self.connect('realize', self._realize_event)
|
||||
else:
|
||||
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),
|
||||
guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
|
||||
)._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),
|
||||
guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
|
||||
)._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),
|
||||
guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
|
||||
)._process()
|
||||
|
||||
def key_press_event(self, controller, keyval, keycode, state):
|
||||
KeyEvent(
|
||||
"key_press_event", self, self._get_key(keyval, keycode, state),
|
||||
*self._mpl_coords(),
|
||||
guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
|
||||
)._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(),
|
||||
guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
|
||||
)._process()
|
||||
return True
|
||||
|
||||
def motion_notify_event(self, controller, x, y):
|
||||
MouseEvent(
|
||||
"motion_notify_event", self, *self._mpl_coords((x, y)),
|
||||
buttons=self._mpl_buttons(controller),
|
||||
modifiers=self._mpl_modifiers(controller),
|
||||
guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
|
||||
)._process()
|
||||
|
||||
def enter_notify_event(self, controller, x, y):
|
||||
LocationEvent(
|
||||
"figure_enter_event", self, *self._mpl_coords((x, y)),
|
||||
modifiers=self._mpl_modifiers(),
|
||||
guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
|
||||
)._process()
|
||||
|
||||
def leave_notify_event(self, controller):
|
||||
LocationEvent(
|
||||
"figure_leave_event", self, *self._mpl_coords(),
|
||||
modifiers=self._mpl_modifiers(),
|
||||
guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
|
||||
)._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_buttons(self, controller):
|
||||
# NOTE: This spews "Broken accounting of active state" warnings on
|
||||
# right click on macOS.
|
||||
surface = self.get_native().get_surface()
|
||||
is_over, x, y, event_state = surface.get_device_position(
|
||||
self.get_display().get_default_seat().get_pointer())
|
||||
# NOTE: alternatively we could use
|
||||
# event_state = controller.get_current_event_state()
|
||||
# but for button_press/button_release this would report the state
|
||||
# *prior* to the event rather than after it; the above reports the
|
||||
# state *after* it.
|
||||
mod_table = [
|
||||
(MouseButton.LEFT, Gdk.ModifierType.BUTTON1_MASK),
|
||||
(MouseButton.MIDDLE, Gdk.ModifierType.BUTTON2_MASK),
|
||||
(MouseButton.RIGHT, Gdk.ModifierType.BUTTON3_MASK),
|
||||
(MouseButton.BACK, Gdk.ModifierType.BUTTON4_MASK),
|
||||
(MouseButton.FORWARD, Gdk.ModifierType.BUTTON5_MASK),
|
||||
]
|
||||
return {name for name, mask in mod_table if event_state & mask}
|
||||
|
||||
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 _realize_event(self, obj):
|
||||
surface = self.get_native().get_surface()
|
||||
surface.connect('notify::scale', self._update_device_pixel_ratio)
|
||||
self._update_device_pixel_ratio()
|
||||
|
||||
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 _GTK_GE_4_12:
|
||||
scale = self.get_native().get_surface().get_scale()
|
||||
else:
|
||||
scale = self.get_scale_factor()
|
||||
assert scale is not None
|
||||
if self._set_device_pixel_ratio(scale):
|
||||
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()
|
||||
return self.UNKNOWN_SAVED_STATUS
|
||||
|
||||
|
||||
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,196 @@
|
||||
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)
|
||||
return 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 : float
|
||||
The width of the canvas in logical units
|
||||
height : float
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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
|
||||
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,326 @@
|
||||
"""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.web
|
||||
import tornado.ioloop
|
||||
import tornado.websocket
|
||||
except ImportError as err:
|
||||
raise RuntimeError("The WebAgg backend requires Tornado.") from err
|
||||
|
||||
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,527 @@
|
||||
"""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, MouseButton, 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
|
||||
e_type = event['type']
|
||||
button = event['button'] + 1 # JS numbers off by 1 compared to mpl.
|
||||
buttons = { # JS ordering different compared to mpl.
|
||||
button for button, mask in [
|
||||
(MouseButton.LEFT, 1),
|
||||
(MouseButton.RIGHT, 2),
|
||||
(MouseButton.MIDDLE, 4),
|
||||
(MouseButton.BACK, 8),
|
||||
(MouseButton.FORWARD, 16),
|
||||
] if event['buttons'] & mask # State *after* press/release.
|
||||
}
|
||||
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,
|
||||
buttons=buttons, 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')
|
||||
return self.UNKNOWN_SAVED_STATUS
|
||||
|
||||
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
|
||||
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