This commit is contained in:
2024-11-29 18:15:30 +00:00
parent 40aade2d8e
commit bc9415586e
5298 changed files with 1938676 additions and 80 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,794 @@
"""
Adjust subplot layouts so that there are no overlapping Axes or Axes
decorations. All Axes decorations are dealt with (labels, ticks, titles,
ticklabels) and some dependent artists are also dealt with (colorbar,
suptitle).
Layout is done via `~matplotlib.gridspec`, with one constraint per gridspec,
so it is possible to have overlapping Axes if the gridspecs overlap (i.e.
using `~matplotlib.gridspec.GridSpecFromSubplotSpec`). Axes placed using
``figure.subplots()`` or ``figure.add_subplots()`` will participate in the
layout. Axes manually placed via ``figure.add_axes()`` will not.
See Tutorial: :ref:`constrainedlayout_guide`
General idea:
-------------
First, a figure has a gridspec that divides the figure into nrows and ncols,
with heights and widths set by ``height_ratios`` and ``width_ratios``,
often just set to 1 for an equal grid.
Subplotspecs that are derived from this gridspec can contain either a
``SubPanel``, a ``GridSpecFromSubplotSpec``, or an ``Axes``. The ``SubPanel``
and ``GridSpecFromSubplotSpec`` are dealt with recursively and each contain an
analogous layout.
Each ``GridSpec`` has a ``_layoutgrid`` attached to it. The ``_layoutgrid``
has the same logical layout as the ``GridSpec``. Each row of the grid spec
has a top and bottom "margin" and each column has a left and right "margin".
The "inner" height of each row is constrained to be the same (or as modified
by ``height_ratio``), and the "inner" width of each column is
constrained to be the same (as modified by ``width_ratio``), where "inner"
is the width or height of each column/row minus the size of the margins.
Then the size of the margins for each row and column are determined as the
max width of the decorators on each Axes that has decorators in that margin.
For instance, a normal Axes would have a left margin that includes the
left ticklabels, and the ylabel if it exists. The right margin may include a
colorbar, the bottom margin the xaxis decorations, and the top margin the
title.
With these constraints, the solver then finds appropriate bounds for the
columns and rows. It's possible that the margins take up the whole figure,
in which case the algorithm is not applied and a warning is raised.
See the tutorial :ref:`constrainedlayout_guide`
for more discussion of the algorithm with examples.
"""
import logging
import numpy as np
from matplotlib import _api, artist as martist
import matplotlib.transforms as mtransforms
import matplotlib._layoutgrid as mlayoutgrid
_log = logging.getLogger(__name__)
######################################################
def do_constrained_layout(fig, h_pad, w_pad,
hspace=None, wspace=None, rect=(0, 0, 1, 1),
compress=False):
"""
Do the constrained_layout. Called at draw time in
``figure.constrained_layout()``
Parameters
----------
fig : `~matplotlib.figure.Figure`
`.Figure` instance to do the layout in.
h_pad, w_pad : float
Padding around the Axes elements in figure-normalized units.
hspace, wspace : float
Fraction of the figure to dedicate to space between the
Axes. These are evenly spread between the gaps between the Axes.
A value of 0.2 for a three-column layout would have a space
of 0.1 of the figure width between each column.
If h/wspace < h/w_pad, then the pads are used instead.
rect : tuple of 4 floats
Rectangle in figure coordinates to perform constrained layout in
[left, bottom, width, height], each from 0-1.
compress : bool
Whether to shift Axes so that white space in between them is
removed. This is useful for simple grids of fixed-aspect Axes (e.g.
a grid of images).
Returns
-------
layoutgrid : private debugging structure
"""
renderer = fig._get_renderer()
# make layoutgrid tree...
layoutgrids = make_layoutgrids(fig, None, rect=rect)
if not layoutgrids['hasgrids']:
_api.warn_external('There are no gridspecs with layoutgrids. '
'Possibly did not call parent GridSpec with the'
' "figure" keyword')
return
for _ in range(2):
# do the algorithm twice. This has to be done because decorations
# change size after the first re-position (i.e. x/yticklabels get
# larger/smaller). This second reposition tends to be much milder,
# so doing twice makes things work OK.
# make margins for all the Axes and subfigures in the
# figure. Add margins for colorbars...
make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
w_pad=w_pad, hspace=hspace, wspace=wspace)
make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
w_pad=w_pad)
# if a layout is such that a columns (or rows) margin has no
# constraints, we need to make all such instances in the grid
# match in margin size.
match_submerged_margins(layoutgrids, fig)
# update all the variables in the layout.
layoutgrids[fig].update_variables()
warn_collapsed = ('constrained_layout not applied because '
'axes sizes collapsed to zero. Try making '
'figure larger or Axes decorations smaller.')
if check_no_collapsed_axes(layoutgrids, fig):
reposition_axes(layoutgrids, fig, renderer, h_pad=h_pad,
w_pad=w_pad, hspace=hspace, wspace=wspace)
if compress:
layoutgrids = compress_fixed_aspect(layoutgrids, fig)
layoutgrids[fig].update_variables()
if check_no_collapsed_axes(layoutgrids, fig):
reposition_axes(layoutgrids, fig, renderer, h_pad=h_pad,
w_pad=w_pad, hspace=hspace, wspace=wspace)
else:
_api.warn_external(warn_collapsed)
else:
_api.warn_external(warn_collapsed)
reset_margins(layoutgrids, fig)
return layoutgrids
def make_layoutgrids(fig, layoutgrids, rect=(0, 0, 1, 1)):
"""
Make the layoutgrid tree.
(Sub)Figures get a layoutgrid so we can have figure margins.
Gridspecs that are attached to Axes get a layoutgrid so Axes
can have margins.
"""
if layoutgrids is None:
layoutgrids = dict()
layoutgrids['hasgrids'] = False
if not hasattr(fig, '_parent'):
# top figure; pass rect as parent to allow user-specified
# margins
layoutgrids[fig] = mlayoutgrid.LayoutGrid(parent=rect, name='figlb')
else:
# subfigure
gs = fig._subplotspec.get_gridspec()
# it is possible the gridspec containing this subfigure hasn't
# been added to the tree yet:
layoutgrids = make_layoutgrids_gs(layoutgrids, gs)
# add the layoutgrid for the subfigure:
parentlb = layoutgrids[gs]
layoutgrids[fig] = mlayoutgrid.LayoutGrid(
parent=parentlb,
name='panellb',
parent_inner=True,
nrows=1, ncols=1,
parent_pos=(fig._subplotspec.rowspan,
fig._subplotspec.colspan))
# recursively do all subfigures in this figure...
for sfig in fig.subfigs:
layoutgrids = make_layoutgrids(sfig, layoutgrids)
# for each Axes at the local level add its gridspec:
for ax in fig._localaxes:
gs = ax.get_gridspec()
if gs is not None:
layoutgrids = make_layoutgrids_gs(layoutgrids, gs)
return layoutgrids
def make_layoutgrids_gs(layoutgrids, gs):
"""
Make the layoutgrid for a gridspec (and anything nested in the gridspec)
"""
if gs in layoutgrids or gs.figure is None:
return layoutgrids
# in order to do constrained_layout there has to be at least *one*
# gridspec in the tree:
layoutgrids['hasgrids'] = True
if not hasattr(gs, '_subplot_spec'):
# normal gridspec
parent = layoutgrids[gs.figure]
layoutgrids[gs] = mlayoutgrid.LayoutGrid(
parent=parent,
parent_inner=True,
name='gridspec',
ncols=gs._ncols, nrows=gs._nrows,
width_ratios=gs.get_width_ratios(),
height_ratios=gs.get_height_ratios())
else:
# this is a gridspecfromsubplotspec:
subplot_spec = gs._subplot_spec
parentgs = subplot_spec.get_gridspec()
# if a nested gridspec it is possible the parent is not in there yet:
if parentgs not in layoutgrids:
layoutgrids = make_layoutgrids_gs(layoutgrids, parentgs)
subspeclb = layoutgrids[parentgs]
# gridspecfromsubplotspec need an outer container:
# get a unique representation:
rep = (gs, 'top')
if rep not in layoutgrids:
layoutgrids[rep] = mlayoutgrid.LayoutGrid(
parent=subspeclb,
name='top',
nrows=1, ncols=1,
parent_pos=(subplot_spec.rowspan, subplot_spec.colspan))
layoutgrids[gs] = mlayoutgrid.LayoutGrid(
parent=layoutgrids[rep],
name='gridspec',
nrows=gs._nrows, ncols=gs._ncols,
width_ratios=gs.get_width_ratios(),
height_ratios=gs.get_height_ratios())
return layoutgrids
def check_no_collapsed_axes(layoutgrids, fig):
"""
Check that no Axes have collapsed to zero size.
"""
for sfig in fig.subfigs:
ok = check_no_collapsed_axes(layoutgrids, sfig)
if not ok:
return False
for ax in fig.axes:
gs = ax.get_gridspec()
if gs in layoutgrids: # also implies gs is not None.
lg = layoutgrids[gs]
for i in range(gs.nrows):
for j in range(gs.ncols):
bb = lg.get_inner_bbox(i, j)
if bb.width <= 0 or bb.height <= 0:
return False
return True
def compress_fixed_aspect(layoutgrids, fig):
gs = None
for ax in fig.axes:
if ax.get_subplotspec() is None:
continue
ax.apply_aspect()
sub = ax.get_subplotspec()
_gs = sub.get_gridspec()
if gs is None:
gs = _gs
extraw = np.zeros(gs.ncols)
extrah = np.zeros(gs.nrows)
elif _gs != gs:
raise ValueError('Cannot do compressed layout if Axes are not'
'all from the same gridspec')
orig = ax.get_position(original=True)
actual = ax.get_position(original=False)
dw = orig.width - actual.width
if dw > 0:
extraw[sub.colspan] = np.maximum(extraw[sub.colspan], dw)
dh = orig.height - actual.height
if dh > 0:
extrah[sub.rowspan] = np.maximum(extrah[sub.rowspan], dh)
if gs is None:
raise ValueError('Cannot do compressed layout if no Axes '
'are part of a gridspec.')
w = np.sum(extraw) / 2
layoutgrids[fig].edit_margin_min('left', w)
layoutgrids[fig].edit_margin_min('right', w)
h = np.sum(extrah) / 2
layoutgrids[fig].edit_margin_min('top', h)
layoutgrids[fig].edit_margin_min('bottom', h)
return layoutgrids
def get_margin_from_padding(obj, *, w_pad=0, h_pad=0,
hspace=0, wspace=0):
ss = obj._subplotspec
gs = ss.get_gridspec()
if hasattr(gs, 'hspace'):
_hspace = (gs.hspace if gs.hspace is not None else hspace)
_wspace = (gs.wspace if gs.wspace is not None else wspace)
else:
_hspace = (gs._hspace if gs._hspace is not None else hspace)
_wspace = (gs._wspace if gs._wspace is not None else wspace)
_wspace = _wspace / 2
_hspace = _hspace / 2
nrows, ncols = gs.get_geometry()
# there are two margins for each direction. The "cb"
# margins are for pads and colorbars, the non-"cb" are
# for the Axes decorations (labels etc).
margin = {'leftcb': w_pad, 'rightcb': w_pad,
'bottomcb': h_pad, 'topcb': h_pad,
'left': 0, 'right': 0,
'top': 0, 'bottom': 0}
if _wspace / ncols > w_pad:
if ss.colspan.start > 0:
margin['leftcb'] = _wspace / ncols
if ss.colspan.stop < ncols:
margin['rightcb'] = _wspace / ncols
if _hspace / nrows > h_pad:
if ss.rowspan.stop < nrows:
margin['bottomcb'] = _hspace / nrows
if ss.rowspan.start > 0:
margin['topcb'] = _hspace / nrows
return margin
def make_layout_margins(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0,
hspace=0, wspace=0):
"""
For each Axes, make a margin between the *pos* layoutbox and the
*axes* layoutbox be a minimum size that can accommodate the
decorations on the axis.
Then make room for colorbars.
Parameters
----------
layoutgrids : dict
fig : `~matplotlib.figure.Figure`
`.Figure` instance to do the layout in.
renderer : `~matplotlib.backend_bases.RendererBase` subclass.
The renderer to use.
w_pad, h_pad : float, default: 0
Width and height padding (in fraction of figure).
hspace, wspace : float, default: 0
Width and height padding as fraction of figure size divided by
number of columns or rows.
"""
for sfig in fig.subfigs: # recursively make child panel margins
ss = sfig._subplotspec
gs = ss.get_gridspec()
make_layout_margins(layoutgrids, sfig, renderer,
w_pad=w_pad, h_pad=h_pad,
hspace=hspace, wspace=wspace)
margins = get_margin_from_padding(sfig, w_pad=0, h_pad=0,
hspace=hspace, wspace=wspace)
layoutgrids[gs].edit_outer_margin_mins(margins, ss)
for ax in fig._localaxes:
if not ax.get_subplotspec() or not ax.get_in_layout():
continue
ss = ax.get_subplotspec()
gs = ss.get_gridspec()
if gs not in layoutgrids:
return
margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
hspace=hspace, wspace=wspace)
pos, bbox = get_pos_and_bbox(ax, renderer)
# the margin is the distance between the bounding box of the Axes
# and its position (plus the padding from above)
margin['left'] += pos.x0 - bbox.x0
margin['right'] += bbox.x1 - pos.x1
# remember that rows are ordered from top:
margin['bottom'] += pos.y0 - bbox.y0
margin['top'] += bbox.y1 - pos.y1
# make margin for colorbars. These margins go in the
# padding margin, versus the margin for Axes decorators.
for cbax in ax._colorbars:
# note pad is a fraction of the parent width...
pad = colorbar_get_pad(layoutgrids, cbax)
# colorbars can be child of more than one subplot spec:
cbp_rspan, cbp_cspan = get_cb_parent_spans(cbax)
loc = cbax._colorbar_info['location']
cbpos, cbbbox = get_pos_and_bbox(cbax, renderer)
if loc == 'right':
if cbp_cspan.stop == ss.colspan.stop:
# only increase if the colorbar is on the right edge
margin['rightcb'] += cbbbox.width + pad
elif loc == 'left':
if cbp_cspan.start == ss.colspan.start:
# only increase if the colorbar is on the left edge
margin['leftcb'] += cbbbox.width + pad
elif loc == 'top':
if cbp_rspan.start == ss.rowspan.start:
margin['topcb'] += cbbbox.height + pad
else:
if cbp_rspan.stop == ss.rowspan.stop:
margin['bottomcb'] += cbbbox.height + pad
# If the colorbars are wider than the parent box in the
# cross direction
if loc in ['top', 'bottom']:
if (cbp_cspan.start == ss.colspan.start and
cbbbox.x0 < bbox.x0):
margin['left'] += bbox.x0 - cbbbox.x0
if (cbp_cspan.stop == ss.colspan.stop and
cbbbox.x1 > bbox.x1):
margin['right'] += cbbbox.x1 - bbox.x1
# or taller:
if loc in ['left', 'right']:
if (cbp_rspan.stop == ss.rowspan.stop and
cbbbox.y0 < bbox.y0):
margin['bottom'] += bbox.y0 - cbbbox.y0
if (cbp_rspan.start == ss.rowspan.start and
cbbbox.y1 > bbox.y1):
margin['top'] += cbbbox.y1 - bbox.y1
# pass the new margins down to the layout grid for the solution...
layoutgrids[gs].edit_outer_margin_mins(margin, ss)
# make margins for figure-level legends:
for leg in fig.legends:
inv_trans_fig = None
if leg._outside_loc and leg._bbox_to_anchor is None:
if inv_trans_fig is None:
inv_trans_fig = fig.transFigure.inverted().transform_bbox
bbox = inv_trans_fig(leg.get_tightbbox(renderer))
w = bbox.width + 2 * w_pad
h = bbox.height + 2 * h_pad
legendloc = leg._outside_loc
if legendloc == 'lower':
layoutgrids[fig].edit_margin_min('bottom', h)
elif legendloc == 'upper':
layoutgrids[fig].edit_margin_min('top', h)
if legendloc == 'right':
layoutgrids[fig].edit_margin_min('right', w)
elif legendloc == 'left':
layoutgrids[fig].edit_margin_min('left', w)
def make_margin_suptitles(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0):
# Figure out how large the suptitle is and make the
# top level figure margin larger.
inv_trans_fig = fig.transFigure.inverted().transform_bbox
# get the h_pad and w_pad as distances in the local subfigure coordinates:
padbox = mtransforms.Bbox([[0, 0], [w_pad, h_pad]])
padbox = (fig.transFigure -
fig.transSubfigure).transform_bbox(padbox)
h_pad_local = padbox.height
w_pad_local = padbox.width
for sfig in fig.subfigs:
make_margin_suptitles(layoutgrids, sfig, renderer,
w_pad=w_pad, h_pad=h_pad)
if fig._suptitle is not None and fig._suptitle.get_in_layout():
p = fig._suptitle.get_position()
if getattr(fig._suptitle, '_autopos', False):
fig._suptitle.set_position((p[0], 1 - h_pad_local))
bbox = inv_trans_fig(fig._suptitle.get_tightbbox(renderer))
layoutgrids[fig].edit_margin_min('top', bbox.height + 2 * h_pad)
if fig._supxlabel is not None and fig._supxlabel.get_in_layout():
p = fig._supxlabel.get_position()
if getattr(fig._supxlabel, '_autopos', False):
fig._supxlabel.set_position((p[0], h_pad_local))
bbox = inv_trans_fig(fig._supxlabel.get_tightbbox(renderer))
layoutgrids[fig].edit_margin_min('bottom',
bbox.height + 2 * h_pad)
if fig._supylabel is not None and fig._supylabel.get_in_layout():
p = fig._supylabel.get_position()
if getattr(fig._supylabel, '_autopos', False):
fig._supylabel.set_position((w_pad_local, p[1]))
bbox = inv_trans_fig(fig._supylabel.get_tightbbox(renderer))
layoutgrids[fig].edit_margin_min('left', bbox.width + 2 * w_pad)
def match_submerged_margins(layoutgrids, fig):
"""
Make the margins that are submerged inside an Axes the same size.
This allows Axes that span two columns (or rows) that are offset
from one another to have the same size.
This gives the proper layout for something like::
fig = plt.figure(constrained_layout=True)
axs = fig.subplot_mosaic("AAAB\nCCDD")
Without this routine, the Axes D will be wider than C, because the
margin width between the two columns in C has no width by default,
whereas the margins between the two columns of D are set by the
width of the margin between A and B. However, obviously the user would
like C and D to be the same size, so we need to add constraints to these
"submerged" margins.
This routine makes all the interior margins the same, and the spacing
between the three columns in A and the two column in C are all set to the
margins between the two columns of D.
See test_constrained_layout::test_constrained_layout12 for an example.
"""
for sfig in fig.subfigs:
match_submerged_margins(layoutgrids, sfig)
axs = [a for a in fig.get_axes()
if a.get_subplotspec() is not None and a.get_in_layout()]
for ax1 in axs:
ss1 = ax1.get_subplotspec()
if ss1.get_gridspec() not in layoutgrids:
axs.remove(ax1)
continue
lg1 = layoutgrids[ss1.get_gridspec()]
# interior columns:
if len(ss1.colspan) > 1:
maxsubl = np.max(
lg1.margin_vals['left'][ss1.colspan[1:]] +
lg1.margin_vals['leftcb'][ss1.colspan[1:]]
)
maxsubr = np.max(
lg1.margin_vals['right'][ss1.colspan[:-1]] +
lg1.margin_vals['rightcb'][ss1.colspan[:-1]]
)
for ax2 in axs:
ss2 = ax2.get_subplotspec()
lg2 = layoutgrids[ss2.get_gridspec()]
if lg2 is not None and len(ss2.colspan) > 1:
maxsubl2 = np.max(
lg2.margin_vals['left'][ss2.colspan[1:]] +
lg2.margin_vals['leftcb'][ss2.colspan[1:]])
if maxsubl2 > maxsubl:
maxsubl = maxsubl2
maxsubr2 = np.max(
lg2.margin_vals['right'][ss2.colspan[:-1]] +
lg2.margin_vals['rightcb'][ss2.colspan[:-1]])
if maxsubr2 > maxsubr:
maxsubr = maxsubr2
for i in ss1.colspan[1:]:
lg1.edit_margin_min('left', maxsubl, cell=i)
for i in ss1.colspan[:-1]:
lg1.edit_margin_min('right', maxsubr, cell=i)
# interior rows:
if len(ss1.rowspan) > 1:
maxsubt = np.max(
lg1.margin_vals['top'][ss1.rowspan[1:]] +
lg1.margin_vals['topcb'][ss1.rowspan[1:]]
)
maxsubb = np.max(
lg1.margin_vals['bottom'][ss1.rowspan[:-1]] +
lg1.margin_vals['bottomcb'][ss1.rowspan[:-1]]
)
for ax2 in axs:
ss2 = ax2.get_subplotspec()
lg2 = layoutgrids[ss2.get_gridspec()]
if lg2 is not None:
if len(ss2.rowspan) > 1:
maxsubt = np.max([np.max(
lg2.margin_vals['top'][ss2.rowspan[1:]] +
lg2.margin_vals['topcb'][ss2.rowspan[1:]]
), maxsubt])
maxsubb = np.max([np.max(
lg2.margin_vals['bottom'][ss2.rowspan[:-1]] +
lg2.margin_vals['bottomcb'][ss2.rowspan[:-1]]
), maxsubb])
for i in ss1.rowspan[1:]:
lg1.edit_margin_min('top', maxsubt, cell=i)
for i in ss1.rowspan[:-1]:
lg1.edit_margin_min('bottom', maxsubb, cell=i)
def get_cb_parent_spans(cbax):
"""
Figure out which subplotspecs this colorbar belongs to.
Parameters
----------
cbax : `~matplotlib.axes.Axes`
Axes for the colorbar.
"""
rowstart = np.inf
rowstop = -np.inf
colstart = np.inf
colstop = -np.inf
for parent in cbax._colorbar_info['parents']:
ss = parent.get_subplotspec()
rowstart = min(ss.rowspan.start, rowstart)
rowstop = max(ss.rowspan.stop, rowstop)
colstart = min(ss.colspan.start, colstart)
colstop = max(ss.colspan.stop, colstop)
rowspan = range(rowstart, rowstop)
colspan = range(colstart, colstop)
return rowspan, colspan
def get_pos_and_bbox(ax, renderer):
"""
Get the position and the bbox for the Axes.
Parameters
----------
ax : `~matplotlib.axes.Axes`
renderer : `~matplotlib.backend_bases.RendererBase` subclass.
Returns
-------
pos : `~matplotlib.transforms.Bbox`
Position in figure coordinates.
bbox : `~matplotlib.transforms.Bbox`
Tight bounding box in figure coordinates.
"""
fig = ax.figure
pos = ax.get_position(original=True)
# pos is in panel co-ords, but we need in figure for the layout
pos = pos.transformed(fig.transSubfigure - fig.transFigure)
tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
if tightbbox is None:
bbox = pos
else:
bbox = tightbbox.transformed(fig.transFigure.inverted())
return pos, bbox
def reposition_axes(layoutgrids, fig, renderer, *,
w_pad=0, h_pad=0, hspace=0, wspace=0):
"""
Reposition all the Axes based on the new inner bounding box.
"""
trans_fig_to_subfig = fig.transFigure - fig.transSubfigure
for sfig in fig.subfigs:
bbox = layoutgrids[sfig].get_outer_bbox()
sfig._redo_transform_rel_fig(
bbox=bbox.transformed(trans_fig_to_subfig))
reposition_axes(layoutgrids, sfig, renderer,
w_pad=w_pad, h_pad=h_pad,
wspace=wspace, hspace=hspace)
for ax in fig._localaxes:
if ax.get_subplotspec() is None or not ax.get_in_layout():
continue
# grid bbox is in Figure coordinates, but we specify in panel
# coordinates...
ss = ax.get_subplotspec()
gs = ss.get_gridspec()
if gs not in layoutgrids:
return
bbox = layoutgrids[gs].get_inner_bbox(rows=ss.rowspan,
cols=ss.colspan)
# transform from figure to panel for set_position:
newbbox = trans_fig_to_subfig.transform_bbox(bbox)
ax._set_position(newbbox)
# move the colorbars:
# we need to keep track of oldw and oldh if there is more than
# one colorbar:
offset = {'left': 0, 'right': 0, 'bottom': 0, 'top': 0}
for nn, cbax in enumerate(ax._colorbars[::-1]):
if ax == cbax._colorbar_info['parents'][0]:
reposition_colorbar(layoutgrids, cbax, renderer,
offset=offset)
def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None):
"""
Place the colorbar in its new place.
Parameters
----------
layoutgrids : dict
cbax : `~matplotlib.axes.Axes`
Axes for the colorbar.
renderer : `~matplotlib.backend_bases.RendererBase` subclass.
The renderer to use.
offset : array-like
Offset the colorbar needs to be pushed to in order to
account for multiple colorbars.
"""
parents = cbax._colorbar_info['parents']
gs = parents[0].get_gridspec()
fig = cbax.figure
trans_fig_to_subfig = fig.transFigure - fig.transSubfigure
cb_rspans, cb_cspans = get_cb_parent_spans(cbax)
bboxparent = layoutgrids[gs].get_bbox_for_cb(rows=cb_rspans,
cols=cb_cspans)
pb = layoutgrids[gs].get_inner_bbox(rows=cb_rspans, cols=cb_cspans)
location = cbax._colorbar_info['location']
anchor = cbax._colorbar_info['anchor']
fraction = cbax._colorbar_info['fraction']
aspect = cbax._colorbar_info['aspect']
shrink = cbax._colorbar_info['shrink']
cbpos, cbbbox = get_pos_and_bbox(cbax, renderer)
# Colorbar gets put at extreme edge of outer bbox of the subplotspec
# It needs to be moved in by: 1) a pad 2) its "margin" 3) by
# any colorbars already added at this location:
cbpad = colorbar_get_pad(layoutgrids, cbax)
if location in ('left', 'right'):
# fraction and shrink are fractions of parent
pbcb = pb.shrunk(fraction, shrink).anchored(anchor, pb)
# The colorbar is at the left side of the parent. Need
# to translate to right (or left)
if location == 'right':
lmargin = cbpos.x0 - cbbbox.x0
dx = bboxparent.x1 - pbcb.x0 + offset['right']
dx += cbpad + lmargin
offset['right'] += cbbbox.width + cbpad
pbcb = pbcb.translated(dx, 0)
else:
lmargin = cbpos.x0 - cbbbox.x0
dx = bboxparent.x0 - pbcb.x0 # edge of parent
dx += -cbbbox.width - cbpad + lmargin - offset['left']
offset['left'] += cbbbox.width + cbpad
pbcb = pbcb.translated(dx, 0)
else: # horizontal axes:
pbcb = pb.shrunk(shrink, fraction).anchored(anchor, pb)
if location == 'top':
bmargin = cbpos.y0 - cbbbox.y0
dy = bboxparent.y1 - pbcb.y0 + offset['top']
dy += cbpad + bmargin
offset['top'] += cbbbox.height + cbpad
pbcb = pbcb.translated(0, dy)
else:
bmargin = cbpos.y0 - cbbbox.y0
dy = bboxparent.y0 - pbcb.y0
dy += -cbbbox.height - cbpad + bmargin - offset['bottom']
offset['bottom'] += cbbbox.height + cbpad
pbcb = pbcb.translated(0, dy)
pbcb = trans_fig_to_subfig.transform_bbox(pbcb)
cbax.set_transform(fig.transSubfigure)
cbax._set_position(pbcb)
cbax.set_anchor(anchor)
if location in ['bottom', 'top']:
aspect = 1 / aspect
cbax.set_box_aspect(aspect)
cbax.set_aspect('auto')
return offset
def reset_margins(layoutgrids, fig):
"""
Reset the margins in the layoutboxes of *fig*.
Margins are usually set as a minimum, so if the figure gets smaller
the minimum needs to be zero in order for it to grow again.
"""
for sfig in fig.subfigs:
reset_margins(layoutgrids, sfig)
for ax in fig.axes:
if ax.get_in_layout():
gs = ax.get_gridspec()
if gs in layoutgrids: # also implies gs is not None.
layoutgrids[gs].reset_margins()
layoutgrids[fig].reset_margins()
def colorbar_get_pad(layoutgrids, cax):
parents = cax._colorbar_info['parents']
gs = parents[0].get_gridspec()
cb_rspans, cb_cspans = get_cb_parent_spans(cax)
bboxouter = layoutgrids[gs].get_inner_bbox(rows=cb_rspans, cols=cb_cspans)
if cax._colorbar_info['location'] in ['right', 'left']:
size = bboxouter.width
else:
size = bboxouter.height
return cax._colorbar_info['pad'] * size

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
from collections.abc import Sequence
import numpy as np
from .transforms import BboxBase
def affine_transform(points: np.ndarray, trans: np.ndarray) -> np.ndarray: ...
def count_bboxes_overlapping_bbox(bbox: BboxBase, bboxes: Sequence[BboxBase]) -> int: ...
def update_path_extents(path, trans, rect, minpos, ignore): ...

View File

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

View File

@ -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: ...

View File

@ -0,0 +1,82 @@
"""
Low-level text helper utilities.
"""
from __future__ import annotations
import dataclasses
from . import _api
from .ft2font import KERNING_DEFAULT, LOAD_NO_HINTING, FT2Font
@dataclasses.dataclass(frozen=True)
class LayoutItem:
ft_object: FT2Font
char: str
glyph_idx: int
x: float
prev_kern: float
def warn_on_missing_glyph(codepoint, fontnames):
_api.warn_external(
f"Glyph {codepoint} "
f"({chr(codepoint).encode('ascii', 'namereplace').decode('ascii')}) "
f"missing from font(s) {fontnames}.")
block = ("Hebrew" if 0x0590 <= codepoint <= 0x05ff else
"Arabic" if 0x0600 <= codepoint <= 0x06ff else
"Devanagari" if 0x0900 <= codepoint <= 0x097f else
"Bengali" if 0x0980 <= codepoint <= 0x09ff else
"Gurmukhi" if 0x0a00 <= codepoint <= 0x0a7f else
"Gujarati" if 0x0a80 <= codepoint <= 0x0aff else
"Oriya" if 0x0b00 <= codepoint <= 0x0b7f else
"Tamil" if 0x0b80 <= codepoint <= 0x0bff else
"Telugu" if 0x0c00 <= codepoint <= 0x0c7f else
"Kannada" if 0x0c80 <= codepoint <= 0x0cff else
"Malayalam" if 0x0d00 <= codepoint <= 0x0d7f else
"Sinhala" if 0x0d80 <= codepoint <= 0x0dff else
None)
if block:
_api.warn_external(
f"Matplotlib currently does not support {block} natively.")
def layout(string, font, *, kern_mode=KERNING_DEFAULT):
"""
Render *string* with *font*.
For each character in *string*, yield a LayoutItem instance. When such an instance
is yielded, the font's glyph is set to the corresponding character.
Parameters
----------
string : str
The string to be rendered.
font : FT2Font
The font.
kern_mode : int
A FreeType kerning mode.
Yields
------
LayoutItem
"""
x = 0
prev_glyph_idx = None
char_to_font = font._get_fontmap(string)
base_font = font
for char in string:
# This has done the fallback logic
font = char_to_font.get(char, base_font)
glyph_idx = font.get_char_index(ord(char))
kern = (
base_font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64
if prev_glyph_idx is not None else 0.
)
x += kern
glyph = font.load_glyph(glyph_idx, flags=LOAD_NO_HINTING)
yield LayoutItem(font, char, glyph_idx, x, kern)
x += glyph.linearHoriAdvance / 65536
prev_glyph_idx = glyph_idx

View File

@ -0,0 +1,84 @@
"""
Helper module for the *bbox_inches* parameter in `.Figure.savefig`.
"""
from matplotlib.transforms import Bbox, TransformedBbox, Affine2D
def adjust_bbox(fig, bbox_inches, fixed_dpi=None):
"""
Temporarily adjust the figure so that only the specified area
(bbox_inches) is saved.
It modifies fig.bbox, fig.bbox_inches,
fig.transFigure._boxout, and fig.patch. While the figure size
changes, the scale of the original figure is conserved. A
function which restores the original values are returned.
"""
origBbox = fig.bbox
origBboxInches = fig.bbox_inches
_boxout = fig.transFigure._boxout
old_aspect = []
locator_list = []
sentinel = object()
for ax in fig.axes:
locator = ax.get_axes_locator()
if locator is not None:
ax.apply_aspect(locator(ax, None))
locator_list.append(locator)
current_pos = ax.get_position(original=False).frozen()
ax.set_axes_locator(lambda a, r, _pos=current_pos: _pos)
# override the method that enforces the aspect ratio on the Axes
if 'apply_aspect' in ax.__dict__:
old_aspect.append(ax.apply_aspect)
else:
old_aspect.append(sentinel)
ax.apply_aspect = lambda pos=None: None
def restore_bbox():
for ax, loc, aspect in zip(fig.axes, locator_list, old_aspect):
ax.set_axes_locator(loc)
if aspect is sentinel:
# delete our no-op function which un-hides the original method
del ax.apply_aspect
else:
ax.apply_aspect = aspect
fig.bbox = origBbox
fig.bbox_inches = origBboxInches
fig.transFigure._boxout = _boxout
fig.transFigure.invalidate()
fig.patch.set_bounds(0, 0, 1, 1)
if fixed_dpi is None:
fixed_dpi = fig.dpi
tr = Affine2D().scale(fixed_dpi)
dpi_scale = fixed_dpi / fig.dpi
fig.bbox_inches = Bbox.from_bounds(0, 0, *bbox_inches.size)
x0, y0 = tr.transform(bbox_inches.p0)
w1, h1 = fig.bbox.size * dpi_scale
fig.transFigure._boxout = Bbox.from_bounds(-x0, -y0, w1, h1)
fig.transFigure.invalidate()
fig.bbox = TransformedBbox(fig.bbox_inches, tr)
fig.patch.set_bounds(x0 / w1, y0 / h1,
fig.bbox.width / w1, fig.bbox.height / h1)
return restore_bbox
def process_figure_for_rasterizing(fig, bbox_inches_restore, fixed_dpi=None):
"""
A function that needs to be called when figure dpi changes during the
drawing (e.g., rasterizing). It recovers the bbox and re-adjust it with
the new dpi.
"""
bbox_inches, restore_bbox = bbox_inches_restore
restore_bbox()
r = adjust_bbox(fig, bbox_inches, fixed_dpi)
return bbox_inches, r

View File

@ -0,0 +1,301 @@
"""
Routines to adjust subplot params so that subplots are
nicely fit in the figure. In doing so, only axis labels, tick labels, Axes
titles and offsetboxes that are anchored to Axes are currently considered.
Internally, this module assumes that the margins (left margin, etc.) which are
differences between ``Axes.get_tightbbox`` and ``Axes.bbox`` are independent of
Axes position. This may fail if ``Axes.adjustable`` is ``datalim`` as well as
such cases as when left or right margin are affected by xlabel.
"""
import numpy as np
import matplotlib as mpl
from matplotlib import _api, artist as martist
from matplotlib.font_manager import FontProperties
from matplotlib.transforms import Bbox
def _auto_adjust_subplotpars(
fig, renderer, shape, span_pairs, subplot_list,
ax_bbox_list=None, pad=1.08, h_pad=None, w_pad=None, rect=None):
"""
Return a dict of subplot parameters to adjust spacing between subplots
or ``None`` if resulting Axes would have zero height or width.
Note that this function ignores geometry information of subplot itself, but
uses what is given by the *shape* and *subplot_list* parameters. Also, the
results could be incorrect if some subplots have ``adjustable=datalim``.
Parameters
----------
shape : tuple[int, int]
Number of rows and columns of the grid.
span_pairs : list[tuple[slice, slice]]
List of rowspans and colspans occupied by each subplot.
subplot_list : list of subplots
List of subplots that will be used to calculate optimal subplot_params.
pad : float
Padding between the figure edge and the edges of subplots, as a
fraction of the font size.
h_pad, w_pad : float
Padding (height/width) between edges of adjacent subplots, as a
fraction of the font size. Defaults to *pad*.
rect : tuple
(left, bottom, right, top), default: None.
"""
rows, cols = shape
font_size_inch = (FontProperties(
size=mpl.rcParams["font.size"]).get_size_in_points() / 72)
pad_inch = pad * font_size_inch
vpad_inch = h_pad * font_size_inch if h_pad is not None else pad_inch
hpad_inch = w_pad * font_size_inch if w_pad is not None else pad_inch
if len(span_pairs) != len(subplot_list) or len(subplot_list) == 0:
raise ValueError
if rect is None:
margin_left = margin_bottom = margin_right = margin_top = None
else:
margin_left, margin_bottom, _right, _top = rect
margin_right = 1 - _right if _right else None
margin_top = 1 - _top if _top else None
vspaces = np.zeros((rows + 1, cols))
hspaces = np.zeros((rows, cols + 1))
if ax_bbox_list is None:
ax_bbox_list = [
Bbox.union([ax.get_position(original=True) for ax in subplots])
for subplots in subplot_list]
for subplots, ax_bbox, (rowspan, colspan) in zip(
subplot_list, ax_bbox_list, span_pairs):
if all(not ax.get_visible() for ax in subplots):
continue
bb = []
for ax in subplots:
if ax.get_visible():
bb += [martist._get_tightbbox_for_layout_only(ax, renderer)]
tight_bbox_raw = Bbox.union(bb)
tight_bbox = fig.transFigure.inverted().transform_bbox(tight_bbox_raw)
hspaces[rowspan, colspan.start] += ax_bbox.xmin - tight_bbox.xmin # l
hspaces[rowspan, colspan.stop] += tight_bbox.xmax - ax_bbox.xmax # r
vspaces[rowspan.start, colspan] += tight_bbox.ymax - ax_bbox.ymax # t
vspaces[rowspan.stop, colspan] += ax_bbox.ymin - tight_bbox.ymin # b
fig_width_inch, fig_height_inch = fig.get_size_inches()
# margins can be negative for Axes with aspect applied, so use max(, 0) to
# make them nonnegative.
if not margin_left:
margin_left = max(hspaces[:, 0].max(), 0) + pad_inch/fig_width_inch
suplabel = fig._supylabel
if suplabel and suplabel.get_in_layout():
rel_width = fig.transFigure.inverted().transform_bbox(
suplabel.get_window_extent(renderer)).width
margin_left += rel_width + pad_inch/fig_width_inch
if not margin_right:
margin_right = max(hspaces[:, -1].max(), 0) + pad_inch/fig_width_inch
if not margin_top:
margin_top = max(vspaces[0, :].max(), 0) + pad_inch/fig_height_inch
if fig._suptitle and fig._suptitle.get_in_layout():
rel_height = fig.transFigure.inverted().transform_bbox(
fig._suptitle.get_window_extent(renderer)).height
margin_top += rel_height + pad_inch/fig_height_inch
if not margin_bottom:
margin_bottom = max(vspaces[-1, :].max(), 0) + pad_inch/fig_height_inch
suplabel = fig._supxlabel
if suplabel and suplabel.get_in_layout():
rel_height = fig.transFigure.inverted().transform_bbox(
suplabel.get_window_extent(renderer)).height
margin_bottom += rel_height + pad_inch/fig_height_inch
if margin_left + margin_right >= 1:
_api.warn_external('Tight layout not applied. The left and right '
'margins cannot be made large enough to '
'accommodate all Axes decorations.')
return None
if margin_bottom + margin_top >= 1:
_api.warn_external('Tight layout not applied. The bottom and top '
'margins cannot be made large enough to '
'accommodate all Axes decorations.')
return None
kwargs = dict(left=margin_left,
right=1 - margin_right,
bottom=margin_bottom,
top=1 - margin_top)
if cols > 1:
hspace = hspaces[:, 1:-1].max() + hpad_inch / fig_width_inch
# axes widths:
h_axes = (1 - margin_right - margin_left - hspace * (cols - 1)) / cols
if h_axes < 0:
_api.warn_external('Tight layout not applied. tight_layout '
'cannot make Axes width small enough to '
'accommodate all Axes decorations')
return None
else:
kwargs["wspace"] = hspace / h_axes
if rows > 1:
vspace = vspaces[1:-1, :].max() + vpad_inch / fig_height_inch
v_axes = (1 - margin_top - margin_bottom - vspace * (rows - 1)) / rows
if v_axes < 0:
_api.warn_external('Tight layout not applied. tight_layout '
'cannot make Axes height small enough to '
'accommodate all Axes decorations.')
return None
else:
kwargs["hspace"] = vspace / v_axes
return kwargs
def get_subplotspec_list(axes_list, grid_spec=None):
"""
Return a list of subplotspec from the given list of Axes.
For an instance of Axes that does not support subplotspec, None is inserted
in the list.
If grid_spec is given, None is inserted for those not from the given
grid_spec.
"""
subplotspec_list = []
for ax in axes_list:
axes_or_locator = ax.get_axes_locator()
if axes_or_locator is None:
axes_or_locator = ax
if hasattr(axes_or_locator, "get_subplotspec"):
subplotspec = axes_or_locator.get_subplotspec()
if subplotspec is not None:
subplotspec = subplotspec.get_topmost_subplotspec()
gs = subplotspec.get_gridspec()
if grid_spec is not None:
if gs != grid_spec:
subplotspec = None
elif gs.locally_modified_subplot_params():
subplotspec = None
else:
subplotspec = None
subplotspec_list.append(subplotspec)
return subplotspec_list
def get_tight_layout_figure(fig, axes_list, subplotspec_list, renderer,
pad=1.08, h_pad=None, w_pad=None, rect=None):
"""
Return subplot parameters for tight-layouted-figure with specified padding.
Parameters
----------
fig : Figure
axes_list : list of Axes
subplotspec_list : list of `.SubplotSpec`
The subplotspecs of each Axes.
renderer : renderer
pad : float
Padding between the figure edge and the edges of subplots, as a
fraction of the font size.
h_pad, w_pad : float
Padding (height/width) between edges of adjacent subplots. Defaults to
*pad*.
rect : tuple (left, bottom, right, top), default: None.
rectangle in normalized figure coordinates
that the whole subplots area (including labels) will fit into.
Defaults to using the entire figure.
Returns
-------
subplotspec or None
subplotspec kwargs to be passed to `.Figure.subplots_adjust` or
None if tight_layout could not be accomplished.
"""
# Multiple Axes can share same subplotspec (e.g., if using axes_grid1);
# we need to group them together.
ss_to_subplots = {ss: [] for ss in subplotspec_list}
for ax, ss in zip(axes_list, subplotspec_list):
ss_to_subplots[ss].append(ax)
if ss_to_subplots.pop(None, None):
_api.warn_external(
"This figure includes Axes that are not compatible with "
"tight_layout, so results might be incorrect.")
if not ss_to_subplots:
return {}
subplot_list = list(ss_to_subplots.values())
ax_bbox_list = [ss.get_position(fig) for ss in ss_to_subplots]
max_nrows = max(ss.get_gridspec().nrows for ss in ss_to_subplots)
max_ncols = max(ss.get_gridspec().ncols for ss in ss_to_subplots)
span_pairs = []
for ss in ss_to_subplots:
# The intent here is to support Axes from different gridspecs where
# one's nrows (or ncols) is a multiple of the other (e.g. 2 and 4),
# but this doesn't actually work because the computed wspace, in
# relative-axes-height, corresponds to different physical spacings for
# the 2-row grid and the 4-row grid. Still, this code is left, mostly
# for backcompat.
rows, cols = ss.get_gridspec().get_geometry()
div_row, mod_row = divmod(max_nrows, rows)
div_col, mod_col = divmod(max_ncols, cols)
if mod_row != 0:
_api.warn_external('tight_layout not applied: number of rows '
'in subplot specifications must be '
'multiples of one another.')
return {}
if mod_col != 0:
_api.warn_external('tight_layout not applied: number of '
'columns in subplot specifications must be '
'multiples of one another.')
return {}
span_pairs.append((
slice(ss.rowspan.start * div_row, ss.rowspan.stop * div_row),
slice(ss.colspan.start * div_col, ss.colspan.stop * div_col)))
kwargs = _auto_adjust_subplotpars(fig, renderer,
shape=(max_nrows, max_ncols),
span_pairs=span_pairs,
subplot_list=subplot_list,
ax_bbox_list=ax_bbox_list,
pad=pad, h_pad=h_pad, w_pad=w_pad)
# kwargs can be none if tight_layout fails...
if rect is not None and kwargs is not None:
# if rect is given, the whole subplots area (including
# labels) will fit into the rect instead of the
# figure. Note that the rect argument of
# *auto_adjust_subplotpars* specify the area that will be
# covered by the total area of axes.bbox. Thus we call
# auto_adjust_subplotpars twice, where the second run
# with adjusted rect parameters.
left, bottom, right, top = rect
if left is not None:
left += kwargs["left"]
if bottom is not None:
bottom += kwargs["bottom"]
if right is not None:
right -= (1 - kwargs["right"])
if top is not None:
top -= (1 - kwargs["top"])
kwargs = _auto_adjust_subplotpars(fig, renderer,
shape=(max_nrows, max_ncols),
span_pairs=span_pairs,
subplot_list=subplot_list,
ax_bbox_list=ax_bbox_list,
pad=pad, h_pad=h_pad, w_pad=w_pad,
rect=(left, bottom, right, top))
return kwargs

View File

@ -0,0 +1,26 @@
# This is a private module implemented in C++
# As such these type stubs are overly generic, but here to allow these types
# as return types for public methods
from typing import Any, final
@final
class TrapezoidMapTriFinder:
def __init__(self, *args, **kwargs) -> None: ...
def find_many(self, *args, **kwargs) -> Any: ...
def get_tree_stats(self, *args, **kwargs) -> Any: ...
def initialize(self, *args, **kwargs) -> Any: ...
def print_tree(self, *args, **kwargs) -> Any: ...
@final
class TriContourGenerator:
def __init__(self, *args, **kwargs) -> None: ...
def create_contour(self, *args, **kwargs) -> Any: ...
def create_filled_contour(self, *args, **kwargs) -> Any: ...
@final
class Triangulation:
def __init__(self, *args, **kwargs) -> None: ...
def calculate_plane_coefficients(self, *args, **kwargs) -> Any: ...
def get_edges(self, *args, **kwargs) -> Any: ...
def get_neighbors(self, *args, **kwargs) -> Any: ...
def set_mask(self, *args, **kwargs) -> Any: ...

View File

@ -0,0 +1,879 @@
"""
A class representing a Type 1 font.
This version reads pfa and pfb files and splits them for embedding in
pdf files. It also supports SlantFont and ExtendFont transformations,
similarly to pdfTeX and friends. There is no support yet for subsetting.
Usage::
font = Type1Font(filename)
clear_part, encrypted_part, finale = font.parts
slanted_font = font.transform({'slant': 0.167})
extended_font = font.transform({'extend': 1.2})
Sources:
* Adobe Technical Note #5040, Supporting Downloadable PostScript
Language Fonts.
* Adobe Type 1 Font Format, Adobe Systems Incorporated, third printing,
v1.1, 1993. ISBN 0-201-57044-0.
"""
from __future__ import annotations
import binascii
import functools
import logging
import re
import string
import struct
import typing as T
import numpy as np
from matplotlib.cbook import _format_approx
from . import _api
_log = logging.getLogger(__name__)
class _Token:
"""
A token in a PostScript stream.
Attributes
----------
pos : int
Position, i.e. offset from the beginning of the data.
raw : str
Raw text of the token.
kind : str
Description of the token (for debugging or testing).
"""
__slots__ = ('pos', 'raw')
kind = '?'
def __init__(self, pos, raw):
_log.debug('type1font._Token %s at %d: %r', self.kind, pos, raw)
self.pos = pos
self.raw = raw
def __str__(self):
return f"<{self.kind} {self.raw} @{self.pos}>"
def endpos(self):
"""Position one past the end of the token"""
return self.pos + len(self.raw)
def is_keyword(self, *names):
"""Is this a name token with one of the names?"""
return False
def is_slash_name(self):
"""Is this a name token that starts with a slash?"""
return False
def is_delim(self):
"""Is this a delimiter token?"""
return False
def is_number(self):
"""Is this a number token?"""
return False
def value(self):
return self.raw
class _NameToken(_Token):
kind = 'name'
def is_slash_name(self):
return self.raw.startswith('/')
def value(self):
return self.raw[1:]
class _BooleanToken(_Token):
kind = 'boolean'
def value(self):
return self.raw == 'true'
class _KeywordToken(_Token):
kind = 'keyword'
def is_keyword(self, *names):
return self.raw in names
class _DelimiterToken(_Token):
kind = 'delimiter'
def is_delim(self):
return True
def opposite(self):
return {'[': ']', ']': '[',
'{': '}', '}': '{',
'<<': '>>', '>>': '<<'
}[self.raw]
class _WhitespaceToken(_Token):
kind = 'whitespace'
class _StringToken(_Token):
kind = 'string'
_escapes_re = re.compile(r'\\([\\()nrtbf]|[0-7]{1,3})')
_replacements = {'\\': '\\', '(': '(', ')': ')', 'n': '\n',
'r': '\r', 't': '\t', 'b': '\b', 'f': '\f'}
_ws_re = re.compile('[\0\t\r\f\n ]')
@classmethod
def _escape(cls, match):
group = match.group(1)
try:
return cls._replacements[group]
except KeyError:
return chr(int(group, 8))
@functools.lru_cache
def value(self):
if self.raw[0] == '(':
return self._escapes_re.sub(self._escape, self.raw[1:-1])
else:
data = self._ws_re.sub('', self.raw[1:-1])
if len(data) % 2 == 1:
data += '0'
return binascii.unhexlify(data)
class _BinaryToken(_Token):
kind = 'binary'
def value(self):
return self.raw[1:]
class _NumberToken(_Token):
kind = 'number'
def is_number(self):
return True
def value(self):
if '.' not in self.raw:
return int(self.raw)
else:
return float(self.raw)
def _tokenize(data: bytes, skip_ws: bool) -> T.Generator[_Token, int, None]:
"""
A generator that produces _Token instances from Type-1 font code.
The consumer of the generator may send an integer to the tokenizer to
indicate that the next token should be _BinaryToken of the given length.
Parameters
----------
data : bytes
The data of the font to tokenize.
skip_ws : bool
If true, the generator will drop any _WhitespaceTokens from the output.
"""
text = data.decode('ascii', 'replace')
whitespace_or_comment_re = re.compile(r'[\0\t\r\f\n ]+|%[^\r\n]*')
token_re = re.compile(r'/{0,2}[^]\0\t\r\f\n ()<>{}/%[]+')
instring_re = re.compile(r'[()\\]')
hex_re = re.compile(r'^<[0-9a-fA-F\0\t\r\f\n ]*>$')
oct_re = re.compile(r'[0-7]{1,3}')
pos = 0
next_binary: int | None = None
while pos < len(text):
if next_binary is not None:
n = next_binary
next_binary = (yield _BinaryToken(pos, data[pos:pos+n]))
pos += n
continue
match = whitespace_or_comment_re.match(text, pos)
if match:
if not skip_ws:
next_binary = (yield _WhitespaceToken(pos, match.group()))
pos = match.end()
elif text[pos] == '(':
# PostScript string rules:
# - parentheses must be balanced
# - backslashes escape backslashes and parens
# - also codes \n\r\t\b\f and octal escapes are recognized
# - other backslashes do not escape anything
start = pos
pos += 1
depth = 1
while depth:
match = instring_re.search(text, pos)
if match is None:
raise ValueError(
f'Unterminated string starting at {start}')
pos = match.end()
if match.group() == '(':
depth += 1
elif match.group() == ')':
depth -= 1
else: # a backslash
char = text[pos]
if char in r'\()nrtbf':
pos += 1
else:
octal = oct_re.match(text, pos)
if octal:
pos = octal.end()
else:
pass # non-escaping backslash
next_binary = (yield _StringToken(start, text[start:pos]))
elif text[pos:pos + 2] in ('<<', '>>'):
next_binary = (yield _DelimiterToken(pos, text[pos:pos + 2]))
pos += 2
elif text[pos] == '<':
start = pos
try:
pos = text.index('>', pos) + 1
except ValueError as e:
raise ValueError(f'Unterminated hex string starting at {start}'
) from e
if not hex_re.match(text[start:pos]):
raise ValueError(f'Malformed hex string starting at {start}')
next_binary = (yield _StringToken(pos, text[start:pos]))
else:
match = token_re.match(text, pos)
if match:
raw = match.group()
if raw.startswith('/'):
next_binary = (yield _NameToken(pos, raw))
elif match.group() in ('true', 'false'):
next_binary = (yield _BooleanToken(pos, raw))
else:
try:
float(raw)
next_binary = (yield _NumberToken(pos, raw))
except ValueError:
next_binary = (yield _KeywordToken(pos, raw))
pos = match.end()
else:
next_binary = (yield _DelimiterToken(pos, text[pos]))
pos += 1
class _BalancedExpression(_Token):
pass
def _expression(initial, tokens, data):
"""
Consume some number of tokens and return a balanced PostScript expression.
Parameters
----------
initial : _Token
The token that triggered parsing a balanced expression.
tokens : iterator of _Token
Following tokens.
data : bytes
Underlying data that the token positions point to.
Returns
-------
_BalancedExpression
"""
delim_stack = []
token = initial
while True:
if token.is_delim():
if token.raw in ('[', '{'):
delim_stack.append(token)
elif token.raw in (']', '}'):
if not delim_stack:
raise RuntimeError(f"unmatched closing token {token}")
match = delim_stack.pop()
if match.raw != token.opposite():
raise RuntimeError(
f"opening token {match} closed by {token}"
)
if not delim_stack:
break
else:
raise RuntimeError(f'unknown delimiter {token}')
elif not delim_stack:
break
token = next(tokens)
return _BalancedExpression(
initial.pos,
data[initial.pos:token.endpos()].decode('ascii', 'replace')
)
class Type1Font:
"""
A class representing a Type-1 font, for use by backends.
Attributes
----------
parts : tuple
A 3-tuple of the cleartext part, the encrypted part, and the finale of
zeros.
decrypted : bytes
The decrypted form of ``parts[1]``.
prop : dict[str, Any]
A dictionary of font properties. Noteworthy keys include:
- FontName: PostScript name of the font
- Encoding: dict from numeric codes to glyph names
- FontMatrix: bytes object encoding a matrix
- UniqueID: optional font identifier, dropped when modifying the font
- CharStrings: dict from glyph names to byte code
- Subrs: array of byte code subroutines
- OtherSubrs: bytes object encoding some PostScript code
"""
__slots__ = ('parts', 'decrypted', 'prop', '_pos', '_abbr')
# the _pos dict contains (begin, end) indices to parts[0] + decrypted
# so that they can be replaced when transforming the font;
# but since sometimes a definition appears in both parts[0] and decrypted,
# _pos[name] is an array of such pairs
#
# _abbr maps three standard abbreviations to their particular names in
# this font (e.g. 'RD' is named '-|' in some fonts)
def __init__(self, input):
"""
Initialize a Type-1 font.
Parameters
----------
input : str or 3-tuple
Either a pfb file name, or a 3-tuple of already-decoded Type-1
font `~.Type1Font.parts`.
"""
if isinstance(input, tuple) and len(input) == 3:
self.parts = input
else:
with open(input, 'rb') as file:
data = self._read(file)
self.parts = self._split(data)
self.decrypted = self._decrypt(self.parts[1], 'eexec')
self._abbr = {'RD': 'RD', 'ND': 'ND', 'NP': 'NP'}
self._parse()
def _read(self, file):
"""Read the font from a file, decoding into usable parts."""
rawdata = file.read()
if not rawdata.startswith(b'\x80'):
return rawdata
data = b''
while rawdata:
if not rawdata.startswith(b'\x80'):
raise RuntimeError('Broken pfb file (expected byte 128, '
'got %d)' % rawdata[0])
type = rawdata[1]
if type in (1, 2):
length, = struct.unpack('<i', rawdata[2:6])
segment = rawdata[6:6 + length]
rawdata = rawdata[6 + length:]
if type == 1: # ASCII text: include verbatim
data += segment
elif type == 2: # binary data: encode in hexadecimal
data += binascii.hexlify(segment)
elif type == 3: # end of file
break
else:
raise RuntimeError('Unknown segment type %d in pfb file' % type)
return data
def _split(self, data):
"""
Split the Type 1 font into its three main parts.
The three parts are: (1) the cleartext part, which ends in a
eexec operator; (2) the encrypted part; (3) the fixed part,
which contains 512 ASCII zeros possibly divided on various
lines, a cleartomark operator, and possibly something else.
"""
# Cleartext part: just find the eexec and skip whitespace
idx = data.index(b'eexec')
idx += len(b'eexec')
while data[idx] in b' \t\r\n':
idx += 1
len1 = idx
# Encrypted part: find the cleartomark operator and count
# zeros backward
idx = data.rindex(b'cleartomark') - 1
zeros = 512
while zeros and data[idx] in b'0' or data[idx] in b'\r\n':
if data[idx] in b'0':
zeros -= 1
idx -= 1
if zeros:
# this may have been a problem on old implementations that
# used the zeros as necessary padding
_log.info('Insufficiently many zeros in Type 1 font')
# Convert encrypted part to binary (if we read a pfb file, we may end
# up converting binary to hexadecimal to binary again; but if we read
# a pfa file, this part is already in hex, and I am not quite sure if
# even the pfb format guarantees that it will be in binary).
idx1 = len1 + ((idx - len1 + 2) & ~1) # ensure an even number of bytes
binary = binascii.unhexlify(data[len1:idx1])
return data[:len1], binary, data[idx+1:]
@staticmethod
def _decrypt(ciphertext, key, ndiscard=4):
"""
Decrypt ciphertext using the Type-1 font algorithm.
The algorithm is described in Adobe's "Adobe Type 1 Font Format".
The key argument can be an integer, or one of the strings
'eexec' and 'charstring', which map to the key specified for the
corresponding part of Type-1 fonts.
The ndiscard argument should be an integer, usually 4.
That number of bytes is discarded from the beginning of plaintext.
"""
key = _api.check_getitem({'eexec': 55665, 'charstring': 4330}, key=key)
plaintext = []
for byte in ciphertext:
plaintext.append(byte ^ (key >> 8))
key = ((key+byte) * 52845 + 22719) & 0xffff
return bytes(plaintext[ndiscard:])
@staticmethod
def _encrypt(plaintext, key, ndiscard=4):
"""
Encrypt plaintext using the Type-1 font algorithm.
The algorithm is described in Adobe's "Adobe Type 1 Font Format".
The key argument can be an integer, or one of the strings
'eexec' and 'charstring', which map to the key specified for the
corresponding part of Type-1 fonts.
The ndiscard argument should be an integer, usually 4. That
number of bytes is prepended to the plaintext before encryption.
This function prepends NUL bytes for reproducibility, even though
the original algorithm uses random bytes, presumably to avoid
cryptanalysis.
"""
key = _api.check_getitem({'eexec': 55665, 'charstring': 4330}, key=key)
ciphertext = []
for byte in b'\0' * ndiscard + plaintext:
c = byte ^ (key >> 8)
ciphertext.append(c)
key = ((key + c) * 52845 + 22719) & 0xffff
return bytes(ciphertext)
def _parse(self):
"""
Find the values of various font properties. This limited kind
of parsing is described in Chapter 10 "Adobe Type Manager
Compatibility" of the Type-1 spec.
"""
# Start with reasonable defaults
prop = {'Weight': 'Regular', 'ItalicAngle': 0.0, 'isFixedPitch': False,
'UnderlinePosition': -100, 'UnderlineThickness': 50}
pos = {}
data = self.parts[0] + self.decrypted
source = _tokenize(data, True)
while True:
# See if there is a key to be assigned a value
# e.g. /FontName in /FontName /Helvetica def
try:
token = next(source)
except StopIteration:
break
if token.is_delim():
# skip over this - we want top-level keys only
_expression(token, source, data)
if token.is_slash_name():
key = token.value()
keypos = token.pos
else:
continue
# Some values need special parsing
if key in ('Subrs', 'CharStrings', 'Encoding', 'OtherSubrs'):
prop[key], endpos = {
'Subrs': self._parse_subrs,
'CharStrings': self._parse_charstrings,
'Encoding': self._parse_encoding,
'OtherSubrs': self._parse_othersubrs
}[key](source, data)
pos.setdefault(key, []).append((keypos, endpos))
continue
try:
token = next(source)
except StopIteration:
break
if isinstance(token, _KeywordToken):
# constructs like
# FontDirectory /Helvetica known {...} {...} ifelse
# mean the key was not really a key
continue
if token.is_delim():
value = _expression(token, source, data).raw
else:
value = token.value()
# look for a 'def' possibly preceded by access modifiers
try:
kw = next(
kw for kw in source
if not kw.is_keyword('readonly', 'noaccess', 'executeonly')
)
except StopIteration:
break
# sometimes noaccess def and readonly def are abbreviated
if kw.is_keyword('def', self._abbr['ND'], self._abbr['NP']):
prop[key] = value
pos.setdefault(key, []).append((keypos, kw.endpos()))
# detect the standard abbreviations
if value == '{noaccess def}':
self._abbr['ND'] = key
elif value == '{noaccess put}':
self._abbr['NP'] = key
elif value == '{string currentfile exch readstring pop}':
self._abbr['RD'] = key
# Fill in the various *Name properties
if 'FontName' not in prop:
prop['FontName'] = (prop.get('FullName') or
prop.get('FamilyName') or
'Unknown')
if 'FullName' not in prop:
prop['FullName'] = prop['FontName']
if 'FamilyName' not in prop:
extras = ('(?i)([ -](regular|plain|italic|oblique|(semi)?bold|'
'(ultra)?light|extra|condensed))+$')
prop['FamilyName'] = re.sub(extras, '', prop['FullName'])
# Decrypt the encrypted parts
ndiscard = prop.get('lenIV', 4)
cs = prop['CharStrings']
for key, value in cs.items():
cs[key] = self._decrypt(value, 'charstring', ndiscard)
if 'Subrs' in prop:
prop['Subrs'] = [
self._decrypt(value, 'charstring', ndiscard)
for value in prop['Subrs']
]
self.prop = prop
self._pos = pos
def _parse_subrs(self, tokens, _data):
count_token = next(tokens)
if not count_token.is_number():
raise RuntimeError(
f"Token following /Subrs must be a number, was {count_token}"
)
count = count_token.value()
array = [None] * count
next(t for t in tokens if t.is_keyword('array'))
for _ in range(count):
next(t for t in tokens if t.is_keyword('dup'))
index_token = next(tokens)
if not index_token.is_number():
raise RuntimeError(
"Token following dup in Subrs definition must be a "
f"number, was {index_token}"
)
nbytes_token = next(tokens)
if not nbytes_token.is_number():
raise RuntimeError(
"Second token following dup in Subrs definition must "
f"be a number, was {nbytes_token}"
)
token = next(tokens)
if not token.is_keyword(self._abbr['RD']):
raise RuntimeError(
f"Token preceding subr must be {self._abbr['RD']}, "
f"was {token}"
)
binary_token = tokens.send(1+nbytes_token.value())
array[index_token.value()] = binary_token.value()
return array, next(tokens).endpos()
@staticmethod
def _parse_charstrings(tokens, _data):
count_token = next(tokens)
if not count_token.is_number():
raise RuntimeError(
"Token following /CharStrings must be a number, "
f"was {count_token}"
)
count = count_token.value()
charstrings = {}
next(t for t in tokens if t.is_keyword('begin'))
while True:
token = next(t for t in tokens
if t.is_keyword('end') or t.is_slash_name())
if token.raw == 'end':
return charstrings, token.endpos()
glyphname = token.value()
nbytes_token = next(tokens)
if not nbytes_token.is_number():
raise RuntimeError(
f"Token following /{glyphname} in CharStrings definition "
f"must be a number, was {nbytes_token}"
)
next(tokens) # usually RD or |-
binary_token = tokens.send(1+nbytes_token.value())
charstrings[glyphname] = binary_token.value()
@staticmethod
def _parse_encoding(tokens, _data):
# this only works for encodings that follow the Adobe manual
# but some old fonts include non-compliant data - we log a warning
# and return a possibly incomplete encoding
encoding = {}
while True:
token = next(t for t in tokens
if t.is_keyword('StandardEncoding', 'dup', 'def'))
if token.is_keyword('StandardEncoding'):
return _StandardEncoding, token.endpos()
if token.is_keyword('def'):
return encoding, token.endpos()
index_token = next(tokens)
if not index_token.is_number():
_log.warning(
f"Parsing encoding: expected number, got {index_token}"
)
continue
name_token = next(tokens)
if not name_token.is_slash_name():
_log.warning(
f"Parsing encoding: expected slash-name, got {name_token}"
)
continue
encoding[index_token.value()] = name_token.value()
@staticmethod
def _parse_othersubrs(tokens, data):
init_pos = None
while True:
token = next(tokens)
if init_pos is None:
init_pos = token.pos
if token.is_delim():
_expression(token, tokens, data)
elif token.is_keyword('def', 'ND', '|-'):
return data[init_pos:token.endpos()], token.endpos()
def transform(self, effects):
"""
Return a new font that is slanted and/or extended.
Parameters
----------
effects : dict
A dict with optional entries:
- 'slant' : float, default: 0
Tangent of the angle that the font is to be slanted to the
right. Negative values slant to the left.
- 'extend' : float, default: 1
Scaling factor for the font width. Values less than 1 condense
the glyphs.
Returns
-------
`Type1Font`
"""
fontname = self.prop['FontName']
italicangle = self.prop['ItalicAngle']
array = [
float(x) for x in (self.prop['FontMatrix']
.lstrip('[').rstrip(']').split())
]
oldmatrix = np.eye(3, 3)
oldmatrix[0:3, 0] = array[::2]
oldmatrix[0:3, 1] = array[1::2]
modifier = np.eye(3, 3)
if 'slant' in effects:
slant = effects['slant']
fontname += f'_Slant_{int(1000 * slant)}'
italicangle = round(
float(italicangle) - np.arctan(slant) / np.pi * 180,
5
)
modifier[1, 0] = slant
if 'extend' in effects:
extend = effects['extend']
fontname += f'_Extend_{int(1000 * extend)}'
modifier[0, 0] = extend
newmatrix = np.dot(modifier, oldmatrix)
array[::2] = newmatrix[0:3, 0]
array[1::2] = newmatrix[0:3, 1]
fontmatrix = (
f"[{' '.join(_format_approx(x, 6) for x in array)}]"
)
replacements = (
[(x, f'/FontName/{fontname} def')
for x in self._pos['FontName']]
+ [(x, f'/ItalicAngle {italicangle} def')
for x in self._pos['ItalicAngle']]
+ [(x, f'/FontMatrix {fontmatrix} readonly def')
for x in self._pos['FontMatrix']]
+ [(x, '') for x in self._pos.get('UniqueID', [])]
)
data = bytearray(self.parts[0])
data.extend(self.decrypted)
len0 = len(self.parts[0])
for (pos0, pos1), value in sorted(replacements, reverse=True):
data[pos0:pos1] = value.encode('ascii', 'replace')
if pos0 < len(self.parts[0]):
if pos1 >= len(self.parts[0]):
raise RuntimeError(
f"text to be replaced with {value} spans "
"the eexec boundary"
)
len0 += len(value) - pos1 + pos0
data = bytes(data)
return Type1Font((
data[:len0],
self._encrypt(data[len0:], 'eexec'),
self.parts[2]
))
_StandardEncoding = {
**{ord(letter): letter for letter in string.ascii_letters},
0: '.notdef',
32: 'space',
33: 'exclam',
34: 'quotedbl',
35: 'numbersign',
36: 'dollar',
37: 'percent',
38: 'ampersand',
39: 'quoteright',
40: 'parenleft',
41: 'parenright',
42: 'asterisk',
43: 'plus',
44: 'comma',
45: 'hyphen',
46: 'period',
47: 'slash',
48: 'zero',
49: 'one',
50: 'two',
51: 'three',
52: 'four',
53: 'five',
54: 'six',
55: 'seven',
56: 'eight',
57: 'nine',
58: 'colon',
59: 'semicolon',
60: 'less',
61: 'equal',
62: 'greater',
63: 'question',
64: 'at',
91: 'bracketleft',
92: 'backslash',
93: 'bracketright',
94: 'asciicircum',
95: 'underscore',
96: 'quoteleft',
123: 'braceleft',
124: 'bar',
125: 'braceright',
126: 'asciitilde',
161: 'exclamdown',
162: 'cent',
163: 'sterling',
164: 'fraction',
165: 'yen',
166: 'florin',
167: 'section',
168: 'currency',
169: 'quotesingle',
170: 'quotedblleft',
171: 'guillemotleft',
172: 'guilsinglleft',
173: 'guilsinglright',
174: 'fi',
175: 'fl',
177: 'endash',
178: 'dagger',
179: 'daggerdbl',
180: 'periodcentered',
182: 'paragraph',
183: 'bullet',
184: 'quotesinglbase',
185: 'quotedblbase',
186: 'quotedblright',
187: 'guillemotright',
188: 'ellipsis',
189: 'perthousand',
191: 'questiondown',
193: 'grave',
194: 'acute',
195: 'circumflex',
196: 'tilde',
197: 'macron',
198: 'breve',
199: 'dotaccent',
200: 'dieresis',
202: 'ring',
203: 'cedilla',
205: 'hungarumlaut',
206: 'ogonek',
207: 'caron',
208: 'emdash',
225: 'AE',
227: 'ordfeminine',
232: 'Lslash',
233: 'Oslash',
234: 'OE',
235: 'ordmasculine',
241: 'ae',
245: 'dotlessi',
248: 'lslash',
249: 'oslash',
250: 'oe',
251: 'germandbls',
}

View File

@ -0,0 +1 @@
version = "3.9.2"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,217 @@
import abc
from collections.abc import Callable, Collection, Iterable, Sequence, Generator
import contextlib
from pathlib import Path
from matplotlib.artist import Artist
from matplotlib.backend_bases import TimerBase
from matplotlib.figure import Figure
from typing import Any
subprocess_creation_flags: int
def adjusted_figsize(w: float, h: float, dpi: float, n: int) -> tuple[float, float]: ...
class MovieWriterRegistry:
def __init__(self) -> None: ...
def register(
self, name: str
) -> Callable[[type[AbstractMovieWriter]], type[AbstractMovieWriter]]: ...
def is_available(self, name: str) -> bool: ...
def __iter__(self) -> Generator[str, None, None]: ...
def list(self) -> list[str]: ...
def __getitem__(self, name: str) -> type[AbstractMovieWriter]: ...
writers: MovieWriterRegistry
class AbstractMovieWriter(abc.ABC, metaclass=abc.ABCMeta):
fps: int
metadata: dict[str, str]
codec: str
bitrate: int
def __init__(
self,
fps: int = ...,
metadata: dict[str, str] | None = ...,
codec: str | None = ...,
bitrate: int | None = ...,
) -> None: ...
outfile: str | Path
fig: Figure
dpi: float
@abc.abstractmethod
def setup(self, fig: Figure, outfile: str | Path, dpi: float | None = ...) -> None: ...
@property
def frame_size(self) -> tuple[int, int]: ...
@abc.abstractmethod
def grab_frame(self, **savefig_kwargs) -> None: ...
@abc.abstractmethod
def finish(self) -> None: ...
@contextlib.contextmanager
def saving(
self, fig: Figure, outfile: str | Path, dpi: float | None, *args, **kwargs
) -> Generator[AbstractMovieWriter, None, None]: ...
class MovieWriter(AbstractMovieWriter):
supported_formats: list[str]
frame_format: str
extra_args: list[str] | None
def __init__(
self,
fps: int = ...,
codec: str | None = ...,
bitrate: int | None = ...,
extra_args: list[str] | None = ...,
metadata: dict[str, str] | None = ...,
) -> None: ...
def setup(self, fig: Figure, outfile: str | Path, dpi: float | None = ...) -> None: ...
def grab_frame(self, **savefig_kwargs) -> None: ...
def finish(self) -> None: ...
@classmethod
def bin_path(cls) -> str: ...
@classmethod
def isAvailable(cls) -> bool: ...
class FileMovieWriter(MovieWriter):
fig: Figure
outfile: str | Path
dpi: float
temp_prefix: str
fname_format_str: str
def setup(
self,
fig: Figure,
outfile: str | Path,
dpi: float | None = ...,
frame_prefix: str | Path | None = ...,
) -> None: ...
def __del__(self) -> None: ...
@property
def frame_format(self) -> str: ...
@frame_format.setter
def frame_format(self, frame_format: str) -> None: ...
class PillowWriter(AbstractMovieWriter):
@classmethod
def isAvailable(cls) -> bool: ...
def setup(
self, fig: Figure, outfile: str | Path, dpi: float | None = ...
) -> None: ...
def grab_frame(self, **savefig_kwargs) -> None: ...
def finish(self) -> None: ...
class FFMpegBase:
codec: str
@property
def output_args(self) -> list[str]: ...
class FFMpegWriter(FFMpegBase, MovieWriter): ...
class FFMpegFileWriter(FFMpegBase, FileMovieWriter):
supported_formats: list[str]
class ImageMagickBase:
@classmethod
def bin_path(cls) -> str: ...
@classmethod
def isAvailable(cls) -> bool: ...
class ImageMagickWriter(ImageMagickBase, MovieWriter):
input_names: str
class ImageMagickFileWriter(ImageMagickBase, FileMovieWriter):
supported_formats: list[str]
@property
def input_names(self) -> str: ...
class HTMLWriter(FileMovieWriter):
supported_formats: list[str]
@classmethod
def isAvailable(cls) -> bool: ...
embed_frames: bool
default_mode: str
def __init__(
self,
fps: int = ...,
codec: str | None = ...,
bitrate: int | None = ...,
extra_args: list[str] | None = ...,
metadata: dict[str, str] | None = ...,
embed_frames: bool = ...,
default_mode: str = ...,
embed_limit: float | None = ...,
) -> None: ...
def setup(
self,
fig: Figure,
outfile: str | Path,
dpi: float | None = ...,
frame_dir: str | Path | None = ...,
) -> None: ...
def grab_frame(self, **savefig_kwargs): ...
def finish(self) -> None: ...
class Animation:
frame_seq: Iterable[Artist]
event_source: Any
def __init__(
self, fig: Figure, event_source: Any | None = ..., blit: bool = ...
) -> None: ...
def __del__(self) -> None: ...
def save(
self,
filename: str | Path,
writer: AbstractMovieWriter | str | None = ...,
fps: int | None = ...,
dpi: float | None = ...,
codec: str | None = ...,
bitrate: int | None = ...,
extra_args: list[str] | None = ...,
metadata: dict[str, str] | None = ...,
extra_anim: list[Animation] | None = ...,
savefig_kwargs: dict[str, Any] | None = ...,
*,
progress_callback: Callable[[int, int], Any] | None = ...
) -> None: ...
def new_frame_seq(self) -> Iterable[Artist]: ...
def new_saved_frame_seq(self) -> Iterable[Artist]: ...
def to_html5_video(self, embed_limit: float | None = ...) -> str: ...
def to_jshtml(
self,
fps: int | None = ...,
embed_frames: bool = ...,
default_mode: str | None = ...,
) -> str: ...
def _repr_html_(self) -> str: ...
def pause(self) -> None: ...
def resume(self) -> None: ...
class TimedAnimation(Animation):
def __init__(
self,
fig: Figure,
interval: int = ...,
repeat_delay: int = ...,
repeat: bool = ...,
event_source: TimerBase | None = ...,
*args,
**kwargs
) -> None: ...
class ArtistAnimation(TimedAnimation):
def __init__(self, fig: Figure, artists: Sequence[Collection[Artist]], *args, **kwargs) -> None: ...
class FuncAnimation(TimedAnimation):
def __init__(
self,
fig: Figure,
func: Callable[..., Iterable[Artist]],
frames: Iterable | int | Callable[[], Generator] | None = ...,
init_func: Callable[[], Iterable[Artist]] | None = ...,
fargs: tuple[Any, ...] | None = ...,
save_count: int | None = ...,
*,
cache_frame_data: bool = ...,
**kwargs
) -> None: ...

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,181 @@
from .axes._base import _AxesBase
from .backend_bases import RendererBase, MouseEvent
from .figure import Figure, SubFigure
from .path import Path
from .patches import Patch
from .patheffects import AbstractPathEffect
from .transforms import (
BboxBase,
Bbox,
Transform,
TransformedPatchPath,
TransformedPath,
)
import numpy as np
from collections.abc import Callable, Iterable
from typing import Any, NamedTuple, TextIO, overload
from numpy.typing import ArrayLike
def allow_rasterization(draw): ...
class _XYPair(NamedTuple):
x: ArrayLike
y: ArrayLike
class _Unset: ...
class Artist:
zorder: float
stale_callback: Callable[[Artist, bool], None] | None
figure: Figure | SubFigure | None
clipbox: BboxBase | None
def __init__(self) -> None: ...
def remove(self) -> None: ...
def have_units(self) -> bool: ...
# TODO units
def convert_xunits(self, x): ...
def convert_yunits(self, y): ...
@property
def axes(self) -> _AxesBase | None: ...
@axes.setter
def axes(self, new_axes: _AxesBase | None) -> None: ...
@property
def stale(self) -> bool: ...
@stale.setter
def stale(self, val: bool) -> None: ...
def get_window_extent(self, renderer: RendererBase | None = ...) -> Bbox: ...
def get_tightbbox(self, renderer: RendererBase | None = ...) -> Bbox | None: ...
def add_callback(self, func: Callable[[Artist], Any]) -> int: ...
def remove_callback(self, oid: int) -> None: ...
def pchanged(self) -> None: ...
def is_transform_set(self) -> bool: ...
def set_transform(self, t: Transform | None) -> None: ...
def get_transform(self) -> Transform: ...
def get_children(self) -> list[Artist]: ...
# TODO can these dicts be type narrowed? e.g. str keys
def contains(self, mouseevent: MouseEvent) -> tuple[bool, dict[Any, Any]]: ...
def pickable(self) -> bool: ...
def pick(self, mouseevent: MouseEvent) -> None: ...
def set_picker(
self,
picker: None
| bool
| float
| Callable[[Artist, MouseEvent], tuple[bool, dict[Any, Any]]],
) -> None: ...
def get_picker(
self,
) -> None | bool | float | Callable[
[Artist, MouseEvent], tuple[bool, dict[Any, Any]]
]: ...
def get_url(self) -> str | None: ...
def set_url(self, url: str | None) -> None: ...
def get_gid(self) -> str | None: ...
def set_gid(self, gid: str | None) -> None: ...
def get_snap(self) -> bool | None: ...
def set_snap(self, snap: bool | None) -> None: ...
def get_sketch_params(self) -> tuple[float, float, float] | None: ...
def set_sketch_params(
self,
scale: float | None = ...,
length: float | None = ...,
randomness: float | None = ...,
) -> None: ...
def set_path_effects(self, path_effects: list[AbstractPathEffect]) -> None: ...
def get_path_effects(self) -> list[AbstractPathEffect]: ...
def get_figure(self) -> Figure | None: ...
def set_figure(self, fig: Figure) -> None: ...
def set_clip_box(self, clipbox: BboxBase | None) -> None: ...
def set_clip_path(
self,
path: Patch | Path | TransformedPath | TransformedPatchPath | None,
transform: Transform | None = ...,
) -> None: ...
def get_alpha(self) -> float | None: ...
def get_visible(self) -> bool: ...
def get_animated(self) -> bool: ...
def get_in_layout(self) -> bool: ...
def get_clip_on(self) -> bool: ...
def get_clip_box(self) -> Bbox | None: ...
def get_clip_path(
self,
) -> Patch | Path | TransformedPath | TransformedPatchPath | None: ...
def get_transformed_clip_path_and_affine(
self,
) -> tuple[None, None] | tuple[Path, Transform]: ...
def set_clip_on(self, b: bool) -> None: ...
def get_rasterized(self) -> bool: ...
def set_rasterized(self, rasterized: bool) -> None: ...
def get_agg_filter(self) -> Callable[[ArrayLike, float], tuple[np.ndarray, float, float]] | None: ...
def set_agg_filter(
self, filter_func: Callable[[ArrayLike, float], tuple[np.ndarray, float, float]] | None
) -> None: ...
def draw(self, renderer: RendererBase) -> None: ...
def set_alpha(self, alpha: float | None) -> None: ...
def set_visible(self, b: bool) -> None: ...
def set_animated(self, b: bool) -> None: ...
def set_in_layout(self, in_layout: bool) -> None: ...
def get_label(self) -> object: ...
def set_label(self, s: object) -> None: ...
def get_zorder(self) -> float: ...
def set_zorder(self, level: float) -> None: ...
@property
def sticky_edges(self) -> _XYPair: ...
def update_from(self, other: Artist) -> None: ...
def properties(self) -> dict[str, Any]: ...
def update(self, props: dict[str, Any]) -> list[Any]: ...
def _internal_update(self, kwargs: Any) -> list[Any]: ...
def set(self, **kwargs: Any) -> list[Any]: ...
def findobj(
self,
match: None | Callable[[Artist], bool] | type[Artist] = ...,
include_self: bool = ...,
) -> list[Artist]: ...
def get_cursor_data(self, event: MouseEvent) -> Any: ...
def format_cursor_data(self, data: Any) -> str: ...
def get_mouseover(self) -> bool: ...
def set_mouseover(self, mouseover: bool) -> None: ...
@property
def mouseover(self) -> bool: ...
@mouseover.setter
def mouseover(self, mouseover: bool) -> None: ...
class ArtistInspector:
oorig: Artist | type[Artist]
o: type[Artist]
aliasd: dict[str, set[str]]
def __init__(
self, o: Artist | type[Artist] | Iterable[Artist | type[Artist]]
) -> None: ...
def get_aliases(self) -> dict[str, set[str]]: ...
def get_valid_values(self, attr: str) -> str | None: ...
def get_setters(self) -> list[str]: ...
@staticmethod
def number_of_parameters(func: Callable) -> int: ...
@staticmethod
def is_alias(method: Callable) -> bool: ...
def aliased_name(self, s: str) -> str: ...
def aliased_name_rest(self, s: str, target: str) -> str: ...
@overload
def pprint_setters(
self, prop: None = ..., leadingspace: int = ...
) -> list[str]: ...
@overload
def pprint_setters(self, prop: str, leadingspace: int = ...) -> str: ...
@overload
def pprint_setters_rest(
self, prop: None = ..., leadingspace: int = ...
) -> list[str]: ...
@overload
def pprint_setters_rest(self, prop: str, leadingspace: int = ...) -> str: ...
def properties(self) -> dict[str, Any]: ...
def pprint_getters(self) -> list[str]: ...
def getp(obj: Artist, property: str | None = ...) -> Any: ...
get = getp
def setp(obj: Artist, *args, file: TextIO | None = ..., **kwargs) -> list[Any] | None: ...
def kwdoc(artist: Artist | type[Artist] | Iterable[Artist | type[Artist]]) -> str: ...

View File

@ -0,0 +1,18 @@
from . import _base
from ._axes import Axes # noqa: F401
# Backcompat.
Subplot = Axes
class _SubplotBaseMeta(type):
def __instancecheck__(self, obj):
return (isinstance(obj, _base._AxesBase)
and obj.get_subplotspec() is not None)
class SubplotBase(metaclass=_SubplotBaseMeta):
pass
def subplot_class_factory(cls): return cls

View File

@ -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

View File

@ -0,0 +1,772 @@
from matplotlib.axes._base import _AxesBase
from matplotlib.axes._secondary_axes import SecondaryAxis
from matplotlib.artist import Artist
from matplotlib.backend_bases import RendererBase
from matplotlib.collections import (
Collection,
LineCollection,
PathCollection,
PolyCollection,
EventCollection,
QuadMesh,
)
from matplotlib.colors import Colormap, Normalize
from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer
from matplotlib.contour import ContourSet, QuadContourSet
from matplotlib.image import AxesImage, PcolorImage
from matplotlib.legend import Legend
from matplotlib.legend_handler import HandlerBase
from matplotlib.lines import Line2D, AxLine
from matplotlib.mlab import GaussianKDE
from matplotlib.patches import Rectangle, FancyArrow, Polygon, StepPatch, Wedge
from matplotlib.quiver import Quiver, QuiverKey, Barbs
from matplotlib.text import Annotation, Text
from matplotlib.transforms import Transform, Bbox
import matplotlib.tri as mtri
import matplotlib.table as mtable
import matplotlib.stackplot as mstack
import matplotlib.streamplot as mstream
import datetime
import PIL.Image
from collections.abc import Callable, Iterable, Sequence
from typing import Any, Literal, overload
import numpy as np
from numpy.typing import ArrayLike
from matplotlib.typing import ColorType, MarkerType, LineStyleType
class Axes(_AxesBase):
def get_title(self, loc: Literal["left", "center", "right"] = ...) -> str: ...
def set_title(
self,
label: str,
fontdict: dict[str, Any] | None = ...,
loc: Literal["left", "center", "right"] | None = ...,
pad: float | None = ...,
*,
y: float | None = ...,
**kwargs
) -> Text: ...
def get_legend_handles_labels(
self, legend_handler_map: dict[type, HandlerBase] | None = ...
) -> tuple[list[Artist], list[Any]]: ...
legend_: Legend | None
@overload
def legend(self) -> Legend: ...
@overload
def legend(self, handles: Iterable[Artist | tuple[Artist, ...]], labels: Iterable[str], **kwargs) -> Legend: ...
@overload
def legend(self, *, handles: Iterable[Artist | tuple[Artist, ...]], **kwargs) -> Legend: ...
@overload
def legend(self, labels: Iterable[str], **kwargs) -> Legend: ...
@overload
def legend(self, **kwargs) -> Legend: ...
def inset_axes(
self,
bounds: tuple[float, float, float, float],
*,
transform: Transform | None = ...,
zorder: float = ...,
**kwargs
) -> Axes: ...
def indicate_inset(
self,
bounds: tuple[float, float, float, float],
inset_ax: Axes | None = ...,
*,
transform: Transform | None = ...,
facecolor: ColorType = ...,
edgecolor: ColorType = ...,
alpha: float = ...,
zorder: float = ...,
**kwargs
) -> Rectangle: ...
def indicate_inset_zoom(self, inset_ax: Axes, **kwargs) -> Rectangle: ...
def secondary_xaxis(
self,
location: Literal["top", "bottom"] | float,
*,
functions: tuple[
Callable[[ArrayLike], ArrayLike], Callable[[ArrayLike], ArrayLike]
]
| Transform
| None = ...,
transform: Transform | None = ...,
**kwargs
) -> SecondaryAxis: ...
def secondary_yaxis(
self,
location: Literal["left", "right"] | float,
*,
functions: tuple[
Callable[[ArrayLike], ArrayLike], Callable[[ArrayLike], ArrayLike]
]
| Transform
| None = ...,
transform: Transform | None = ...,
**kwargs
) -> SecondaryAxis: ...
def text(
self,
x: float,
y: float,
s: str,
fontdict: dict[str, Any] | None = ...,
**kwargs
) -> Text: ...
def annotate(
self,
text: str,
xy: tuple[float, float],
xytext: tuple[float, float] | None = ...,
xycoords: str
| Artist
| Transform
| Callable[[RendererBase], Bbox | Transform]
| tuple[float, float] = ...,
textcoords: str
| Artist
| Transform
| Callable[[RendererBase], Bbox | Transform]
| tuple[float, float]
| None = ...,
arrowprops: dict[str, Any] | None = ...,
annotation_clip: bool | None = ...,
**kwargs
) -> Annotation: ...
def axhline(
self, y: float = ..., xmin: float = ..., xmax: float = ..., **kwargs
) -> Line2D: ...
def axvline(
self, x: float = ..., ymin: float = ..., ymax: float = ..., **kwargs
) -> Line2D: ...
# TODO: Could separate the xy2 and slope signatures
def axline(
self,
xy1: tuple[float, float],
xy2: tuple[float, float] | None = ...,
*,
slope: float | None = ...,
**kwargs
) -> AxLine: ...
def axhspan(
self, ymin: float, ymax: float, xmin: float = ..., xmax: float = ..., **kwargs
) -> Rectangle: ...
def axvspan(
self, xmin: float, xmax: float, ymin: float = ..., ymax: float = ..., **kwargs
) -> Rectangle: ...
def hlines(
self,
y: float | ArrayLike,
xmin: float | ArrayLike,
xmax: float | ArrayLike,
colors: ColorType | Sequence[ColorType] | None = ...,
linestyles: LineStyleType = ...,
label: str = ...,
*,
data=...,
**kwargs
) -> LineCollection: ...
def vlines(
self,
x: float | ArrayLike,
ymin: float | ArrayLike,
ymax: float | ArrayLike,
colors: ColorType | Sequence[ColorType] | None = ...,
linestyles: LineStyleType = ...,
label: str = ...,
*,
data=...,
**kwargs
) -> LineCollection: ...
def eventplot(
self,
positions: ArrayLike | Sequence[ArrayLike],
orientation: Literal["horizontal", "vertical"] = ...,
lineoffsets: float | Sequence[float] = ...,
linelengths: float | Sequence[float] = ...,
linewidths: float | Sequence[float] | None = ...,
colors: ColorType | Sequence[ColorType] | None = ...,
alpha: float | Sequence[float] | None = ...,
linestyles: LineStyleType | Sequence[LineStyleType] = ...,
*,
data=...,
**kwargs
) -> EventCollection: ...
def plot(
self,
*args: float | ArrayLike | str,
scalex: bool = ...,
scaley: bool = ...,
data = ...,
**kwargs
) -> list[Line2D]: ...
def plot_date(
self,
x: ArrayLike,
y: ArrayLike,
fmt: str = ...,
tz: str | datetime.tzinfo | None = ...,
xdate: bool = ...,
ydate: bool = ...,
*,
data=...,
**kwargs
) -> list[Line2D]: ...
def loglog(self, *args, **kwargs) -> list[Line2D]: ...
def semilogx(self, *args, **kwargs) -> list[Line2D]: ...
def semilogy(self, *args, **kwargs) -> list[Line2D]: ...
def acorr(
self, x: ArrayLike, *, data=..., **kwargs
) -> tuple[np.ndarray, np.ndarray, LineCollection | Line2D, Line2D | None]: ...
def xcorr(
self,
x: ArrayLike,
y: ArrayLike,
normed: bool = ...,
detrend: Callable[[ArrayLike], ArrayLike] = ...,
usevlines: bool = ...,
maxlags: int = ...,
*,
data = ...,
**kwargs
) -> tuple[np.ndarray, np.ndarray, LineCollection | Line2D, Line2D | None]: ...
def step(
self,
x: ArrayLike,
y: ArrayLike,
*args,
where: Literal["pre", "post", "mid"] = ...,
data = ...,
**kwargs
) -> list[Line2D]: ...
def bar(
self,
x: float | ArrayLike,
height: float | ArrayLike,
width: float | ArrayLike = ...,
bottom: float | ArrayLike | None = ...,
*,
align: Literal["center", "edge"] = ...,
data = ...,
**kwargs
) -> BarContainer: ...
def barh(
self,
y: float | ArrayLike,
width: float | ArrayLike,
height: float | ArrayLike = ...,
left: float | ArrayLike | None = ...,
*,
align: Literal["center", "edge"] = ...,
data = ...,
**kwargs
) -> BarContainer: ...
def bar_label(
self,
container: BarContainer,
labels: ArrayLike | None = ...,
*,
fmt: str | Callable[[float], str] = ...,
label_type: Literal["center", "edge"] = ...,
padding: float = ...,
**kwargs
) -> list[Annotation]: ...
def broken_barh(
self,
xranges: Sequence[tuple[float, float]],
yrange: tuple[float, float],
*,
data=...,
**kwargs
) -> PolyCollection: ...
def stem(
self,
*args: ArrayLike | str,
linefmt: str | None = ...,
markerfmt: str | None = ...,
basefmt: str | None = ...,
bottom: float = ...,
label: str | None = ...,
orientation: Literal["vertical", "horizontal"] = ...,
data=...,
) -> StemContainer: ...
# TODO: data kwarg preprocessor?
def pie(
self,
x: ArrayLike,
explode: ArrayLike | None = ...,
labels: Sequence[str] | None = ...,
colors: ColorType | Sequence[ColorType] | None = ...,
autopct: str | Callable[[float], str] | None = ...,
pctdistance: float = ...,
shadow: bool = ...,
labeldistance: float | None = ...,
startangle: float = ...,
radius: float = ...,
counterclock: bool = ...,
wedgeprops: dict[str, Any] | None = ...,
textprops: dict[str, Any] | None = ...,
center: tuple[float, float] = ...,
frame: bool = ...,
rotatelabels: bool = ...,
*,
normalize: bool = ...,
hatch: str | Sequence[str] | None = ...,
data=...,
) -> tuple[list[Wedge], list[Text]] | tuple[
list[Wedge], list[Text], list[Text]
]: ...
def errorbar(
self,
x: float | ArrayLike,
y: float | ArrayLike,
yerr: float | ArrayLike | None = ...,
xerr: float | ArrayLike | None = ...,
fmt: str = ...,
ecolor: ColorType | None = ...,
elinewidth: float | None = ...,
capsize: float | None = ...,
barsabove: bool = ...,
lolims: bool | ArrayLike = ...,
uplims: bool | ArrayLike = ...,
xlolims: bool | ArrayLike = ...,
xuplims: bool | ArrayLike = ...,
errorevery: int | tuple[int, int] = ...,
capthick: float | None = ...,
*,
data=...,
**kwargs
) -> ErrorbarContainer: ...
def boxplot(
self,
x: ArrayLike | Sequence[ArrayLike],
notch: bool | None = ...,
sym: str | None = ...,
vert: bool | None = ...,
whis: float | tuple[float, float] | None = ...,
positions: ArrayLike | None = ...,
widths: float | ArrayLike | None = ...,
patch_artist: bool | None = ...,
bootstrap: int | None = ...,
usermedians: ArrayLike | None = ...,
conf_intervals: ArrayLike | None = ...,
meanline: bool | None = ...,
showmeans: bool | None = ...,
showcaps: bool | None = ...,
showbox: bool | None = ...,
showfliers: bool | None = ...,
boxprops: dict[str, Any] | None = ...,
tick_labels: Sequence[str] | None = ...,
flierprops: dict[str, Any] | None = ...,
medianprops: dict[str, Any] | None = ...,
meanprops: dict[str, Any] | None = ...,
capprops: dict[str, Any] | None = ...,
whiskerprops: dict[str, Any] | None = ...,
manage_ticks: bool = ...,
autorange: bool = ...,
zorder: float | None = ...,
capwidths: float | ArrayLike | None = ...,
label: Sequence[str] | None = ...,
*,
data=...,
) -> dict[str, Any]: ...
def bxp(
self,
bxpstats: Sequence[dict[str, Any]],
positions: ArrayLike | None = ...,
widths: float | ArrayLike | None = ...,
vert: bool = ...,
patch_artist: bool = ...,
shownotches: bool = ...,
showmeans: bool = ...,
showcaps: bool = ...,
showbox: bool = ...,
showfliers: bool = ...,
boxprops: dict[str, Any] | None = ...,
whiskerprops: dict[str, Any] | None = ...,
flierprops: dict[str, Any] | None = ...,
medianprops: dict[str, Any] | None = ...,
capprops: dict[str, Any] | None = ...,
meanprops: dict[str, Any] | None = ...,
meanline: bool = ...,
manage_ticks: bool = ...,
zorder: float | None = ...,
capwidths: float | ArrayLike | None = ...,
label: Sequence[str] | None = ...,
) -> dict[str, Any]: ...
def scatter(
self,
x: float | ArrayLike,
y: float | ArrayLike,
s: float | ArrayLike | None = ...,
c: ArrayLike | Sequence[ColorType] | ColorType | None = ...,
marker: MarkerType | None = ...,
cmap: str | Colormap | None = ...,
norm: str | Normalize | None = ...,
vmin: float | None = ...,
vmax: float | None = ...,
alpha: float | None = ...,
linewidths: float | Sequence[float] | None = ...,
*,
edgecolors: Literal["face", "none"] | ColorType | Sequence[ColorType] | None = ...,
plotnonfinite: bool = ...,
data=...,
**kwargs
) -> PathCollection: ...
def hexbin(
self,
x: ArrayLike,
y: ArrayLike,
C: ArrayLike | None = ...,
gridsize: int | tuple[int, int] = ...,
bins: Literal["log"] | int | Sequence[float] | None = ...,
xscale: Literal["linear", "log"] = ...,
yscale: Literal["linear", "log"] = ...,
extent: tuple[float, float, float, float] | None = ...,
cmap: str | Colormap | None = ...,
norm: str | Normalize | None = ...,
vmin: float | None = ...,
vmax: float | None = ...,
alpha: float | None = ...,
linewidths: float | None = ...,
edgecolors: Literal["face", "none"] | ColorType = ...,
reduce_C_function: Callable[[np.ndarray | list[float]], float] = ...,
mincnt: int | None = ...,
marginals: bool = ...,
*,
data=...,
**kwargs
) -> PolyCollection: ...
def arrow(
self, x: float, y: float, dx: float, dy: float, **kwargs
) -> FancyArrow: ...
def quiverkey(
self, Q: Quiver, X: float, Y: float, U: float, label: str, **kwargs
) -> QuiverKey: ...
def quiver(self, *args, data=..., **kwargs) -> Quiver: ...
def barbs(self, *args, data=..., **kwargs) -> Barbs: ...
def fill(self, *args, data=..., **kwargs) -> list[Polygon]: ...
def fill_between(
self,
x: ArrayLike,
y1: ArrayLike | float,
y2: ArrayLike | float = ...,
where: Sequence[bool] | None = ...,
interpolate: bool = ...,
step: Literal["pre", "post", "mid"] | None = ...,
*,
data=...,
**kwargs
) -> PolyCollection: ...
def fill_betweenx(
self,
y: ArrayLike,
x1: ArrayLike | float,
x2: ArrayLike | float = ...,
where: Sequence[bool] | None = ...,
step: Literal["pre", "post", "mid"] | None = ...,
interpolate: bool = ...,
*,
data=...,
**kwargs
) -> PolyCollection: ...
def imshow(
self,
X: ArrayLike | PIL.Image.Image,
cmap: str | Colormap | None = ...,
norm: str | Normalize | None = ...,
*,
aspect: Literal["equal", "auto"] | float | None = ...,
interpolation: str | None = ...,
alpha: float | ArrayLike | None = ...,
vmin: float | None = ...,
vmax: float | None = ...,
origin: Literal["upper", "lower"] | None = ...,
extent: tuple[float, float, float, float] | None = ...,
interpolation_stage: Literal["data", "rgba"] | None = ...,
filternorm: bool = ...,
filterrad: float = ...,
resample: bool | None = ...,
url: str | None = ...,
data=...,
**kwargs
) -> AxesImage: ...
def pcolor(
self,
*args: ArrayLike,
shading: Literal["flat", "nearest", "auto"] | None = ...,
alpha: float | None = ...,
norm: str | Normalize | None = ...,
cmap: str | Colormap | None = ...,
vmin: float | None = ...,
vmax: float | None = ...,
data=...,
**kwargs
) -> Collection: ...
def pcolormesh(
self,
*args: ArrayLike,
alpha: float | None = ...,
norm: str | Normalize | None = ...,
cmap: str | Colormap | None = ...,
vmin: float | None = ...,
vmax: float | None = ...,
shading: Literal["flat", "nearest", "gouraud", "auto"] | None = ...,
antialiased: bool = ...,
data=...,
**kwargs
) -> QuadMesh: ...
def pcolorfast(
self,
*args: ArrayLike | tuple[float, float],
alpha: float | None = ...,
norm: str | Normalize | None = ...,
cmap: str | Colormap | None = ...,
vmin: float | None = ...,
vmax: float | None = ...,
data=...,
**kwargs
) -> AxesImage | PcolorImage | QuadMesh: ...
def contour(self, *args, data=..., **kwargs) -> QuadContourSet: ...
def contourf(self, *args, data=..., **kwargs) -> QuadContourSet: ...
def clabel(
self, CS: ContourSet, levels: ArrayLike | None = ..., **kwargs
) -> list[Text]: ...
def hist(
self,
x: ArrayLike | Sequence[ArrayLike],
bins: int | Sequence[float] | str | None = ...,
range: tuple[float, float] | None = ...,
density: bool = ...,
weights: ArrayLike | None = ...,
cumulative: bool | float = ...,
bottom: ArrayLike | float | None = ...,
histtype: Literal["bar", "barstacked", "step", "stepfilled"] = ...,
align: Literal["left", "mid", "right"] = ...,
orientation: Literal["vertical", "horizontal"] = ...,
rwidth: float | None = ...,
log: bool = ...,
color: ColorType | Sequence[ColorType] | None = ...,
label: str | Sequence[str] | None = ...,
stacked: bool = ...,
*,
data=...,
**kwargs
) -> tuple[
np.ndarray | list[np.ndarray],
np.ndarray,
BarContainer | Polygon | list[BarContainer | Polygon],
]: ...
def stairs(
self,
values: ArrayLike,
edges: ArrayLike | None = ...,
*,
orientation: Literal["vertical", "horizontal"] = ...,
baseline: float | ArrayLike | None = ...,
fill: bool = ...,
data=...,
**kwargs
) -> StepPatch: ...
def hist2d(
self,
x: ArrayLike,
y: ArrayLike,
bins: None
| int
| tuple[int, int]
| ArrayLike
| tuple[ArrayLike, ArrayLike] = ...,
range: ArrayLike | None = ...,
density: bool = ...,
weights: ArrayLike | None = ...,
cmin: float | None = ...,
cmax: float | None = ...,
*,
data=...,
**kwargs
) -> tuple[np.ndarray, np.ndarray, np.ndarray, QuadMesh]: ...
def ecdf(
self,
x: ArrayLike,
weights: ArrayLike | None = ...,
*,
complementary: bool=...,
orientation: Literal["vertical", "horizonatal"]=...,
compress: bool=...,
data=...,
**kwargs
) -> Line2D: ...
def psd(
self,
x: ArrayLike,
NFFT: int | None = ...,
Fs: float | None = ...,
Fc: int | None = ...,
detrend: Literal["none", "mean", "linear"]
| Callable[[ArrayLike], ArrayLike]
| None = ...,
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ...,
noverlap: int | None = ...,
pad_to: int | None = ...,
sides: Literal["default", "onesided", "twosided"] | None = ...,
scale_by_freq: bool | None = ...,
return_line: bool | None = ...,
*,
data=...,
**kwargs
) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, Line2D]: ...
def csd(
self,
x: ArrayLike,
y: ArrayLike,
NFFT: int | None = ...,
Fs: float | None = ...,
Fc: int | None = ...,
detrend: Literal["none", "mean", "linear"]
| Callable[[ArrayLike], ArrayLike]
| None = ...,
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ...,
noverlap: int | None = ...,
pad_to: int | None = ...,
sides: Literal["default", "onesided", "twosided"] | None = ...,
scale_by_freq: bool | None = ...,
return_line: bool | None = ...,
*,
data=...,
**kwargs
) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, Line2D]: ...
def magnitude_spectrum(
self,
x: ArrayLike,
Fs: float | None = ...,
Fc: int | None = ...,
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ...,
pad_to: int | None = ...,
sides: Literal["default", "onesided", "twosided"] | None = ...,
scale: Literal["default", "linear", "dB"] | None = ...,
*,
data=...,
**kwargs
) -> tuple[np.ndarray, np.ndarray, Line2D]: ...
def angle_spectrum(
self,
x: ArrayLike,
Fs: float | None = ...,
Fc: int | None = ...,
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ...,
pad_to: int | None = ...,
sides: Literal["default", "onesided", "twosided"] | None = ...,
*,
data=...,
**kwargs
) -> tuple[np.ndarray, np.ndarray, Line2D]: ...
def phase_spectrum(
self,
x: ArrayLike,
Fs: float | None = ...,
Fc: int | None = ...,
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ...,
pad_to: int | None = ...,
sides: Literal["default", "onesided", "twosided"] | None = ...,
*,
data=...,
**kwargs
) -> tuple[np.ndarray, np.ndarray, Line2D]: ...
def cohere(
self,
x: ArrayLike,
y: ArrayLike,
NFFT: int = ...,
Fs: float = ...,
Fc: int = ...,
detrend: Literal["none", "mean", "linear"]
| Callable[[ArrayLike], ArrayLike] = ...,
window: Callable[[ArrayLike], ArrayLike] | ArrayLike = ...,
noverlap: int = ...,
pad_to: int | None = ...,
sides: Literal["default", "onesided", "twosided"] = ...,
scale_by_freq: bool | None = ...,
*,
data=...,
**kwargs
) -> tuple[np.ndarray, np.ndarray]: ...
def specgram(
self,
x: ArrayLike,
NFFT: int | None = ...,
Fs: float | None = ...,
Fc: int | None = ...,
detrend: Literal["none", "mean", "linear"]
| Callable[[ArrayLike], ArrayLike]
| None = ...,
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ...,
noverlap: int | None = ...,
cmap: str | Colormap | None = ...,
xextent: tuple[float, float] | None = ...,
pad_to: int | None = ...,
sides: Literal["default", "onesided", "twosided"] | None = ...,
scale_by_freq: bool | None = ...,
mode: Literal["default", "psd", "magnitude", "angle", "phase"] | None = ...,
scale: Literal["default", "linear", "dB"] | None = ...,
vmin: float | None = ...,
vmax: float | None = ...,
*,
data=...,
**kwargs
) -> tuple[np.ndarray, np.ndarray, np.ndarray, AxesImage]: ...
def spy(
self,
Z: ArrayLike,
precision: float | Literal["present"] = ...,
marker: str | None = ...,
markersize: float | None = ...,
aspect: Literal["equal", "auto"] | float | None = ...,
origin: Literal["upper", "lower"] = ...,
**kwargs
) -> AxesImage: ...
def matshow(self, Z: ArrayLike, **kwargs) -> AxesImage: ...
def violinplot(
self,
dataset: ArrayLike | Sequence[ArrayLike],
positions: ArrayLike | None = ...,
vert: bool = ...,
widths: float | ArrayLike = ...,
showmeans: bool = ...,
showextrema: bool = ...,
showmedians: bool = ...,
quantiles: Sequence[float | Sequence[float]] | None = ...,
points: int = ...,
bw_method: Literal["scott", "silverman"]
| float
| Callable[[GaussianKDE], float]
| None = ...,
side: Literal["both", "low", "high"] = ...,
*,
data=...,
) -> dict[str, Collection]: ...
def violin(
self,
vpstats: Sequence[dict[str, Any]],
positions: ArrayLike | None = ...,
vert: bool = ...,
widths: float | ArrayLike = ...,
showmeans: bool = ...,
showextrema: bool = ...,
showmedians: bool = ...,
side: Literal["both", "low", "high"] = ...,
) -> dict[str, Collection]: ...
table = mtable.table
stackplot = mstack.stackplot
streamplot = mstream.streamplot
tricontour = mtri.tricontour
tricontourf = mtri.tricontourf
tripcolor = mtri.tripcolor
triplot = mtri.triplot

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,459 @@
import matplotlib.artist as martist
import datetime
from collections.abc import Callable, Iterable, Iterator, Sequence
from matplotlib import cbook
from matplotlib.artist import Artist
from matplotlib.axes import Axes
from matplotlib.axis import XAxis, YAxis, Tick
from matplotlib.backend_bases import RendererBase, MouseButton, MouseEvent
from matplotlib.cbook import CallbackRegistry
from matplotlib.container import Container
from matplotlib.collections import Collection
from matplotlib.cm import ScalarMappable
from matplotlib.legend import Legend
from matplotlib.lines import Line2D
from matplotlib.gridspec import SubplotSpec, GridSpec
from matplotlib.figure import Figure
from matplotlib.image import AxesImage
from matplotlib.patches import Patch
from matplotlib.scale import ScaleBase
from matplotlib.spines import Spines
from matplotlib.table import Table
from matplotlib.text import Text
from matplotlib.transforms import Transform, Bbox
from cycler import Cycler
import numpy as np
from numpy.typing import ArrayLike
from typing import Any, Literal, TypeVar, overload
from matplotlib.typing import ColorType
_T = TypeVar("_T", bound=Artist)
class _axis_method_wrapper:
attr_name: str
method_name: str
__doc__: str
def __init__(
self, attr_name: str, method_name: str, *, doc_sub: dict[str, str] | None = ...
) -> None: ...
def __set_name__(self, owner: Any, name: str) -> None: ...
class _AxesBase(martist.Artist):
name: str
patch: Patch
spines: Spines
fmt_xdata: Callable[[float], str] | None
fmt_ydata: Callable[[float], str] | None
xaxis: XAxis
yaxis: YAxis
bbox: Bbox
dataLim: Bbox
transAxes: Transform
transScale: Transform
transLimits: Transform
transData: Transform
ignore_existing_data_limits: bool
axison: bool
containers: list[Container]
callbacks: CallbackRegistry
child_axes: list[_AxesBase]
legend_: Legend | None
title: Text
_projection_init: Any
def __init__(
self,
fig: Figure,
*args: tuple[float, float, float, float] | Bbox | int,
facecolor: ColorType | None = ...,
frameon: bool = ...,
sharex: _AxesBase | None = ...,
sharey: _AxesBase | None = ...,
label: Any = ...,
xscale: str | ScaleBase | None = ...,
yscale: str | ScaleBase | None = ...,
box_aspect: float | None = ...,
forward_navigation_events: bool | Literal["auto"] = ...,
**kwargs
) -> None: ...
def get_subplotspec(self) -> SubplotSpec | None: ...
def set_subplotspec(self, subplotspec: SubplotSpec) -> None: ...
def get_gridspec(self) -> GridSpec | None: ...
def set_figure(self, fig: Figure) -> None: ...
@property
def viewLim(self) -> Bbox: ...
def get_xaxis_transform(
self, which: Literal["grid", "tick1", "tick2"] = ...
) -> Transform: ...
def get_xaxis_text1_transform(
self, pad_points: float
) -> tuple[
Transform,
Literal["center", "top", "bottom", "baseline", "center_baseline"],
Literal["center", "left", "right"],
]: ...
def get_xaxis_text2_transform(
self, pad_points
) -> tuple[
Transform,
Literal["center", "top", "bottom", "baseline", "center_baseline"],
Literal["center", "left", "right"],
]: ...
def get_yaxis_transform(
self, which: Literal["grid", "tick1", "tick2"] = ...
) -> Transform: ...
def get_yaxis_text1_transform(
self, pad_points
) -> tuple[
Transform,
Literal["center", "top", "bottom", "baseline", "center_baseline"],
Literal["center", "left", "right"],
]: ...
def get_yaxis_text2_transform(
self, pad_points
) -> tuple[
Transform,
Literal["center", "top", "bottom", "baseline", "center_baseline"],
Literal["center", "left", "right"],
]: ...
def get_position(self, original: bool = ...) -> Bbox: ...
def set_position(
self,
pos: Bbox | tuple[float, float, float, float],
which: Literal["both", "active", "original"] = ...,
) -> None: ...
def reset_position(self) -> None: ...
def set_axes_locator(
self, locator: Callable[[_AxesBase, RendererBase], Bbox]
) -> None: ...
def get_axes_locator(self) -> Callable[[_AxesBase, RendererBase], Bbox]: ...
def sharex(self, other: _AxesBase) -> None: ...
def sharey(self, other: _AxesBase) -> None: ...
def clear(self) -> None: ...
def cla(self) -> None: ...
class ArtistList(Sequence[_T]):
def __init__(
self,
axes: _AxesBase,
prop_name: str,
valid_types: type | Iterable[type] | None = ...,
invalid_types: type | Iterable[type] | None = ...,
) -> None: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[_T]: ...
@overload
def __getitem__(self, key: int) -> _T: ...
@overload
def __getitem__(self, key: slice) -> list[_T]: ...
@overload
def __add__(self, other: _AxesBase.ArtistList[_T]) -> list[_T]: ...
@overload
def __add__(self, other: list[Any]) -> list[Any]: ...
@overload
def __add__(self, other: tuple[Any]) -> tuple[Any]: ...
@overload
def __radd__(self, other: _AxesBase.ArtistList[_T]) -> list[_T]: ...
@overload
def __radd__(self, other: list[Any]) -> list[Any]: ...
@overload
def __radd__(self, other: tuple[Any]) -> tuple[Any]: ...
@property
def artists(self) -> _AxesBase.ArtistList[Artist]: ...
@property
def collections(self) -> _AxesBase.ArtistList[Collection]: ...
@property
def images(self) -> _AxesBase.ArtistList[AxesImage]: ...
@property
def lines(self) -> _AxesBase.ArtistList[Line2D]: ...
@property
def patches(self) -> _AxesBase.ArtistList[Patch]: ...
@property
def tables(self) -> _AxesBase.ArtistList[Table]: ...
@property
def texts(self) -> _AxesBase.ArtistList[Text]: ...
def get_facecolor(self) -> ColorType: ...
def set_facecolor(self, color: ColorType | None) -> None: ...
@overload
def set_prop_cycle(self, cycler: Cycler) -> None: ...
@overload
def set_prop_cycle(self, label: str, values: Iterable[Any]) -> None: ...
@overload
def set_prop_cycle(self, **kwargs: Iterable[Any]) -> None: ...
def get_aspect(self) -> float | Literal["auto"]: ...
def set_aspect(
self,
aspect: float | Literal["auto", "equal"],
adjustable: Literal["box", "datalim"] | None = ...,
anchor: str | tuple[float, float] | None = ...,
share: bool = ...,
) -> None: ...
def get_adjustable(self) -> Literal["box", "datalim"]: ...
def set_adjustable(
self, adjustable: Literal["box", "datalim"], share: bool = ...
) -> None: ...
def get_box_aspect(self) -> float | None: ...
def set_box_aspect(self, aspect: float | None = ...) -> None: ...
def get_anchor(self) -> str | tuple[float, float]: ...
def set_anchor(
self, anchor: str | tuple[float, float], share: bool = ...
) -> None: ...
def get_data_ratio(self) -> float: ...
def apply_aspect(self, position: Bbox | None = ...) -> None: ...
@overload
def axis(
self,
arg: tuple[float, float, float, float] | bool | str | None = ...,
/,
*,
emit: bool = ...
) -> tuple[float, float, float, float]: ...
@overload
def axis(
self,
*,
emit: bool = ...,
xmin: float | None = ...,
xmax: float | None = ...,
ymin: float | None = ...,
ymax: float | None = ...
) -> tuple[float, float, float, float]: ...
def get_legend(self) -> Legend: ...
def get_images(self) -> list[AxesImage]: ...
def get_lines(self) -> list[Line2D]: ...
def get_xaxis(self) -> XAxis: ...
def get_yaxis(self) -> YAxis: ...
def has_data(self) -> bool: ...
def add_artist(self, a: Artist) -> Artist: ...
def add_child_axes(self, ax: _AxesBase) -> _AxesBase: ...
def add_collection(
self, collection: Collection, autolim: bool = ...
) -> Collection: ...
def add_image(self, image: AxesImage) -> AxesImage: ...
def add_line(self, line: Line2D) -> Line2D: ...
def add_patch(self, p: Patch) -> Patch: ...
def add_table(self, tab: Table) -> Table: ...
def add_container(self, container: Container) -> Container: ...
def relim(self, visible_only: bool = ...) -> None: ...
def update_datalim(
self, xys: ArrayLike, updatex: bool = ..., updatey: bool = ...
) -> None: ...
def in_axes(self, mouseevent: MouseEvent) -> bool: ...
def get_autoscale_on(self) -> bool: ...
def set_autoscale_on(self, b: bool) -> None: ...
@property
def use_sticky_edges(self) -> bool: ...
@use_sticky_edges.setter
def use_sticky_edges(self, b: bool) -> None: ...
def get_xmargin(self) -> float: ...
def get_ymargin(self) -> float: ...
def set_xmargin(self, m: float) -> None: ...
def set_ymargin(self, m: float) -> None: ...
# Probably could be made better with overloads
def margins(
self,
*margins: float,
x: float | None = ...,
y: float | None = ...,
tight: bool | None = ...
) -> tuple[float, float] | None: ...
def set_rasterization_zorder(self, z: float | None) -> None: ...
def get_rasterization_zorder(self) -> float | None: ...
def autoscale(
self,
enable: bool = ...,
axis: Literal["both", "x", "y"] = ...,
tight: bool | None = ...,
) -> None: ...
def autoscale_view(
self, tight: bool | None = ..., scalex: bool = ..., scaley: bool = ...
) -> None: ...
def draw_artist(self, a: Artist) -> None: ...
def redraw_in_frame(self) -> None: ...
def get_frame_on(self) -> bool: ...
def set_frame_on(self, b: bool) -> None: ...
def get_axisbelow(self) -> bool | Literal["line"]: ...
def set_axisbelow(self, b: bool | Literal["line"]) -> None: ...
def grid(
self,
visible: bool | None = ...,
which: Literal["major", "minor", "both"] = ...,
axis: Literal["both", "x", "y"] = ...,
**kwargs
) -> None: ...
def ticklabel_format(
self,
*,
axis: Literal["both", "x", "y"] = ...,
style: Literal["", "sci", "scientific", "plain"] | None = ...,
scilimits: tuple[int, int] | None = ...,
useOffset: bool | float | None = ...,
useLocale: bool | None = ...,
useMathText: bool | None = ...
) -> None: ...
def locator_params(
self, axis: Literal["both", "x", "y"] = ..., tight: bool | None = ..., **kwargs
) -> None: ...
def tick_params(self, axis: Literal["both", "x", "y"] = ..., **kwargs) -> None: ...
def set_axis_off(self) -> None: ...
def set_axis_on(self) -> None: ...
def get_xlabel(self) -> str: ...
def set_xlabel(
self,
xlabel: str,
fontdict: dict[str, Any] | None = ...,
labelpad: float | None = ...,
*,
loc: Literal["left", "center", "right"] | None = ...,
**kwargs
) -> Text: ...
def invert_xaxis(self) -> None: ...
def get_xbound(self) -> tuple[float, float]: ...
def set_xbound(
self, lower: float | None = ..., upper: float | None = ...
) -> None: ...
def get_xlim(self) -> tuple[float, float]: ...
def set_xlim(
self,
left: float | tuple[float, float] | None = ...,
right: float | None = ...,
*,
emit: bool = ...,
auto: bool | None = ...,
xmin: float | None = ...,
xmax: float | None = ...
) -> tuple[float, float]: ...
def get_ylabel(self) -> str: ...
def set_ylabel(
self,
ylabel: str,
fontdict: dict[str, Any] | None = ...,
labelpad: float | None = ...,
*,
loc: Literal["bottom", "center", "top"] | None = ...,
**kwargs
) -> Text: ...
def invert_yaxis(self) -> None: ...
def get_ybound(self) -> tuple[float, float]: ...
def set_ybound(
self, lower: float | None = ..., upper: float | None = ...
) -> None: ...
def get_ylim(self) -> tuple[float, float]: ...
def set_ylim(
self,
bottom: float | tuple[float, float] | None = ...,
top: float | None = ...,
*,
emit: bool = ...,
auto: bool | None = ...,
ymin: float | None = ...,
ymax: float | None = ...
) -> tuple[float, float]: ...
def format_xdata(self, x: float) -> str: ...
def format_ydata(self, y: float) -> str: ...
def format_coord(self, x: float, y: float) -> str: ...
def minorticks_on(self) -> None: ...
def minorticks_off(self) -> None: ...
def can_zoom(self) -> bool: ...
def can_pan(self) -> bool: ...
def get_navigate(self) -> bool: ...
def set_navigate(self, b: bool) -> None: ...
def get_forward_navigation_events(self) -> bool | Literal["auto"]: ...
def set_forward_navigation_events(self, forward: bool | Literal["auto"]) -> None: ...
def get_navigate_mode(self) -> Literal["PAN", "ZOOM"] | None: ...
def set_navigate_mode(self, b: Literal["PAN", "ZOOM"] | None) -> None: ...
def start_pan(self, x: float, y: float, button: MouseButton) -> None: ...
def end_pan(self) -> None: ...
def drag_pan(
self, button: MouseButton, key: str | None, x: float, y: float
) -> None: ...
def get_children(self) -> list[Artist]: ...
def contains_point(self, point: tuple[int, int]) -> bool: ...
def get_default_bbox_extra_artists(self) -> list[Artist]: ...
def get_tightbbox(
self,
renderer: RendererBase | None = ...,
*,
call_axes_locator: bool = ...,
bbox_extra_artists: Sequence[Artist] | None = ...,
for_layout_only: bool = ...
) -> Bbox | None: ...
def twinx(self) -> Axes: ...
def twiny(self) -> Axes: ...
def get_shared_x_axes(self) -> cbook.GrouperView: ...
def get_shared_y_axes(self) -> cbook.GrouperView: ...
def label_outer(self, remove_inner_ticks: bool = ...) -> None: ...
# The methods underneath this line are added via the `_axis_method_wrapper` class
# Initially they are set to an object, but that object uses `__set_name__` to override
# itself with a method modified from the Axis methods for the x or y Axis.
# As such, they are typed according to the resultant method rather than as that object.
def get_xgridlines(self) -> list[Line2D]: ...
def get_xticklines(self, minor: bool = ...) -> list[Line2D]: ...
def get_ygridlines(self) -> list[Line2D]: ...
def get_yticklines(self, minor: bool = ...) -> list[Line2D]: ...
def _sci(self, im: ScalarMappable) -> None: ...
def get_autoscalex_on(self) -> bool: ...
def get_autoscaley_on(self) -> bool: ...
def set_autoscalex_on(self, b: bool) -> None: ...
def set_autoscaley_on(self, b: bool) -> None: ...
def xaxis_inverted(self) -> bool: ...
def get_xscale(self) -> str: ...
def set_xscale(self, value: str | ScaleBase, **kwargs) -> None: ...
def get_xticks(self, *, minor: bool = ...) -> np.ndarray: ...
def set_xticks(
self,
ticks: ArrayLike,
labels: Iterable[str] | None = ...,
*,
minor: bool = ...,
**kwargs
) -> list[Tick]: ...
def get_xmajorticklabels(self) -> list[Text]: ...
def get_xminorticklabels(self) -> list[Text]: ...
def get_xticklabels(
self, minor: bool = ..., which: Literal["major", "minor", "both"] | None = ...
) -> list[Text]: ...
def set_xticklabels(
self,
labels: Iterable[str | Text],
*,
minor: bool = ...,
fontdict: dict[str, Any] | None = ...,
**kwargs
) -> list[Text]: ...
def yaxis_inverted(self) -> bool: ...
def get_yscale(self) -> str: ...
def set_yscale(self, value: str | ScaleBase, **kwargs) -> None: ...
def get_yticks(self, *, minor: bool = ...) -> np.ndarray: ...
def set_yticks(
self,
ticks: ArrayLike,
labels: Iterable[str] | None = ...,
*,
minor: bool = ...,
**kwargs
) -> list[Tick]: ...
def get_ymajorticklabels(self) -> list[Text]: ...
def get_yminorticklabels(self) -> list[Text]: ...
def get_yticklabels(
self, minor: bool = ..., which: Literal["major", "minor", "both"] | None = ...
) -> list[Text]: ...
def set_yticklabels(
self,
labels: Iterable[str | Text],
*,
minor: bool = ...,
fontdict: dict[str, Any] | None = ...,
**kwargs
) -> list[Text]: ...
def xaxis_date(self, tz: str | datetime.tzinfo | None = ...) -> None: ...
def yaxis_date(self, tz: str | datetime.tzinfo | None = ...) -> None: ...

View File

@ -0,0 +1,321 @@
import numbers
import numpy as np
from matplotlib import _api, _docstring, transforms
import matplotlib.ticker as mticker
from matplotlib.axes._base import _AxesBase, _TransformedBoundsLocator
from matplotlib.axis import Axis
from matplotlib.transforms import Transform
class SecondaryAxis(_AxesBase):
"""
General class to hold a Secondary_X/Yaxis.
"""
def __init__(self, parent, orientation, location, functions, transform=None,
**kwargs):
"""
See `.secondary_xaxis` and `.secondary_yaxis` for the doc string.
While there is no need for this to be private, it should really be
called by those higher level functions.
"""
_api.check_in_list(["x", "y"], orientation=orientation)
self._functions = functions
self._parent = parent
self._orientation = orientation
self._ticks_set = False
if self._orientation == 'x':
super().__init__(self._parent.figure, [0, 1., 1, 0.0001], **kwargs)
self._axis = self.xaxis
self._locstrings = ['top', 'bottom']
self._otherstrings = ['left', 'right']
else: # 'y'
super().__init__(self._parent.figure, [0, 1., 0.0001, 1], **kwargs)
self._axis = self.yaxis
self._locstrings = ['right', 'left']
self._otherstrings = ['top', 'bottom']
self._parentscale = None
# this gets positioned w/o constrained_layout so exclude:
self.set_location(location, transform)
self.set_functions(functions)
# styling:
otheraxis = self.yaxis if self._orientation == 'x' else self.xaxis
otheraxis.set_major_locator(mticker.NullLocator())
otheraxis.set_ticks_position('none')
self.spines[self._otherstrings].set_visible(False)
self.spines[self._locstrings].set_visible(True)
if self._pos < 0.5:
# flip the location strings...
self._locstrings = self._locstrings[::-1]
self.set_alignment(self._locstrings[0])
def set_alignment(self, align):
"""
Set if axes spine and labels are drawn at top or bottom (or left/right)
of the Axes.
Parameters
----------
align : {'top', 'bottom', 'left', 'right'}
Either 'top' or 'bottom' for orientation='x' or
'left' or 'right' for orientation='y' axis.
"""
_api.check_in_list(self._locstrings, align=align)
if align == self._locstrings[1]: # Need to change the orientation.
self._locstrings = self._locstrings[::-1]
self.spines[self._locstrings[0]].set_visible(True)
self.spines[self._locstrings[1]].set_visible(False)
self._axis.set_ticks_position(align)
self._axis.set_label_position(align)
def set_location(self, location, transform=None):
"""
Set the vertical or horizontal location of the axes in
parent-normalized coordinates.
Parameters
----------
location : {'top', 'bottom', 'left', 'right'} or float
The position to put the secondary axis. Strings can be 'top' or
'bottom' for orientation='x' and 'right' or 'left' for
orientation='y'. A float indicates the relative position on the
parent Axes to put the new Axes, 0.0 being the bottom (or left)
and 1.0 being the top (or right).
transform : `.Transform`, optional
Transform for the location to use. Defaults to
the parent's ``transAxes``, so locations are normally relative to
the parent axes.
.. versionadded:: 3.9
"""
_api.check_isinstance((transforms.Transform, None), transform=transform)
# This puts the rectangle into figure-relative coordinates.
if isinstance(location, str):
_api.check_in_list(self._locstrings, location=location)
self._pos = 1. if location in ('top', 'right') else 0.
elif isinstance(location, numbers.Real):
self._pos = location
else:
raise ValueError(
f"location must be {self._locstrings[0]!r}, "
f"{self._locstrings[1]!r}, or a float, not {location!r}")
self._loc = location
if self._orientation == 'x':
# An x-secondary axes is like an inset axes from x = 0 to x = 1 and
# from y = pos to y = pos + eps, in the parent's transAxes coords.
bounds = [0, self._pos, 1., 1e-10]
# If a transformation is provided, use its y component rather than
# the parent's transAxes. This can be used to place axes in the data
# coords, for instance.
if transform is not None:
transform = transforms.blended_transform_factory(
self._parent.transAxes, transform)
else: # 'y'
bounds = [self._pos, 0, 1e-10, 1]
if transform is not None:
transform = transforms.blended_transform_factory(
transform, self._parent.transAxes) # Use provided x axis
# If no transform is provided, use the parent's transAxes
if transform is None:
transform = self._parent.transAxes
# this locator lets the axes move in the parent axes coordinates.
# so it never needs to know where the parent is explicitly in
# figure coordinates.
# it gets called in ax.apply_aspect() (of all places)
self.set_axes_locator(_TransformedBoundsLocator(bounds, transform))
def apply_aspect(self, position=None):
# docstring inherited.
self._set_lims()
super().apply_aspect(position)
@_docstring.copy(Axis.set_ticks)
def set_ticks(self, ticks, labels=None, *, minor=False, **kwargs):
ret = self._axis.set_ticks(ticks, labels, minor=minor, **kwargs)
self.stale = True
self._ticks_set = True
return ret
def set_functions(self, functions):
"""
Set how the secondary axis converts limits from the parent Axes.
Parameters
----------
functions : 2-tuple of func, or `Transform` with an inverse.
Transform between the parent axis values and the secondary axis
values.
If supplied as a 2-tuple of functions, the first function is
the forward transform function and the second is the inverse
transform.
If a transform is supplied, then the transform must have an
inverse.
"""
if (isinstance(functions, tuple) and len(functions) == 2 and
callable(functions[0]) and callable(functions[1])):
# make an arbitrary convert from a two-tuple of functions
# forward and inverse.
self._functions = functions
elif isinstance(functions, Transform):
self._functions = (
functions.transform,
lambda x: functions.inverted().transform(x)
)
elif functions is None:
self._functions = (lambda x: x, lambda x: x)
else:
raise ValueError('functions argument of secondary Axes '
'must be a two-tuple of callable functions '
'with the first function being the transform '
'and the second being the inverse')
self._set_scale()
def draw(self, renderer):
"""
Draw the secondary Axes.
Consults the parent Axes for its limits and converts them
using the converter specified by
`~.axes._secondary_axes.set_functions` (or *functions*
parameter when Axes initialized.)
"""
self._set_lims()
# this sets the scale in case the parent has set its scale.
self._set_scale()
super().draw(renderer)
def _set_scale(self):
"""
Check if parent has set its scale
"""
if self._orientation == 'x':
pscale = self._parent.xaxis.get_scale()
set_scale = self.set_xscale
else: # 'y'
pscale = self._parent.yaxis.get_scale()
set_scale = self.set_yscale
if pscale == self._parentscale:
return
if self._ticks_set:
ticks = self._axis.get_ticklocs()
# need to invert the roles here for the ticks to line up.
set_scale('functionlog' if pscale == 'log' else 'function',
functions=self._functions[::-1])
# OK, set_scale sets the locators, but if we've called
# axsecond.set_ticks, we want to keep those.
if self._ticks_set:
self._axis.set_major_locator(mticker.FixedLocator(ticks))
# If the parent scale doesn't change, we can skip this next time.
self._parentscale = pscale
def _set_lims(self):
"""
Set the limits based on parent limits and the convert method
between the parent and this secondary Axes.
"""
if self._orientation == 'x':
lims = self._parent.get_xlim()
set_lim = self.set_xlim
else: # 'y'
lims = self._parent.get_ylim()
set_lim = self.set_ylim
order = lims[0] < lims[1]
lims = self._functions[0](np.array(lims))
neworder = lims[0] < lims[1]
if neworder != order:
# Flip because the transform will take care of the flipping.
lims = lims[::-1]
set_lim(lims)
def set_aspect(self, *args, **kwargs):
"""
Secondary Axes cannot set the aspect ratio, so calling this just
sets a warning.
"""
_api.warn_external("Secondary Axes can't set the aspect ratio")
def set_color(self, color):
"""
Change the color of the secondary Axes and all decorators.
Parameters
----------
color : :mpltype:`color`
"""
axis = self._axis_map[self._orientation]
axis.set_tick_params(colors=color)
for spine in self.spines.values():
if spine.axis is axis:
spine.set_color(color)
axis.label.set_color(color)
_secax_docstring = '''
Warnings
--------
This method is experimental as of 3.1, and the API may change.
Parameters
----------
location : {'top', 'bottom', 'left', 'right'} or float
The position to put the secondary axis. Strings can be 'top' or
'bottom' for orientation='x' and 'right' or 'left' for
orientation='y'. A float indicates the relative position on the
parent Axes to put the new Axes, 0.0 being the bottom (or left)
and 1.0 being the top (or right).
functions : 2-tuple of func, or Transform with an inverse
If a 2-tuple of functions, the user specifies the transform
function and its inverse. i.e.
``functions=(lambda x: 2 / x, lambda x: 2 / x)`` would be an
reciprocal transform with a factor of 2. Both functions must accept
numpy arrays as input.
The user can also directly supply a subclass of
`.transforms.Transform` so long as it has an inverse.
See :doc:`/gallery/subplots_axes_and_figures/secondary_axis`
for examples of making these conversions.
transform : `.Transform`, optional
If specified, *location* will be
placed relative to this transform (in the direction of the axis)
rather than the parent's axis. i.e. a secondary x-axis will
use the provided y transform and the x transform of the parent.
.. versionadded:: 3.9
Returns
-------
ax : axes._secondary_axes.SecondaryAxis
Other Parameters
----------------
**kwargs : `~matplotlib.axes.Axes` properties.
Other miscellaneous Axes parameters.
'''
_docstring.interpd.update(_secax_docstring=_secax_docstring)

View File

@ -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

View File

@ -0,0 +1,280 @@
from collections.abc import Callable, Iterable, Sequence
import datetime
from typing import Any, Literal, overload
import numpy as np
from numpy.typing import ArrayLike
import matplotlib.artist as martist
from matplotlib import cbook
from matplotlib.axes import Axes
from matplotlib.backend_bases import RendererBase
from matplotlib.lines import Line2D
from matplotlib.text import Text
from matplotlib.ticker import Locator, Formatter
from matplotlib.transforms import Transform, Bbox
from matplotlib.typing import ColorType
GRIDLINE_INTERPOLATION_STEPS: int
class Tick(martist.Artist):
axes: Axes
tick1line: Line2D
tick2line: Line2D
gridline: Line2D
label1: Text
label2: Text
def __init__(
self,
axes: Axes,
loc: float,
*,
size: float | None = ...,
width: float | None = ...,
color: ColorType | None = ...,
tickdir: Literal["in", "inout", "out"] | None = ...,
pad: float | None = ...,
labelsize: float | None = ...,
labelcolor: ColorType | None = ...,
labelfontfamily: str | Sequence[str] | None = ...,
zorder: float | None = ...,
gridOn: bool | None = ...,
tick1On: bool = ...,
tick2On: bool = ...,
label1On: bool = ...,
label2On: bool = ...,
major: bool = ...,
labelrotation: float = ...,
grid_color: ColorType | None = ...,
grid_linestyle: str | None = ...,
grid_linewidth: float | None = ...,
grid_alpha: float | None = ...,
**kwargs
) -> None: ...
def get_tickdir(self) -> Literal["in", "inout", "out"]: ...
def get_tick_padding(self) -> float: ...
def get_children(self) -> list[martist.Artist]: ...
stale: bool
def set_pad(self, val: float) -> None: ...
def get_pad(self) -> None: ...
def get_loc(self) -> float: ...
def set_label1(self, s: object) -> None: ...
def set_label(self, s: object) -> None: ...
def set_label2(self, s: object) -> None: ...
def set_url(self, url: str | None) -> None: ...
def get_view_interval(self) -> ArrayLike: ...
def update_position(self, loc: float) -> None: ...
class XTick(Tick):
__name__: str
def __init__(self, *args, **kwargs) -> None: ...
stale: bool
def update_position(self, loc: float) -> None: ...
def get_view_interval(self) -> np.ndarray: ...
class YTick(Tick):
__name__: str
def __init__(self, *args, **kwargs) -> None: ...
stale: bool
def update_position(self, loc: float) -> None: ...
def get_view_interval(self) -> np.ndarray: ...
class Ticker:
def __init__(self) -> None: ...
@property
def locator(self) -> Locator | None: ...
@locator.setter
def locator(self, locator: Locator) -> None: ...
@property
def formatter(self) -> Formatter | None: ...
@formatter.setter
def formatter(self, formatter: Formatter) -> None: ...
class _LazyTickList:
def __init__(self, major: bool) -> None: ...
# Replace return with Self when py3.9 is dropped
@overload
def __get__(self, instance: None, owner: None) -> _LazyTickList: ...
@overload
def __get__(self, instance: Axis, owner: type[Axis]) -> list[Tick]: ...
class Axis(martist.Artist):
OFFSETTEXTPAD: int
isDefault_label: bool
axes: Axes
major: Ticker
minor: Ticker
callbacks: cbook.CallbackRegistry
label: Text
offsetText: Text
labelpad: float
pickradius: float
def __init__(self, axes, *, pickradius: float = ...,
clear: bool = ...) -> None: ...
@property
def isDefault_majloc(self) -> bool: ...
@isDefault_majloc.setter
def isDefault_majloc(self, value: bool) -> None: ...
@property
def isDefault_majfmt(self) -> bool: ...
@isDefault_majfmt.setter
def isDefault_majfmt(self, value: bool) -> None: ...
@property
def isDefault_minloc(self) -> bool: ...
@isDefault_minloc.setter
def isDefault_minloc(self, value: bool) -> None: ...
@property
def isDefault_minfmt(self) -> bool: ...
@isDefault_minfmt.setter
def isDefault_minfmt(self, value: bool) -> None: ...
majorTicks: _LazyTickList
minorTicks: _LazyTickList
def get_remove_overlapping_locs(self) -> bool: ...
def set_remove_overlapping_locs(self, val: bool) -> None: ...
@property
def remove_overlapping_locs(self) -> bool: ...
@remove_overlapping_locs.setter
def remove_overlapping_locs(self, val: bool) -> None: ...
stale: bool
def set_label_coords(
self, x: float, y: float, transform: Transform | None = ...
) -> None: ...
def get_transform(self) -> Transform: ...
def get_scale(self) -> str: ...
def limit_range_for_scale(
self, vmin: float, vmax: float
) -> tuple[float, float]: ...
def get_children(self) -> list[martist.Artist]: ...
# TODO units
converter: Any
units: Any
def clear(self) -> None: ...
def reset_ticks(self) -> None: ...
def minorticks_on(self) -> None: ...
def minorticks_off(self) -> None: ...
def set_tick_params(
self,
which: Literal["major", "minor", "both"] = ...,
reset: bool = ...,
**kwargs
) -> None: ...
def get_tick_params(
self, which: Literal["major", "minor"] = ...
) -> dict[str, Any]: ...
def get_view_interval(self) -> tuple[float, float]: ...
def set_view_interval(
self, vmin: float, vmax: float, ignore: bool = ...
) -> None: ...
def get_data_interval(self) -> tuple[float, float]: ...
def set_data_interval(
self, vmin: float, vmax: float, ignore: bool = ...
) -> None: ...
def get_inverted(self) -> bool: ...
def set_inverted(self, inverted: bool) -> None: ...
def set_default_intervals(self) -> None: ...
def get_tightbbox(
self, renderer: RendererBase | None = ..., *, for_layout_only: bool = ...
) -> Bbox | None: ...
def get_tick_padding(self) -> float: ...
def get_gridlines(self) -> list[Line2D]: ...
def get_label(self) -> Text: ...
def get_offset_text(self) -> Text: ...
def get_pickradius(self) -> float: ...
def get_majorticklabels(self) -> list[Text]: ...
def get_minorticklabels(self) -> list[Text]: ...
def get_ticklabels(
self, minor: bool = ..., which: Literal["major", "minor", "both"] | None = ...
) -> list[Text]: ...
def get_majorticklines(self) -> list[Line2D]: ...
def get_minorticklines(self) -> list[Line2D]: ...
def get_ticklines(self, minor: bool = ...) -> list[Line2D]: ...
def get_majorticklocs(self) -> np.ndarray: ...
def get_minorticklocs(self) -> np.ndarray: ...
def get_ticklocs(self, *, minor: bool = ...) -> np.ndarray: ...
def get_ticks_direction(self, minor: bool = ...) -> np.ndarray: ...
def get_label_text(self) -> str: ...
def get_major_locator(self) -> Locator: ...
def get_minor_locator(self) -> Locator: ...
def get_major_formatter(self) -> Formatter: ...
def get_minor_formatter(self) -> Formatter: ...
def get_major_ticks(self, numticks: int | None = ...) -> list[Tick]: ...
def get_minor_ticks(self, numticks: int | None = ...) -> list[Tick]: ...
def grid(
self,
visible: bool | None = ...,
which: Literal["major", "minor", "both"] = ...,
**kwargs
) -> None: ...
# TODO units
def update_units(self, data): ...
def have_units(self) -> bool: ...
def convert_units(self, x): ...
def set_units(self, u) -> None: ...
def get_units(self): ...
def set_label_text(
self, label: str, fontdict: dict[str, Any] | None = ..., **kwargs
) -> Text: ...
def set_major_formatter(
self, formatter: Formatter | str | Callable[[float, float], str]
) -> None: ...
def set_minor_formatter(
self, formatter: Formatter | str | Callable[[float, float], str]
) -> None: ...
def set_major_locator(self, locator: Locator) -> None: ...
def set_minor_locator(self, locator: Locator) -> None: ...
def set_pickradius(self, pickradius: float) -> None: ...
def set_ticklabels(
self,
labels: Iterable[str | Text],
*,
minor: bool = ...,
fontdict: dict[str, Any] | None = ...,
**kwargs
) -> list[Text]: ...
def set_ticks(
self,
ticks: ArrayLike,
labels: Iterable[str] | None = ...,
*,
minor: bool = ...,
**kwargs
) -> list[Tick]: ...
def axis_date(self, tz: str | datetime.tzinfo | None = ...) -> None: ...
def get_tick_space(self) -> int: ...
def get_label_position(self) -> Literal["top", "bottom"]: ...
def set_label_position(
self, position: Literal["top", "bottom", "left", "right"]
) -> None: ...
def get_minpos(self) -> float: ...
class XAxis(Axis):
__name__: str
axis_name: str
def __init__(self, *args, **kwargs) -> None: ...
label_position: Literal["bottom", "top"]
stale: bool
def set_label_position(self, position: Literal["bottom", "top"]) -> None: ... # type: ignore[override]
def set_ticks_position(
self, position: Literal["top", "bottom", "both", "default", "none"]
) -> None: ...
def tick_top(self) -> None: ...
def tick_bottom(self) -> None: ...
def get_ticks_position(self) -> Literal["top", "bottom", "default", "unknown"]: ...
def get_tick_space(self) -> int: ...
class YAxis(Axis):
__name__: str
axis_name: str
def __init__(self, *args, **kwargs) -> None: ...
label_position: Literal["left", "right"]
stale: bool
def set_label_position(self, position: Literal["left", "right"]) -> None: ... # type: ignore[override]
def set_offset_position(self, position: Literal["left", "right"]) -> None: ...
def set_ticks_position(
self, position: Literal["left", "right", "both", "default", "none"]
) -> None: ...
def tick_right(self) -> None: ...
def tick_left(self) -> None: ...
def get_ticks_position(self) -> Literal["left", "right", "default", "unknown"]: ...
def get_tick_space(self) -> int: ...

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,483 @@
from enum import Enum, IntEnum
import os
from matplotlib import (
cbook,
transforms,
widgets,
_api,
)
from matplotlib.artist import Artist
from matplotlib.axes import Axes
from matplotlib.backend_managers import ToolManager
from matplotlib.backend_tools import Cursors, ToolBase
from matplotlib.colorbar import Colorbar
from matplotlib.figure import Figure
from matplotlib.font_manager import FontProperties
from matplotlib.path import Path
from matplotlib.texmanager import TexManager
from matplotlib.text import Text
from matplotlib.transforms import Bbox, BboxBase, Transform, TransformedPath
from collections.abc import Callable, Iterable, Sequence
from typing import Any, IO, Literal, NamedTuple, TypeVar
from numpy.typing import ArrayLike
from .typing import ColorType, LineStyleType, CapStyleType, JoinStyleType
def register_backend(
format: str, backend: str | type[FigureCanvasBase], description: str | None = ...
) -> None: ...
def get_registered_canvas_class(format: str) -> type[FigureCanvasBase]: ...
class RendererBase:
def __init__(self) -> None: ...
def open_group(self, s: str, gid: str | None = ...) -> None: ...
def close_group(self, s: str) -> None: ...
def draw_path(
self,
gc: GraphicsContextBase,
path: Path,
transform: Transform,
rgbFace: ColorType | None = ...,
) -> None: ...
def draw_markers(
self,
gc: GraphicsContextBase,
marker_path: Path,
marker_trans: Transform,
path: Path,
trans: Transform,
rgbFace: ColorType | None = ...,
) -> None: ...
def draw_path_collection(
self,
gc: GraphicsContextBase,
master_transform: Transform,
paths: Sequence[Path],
all_transforms: Sequence[ArrayLike],
offsets: ArrayLike | Sequence[ArrayLike],
offset_trans: Transform,
facecolors: ColorType | Sequence[ColorType],
edgecolors: ColorType | Sequence[ColorType],
linewidths: float | Sequence[float],
linestyles: LineStyleType | Sequence[LineStyleType],
antialiaseds: bool | Sequence[bool],
urls: str | Sequence[str],
offset_position: Any,
) -> None: ...
def draw_quad_mesh(
self,
gc: GraphicsContextBase,
master_transform: Transform,
meshWidth,
meshHeight,
coordinates: ArrayLike,
offsets: ArrayLike | Sequence[ArrayLike],
offsetTrans: Transform,
facecolors: Sequence[ColorType],
antialiased: bool,
edgecolors: Sequence[ColorType] | ColorType | None,
) -> None: ...
def draw_gouraud_triangles(
self,
gc: GraphicsContextBase,
triangles_array: ArrayLike,
colors_array: ArrayLike,
transform: Transform,
) -> None: ...
def get_image_magnification(self) -> float: ...
def draw_image(
self,
gc: GraphicsContextBase,
x: float,
y: float,
im: ArrayLike,
transform: transforms.Affine2DBase | None = ...,
) -> None: ...
def option_image_nocomposite(self) -> bool: ...
def option_scale_image(self) -> bool: ...
def draw_tex(
self,
gc: GraphicsContextBase,
x: float,
y: float,
s: str,
prop: FontProperties,
angle: float,
*,
mtext: Text | None = ...
) -> None: ...
def draw_text(
self,
gc: GraphicsContextBase,
x: float,
y: float,
s: str,
prop: FontProperties,
angle: float,
ismath: bool | Literal["TeX"] = ...,
mtext: Text | None = ...,
) -> None: ...
def get_text_width_height_descent(
self, s: str, prop: FontProperties, ismath: bool | Literal["TeX"]
) -> tuple[float, float, float]: ...
def flipy(self) -> bool: ...
def get_canvas_width_height(self) -> tuple[float, float]: ...
def get_texmanager(self) -> TexManager: ...
def new_gc(self) -> GraphicsContextBase: ...
def points_to_pixels(self, points: ArrayLike) -> ArrayLike: ...
def start_rasterizing(self) -> None: ...
def stop_rasterizing(self) -> None: ...
def start_filter(self) -> None: ...
def stop_filter(self, filter_func) -> None: ...
class GraphicsContextBase:
def __init__(self) -> None: ...
def copy_properties(self, gc: GraphicsContextBase) -> None: ...
def restore(self) -> None: ...
def get_alpha(self) -> float: ...
def get_antialiased(self) -> int: ...
def get_capstyle(self) -> Literal["butt", "projecting", "round"]: ...
def get_clip_rectangle(self) -> Bbox | None: ...
def get_clip_path(
self,
) -> tuple[TransformedPath, Transform] | tuple[None, None]: ...
def get_dashes(self) -> tuple[float, ArrayLike | None]: ...
def get_forced_alpha(self) -> bool: ...
def get_joinstyle(self) -> Literal["miter", "round", "bevel"]: ...
def get_linewidth(self) -> float: ...
def get_rgb(self) -> tuple[float, float, float, float]: ...
def get_url(self) -> str | None: ...
def get_gid(self) -> int | None: ...
def get_snap(self) -> bool | None: ...
def set_alpha(self, alpha: float) -> None: ...
def set_antialiased(self, b: bool) -> None: ...
def set_capstyle(self, cs: CapStyleType) -> None: ...
def set_clip_rectangle(self, rectangle: Bbox | None) -> None: ...
def set_clip_path(self, path: TransformedPath | None) -> None: ...
def set_dashes(self, dash_offset: float, dash_list: ArrayLike | None) -> None: ...
def set_foreground(self, fg: ColorType, isRGBA: bool = ...) -> None: ...
def set_joinstyle(self, js: JoinStyleType) -> None: ...
def set_linewidth(self, w: float) -> None: ...
def set_url(self, url: str | None) -> None: ...
def set_gid(self, id: int | None) -> None: ...
def set_snap(self, snap: bool | None) -> None: ...
def set_hatch(self, hatch: str | None) -> None: ...
def get_hatch(self) -> str | None: ...
def get_hatch_path(self, density: float = ...) -> Path: ...
def get_hatch_color(self) -> ColorType: ...
def set_hatch_color(self, hatch_color: ColorType) -> None: ...
def get_hatch_linewidth(self) -> float: ...
def get_sketch_params(self) -> tuple[float, float, float] | None: ...
def set_sketch_params(
self,
scale: float | None = ...,
length: float | None = ...,
randomness: float | None = ...,
) -> None: ...
class TimerBase:
callbacks: list[tuple[Callable, tuple, dict[str, Any]]]
def __init__(
self,
interval: int | None = ...,
callbacks: list[tuple[Callable, tuple, dict[str, Any]]] | None = ...,
) -> None: ...
def __del__(self) -> None: ...
def start(self, interval: int | None = ...) -> None: ...
def stop(self) -> None: ...
@property
def interval(self) -> int: ...
@interval.setter
def interval(self, interval: int) -> None: ...
@property
def single_shot(self) -> bool: ...
@single_shot.setter
def single_shot(self, ss: bool) -> None: ...
def add_callback(self, func: Callable, *args, **kwargs) -> Callable: ...
def remove_callback(self, func: Callable, *args, **kwargs) -> None: ...
class Event:
name: str
canvas: FigureCanvasBase
def __init__(
self, name: str, canvas: FigureCanvasBase, guiEvent: Any | None = ...
) -> None: ...
@property
def guiEvent(self) -> Any: ...
class DrawEvent(Event):
renderer: RendererBase
def __init__(
self, name: str, canvas: FigureCanvasBase, renderer: RendererBase
) -> None: ...
class ResizeEvent(Event):
width: int
height: int
def __init__(self, name: str, canvas: FigureCanvasBase) -> None: ...
class CloseEvent(Event): ...
class LocationEvent(Event):
lastevent: Event | None
x: int
y: int
inaxes: Axes | None
xdata: float | None
ydata: float | None
def __init__(
self,
name: str,
canvas: FigureCanvasBase,
x: int,
y: int,
guiEvent: Any | None = ...,
*,
modifiers: Iterable[str] | None = ...,
) -> None: ...
class MouseButton(IntEnum):
LEFT: int
MIDDLE: int
RIGHT: int
BACK: int
FORWARD: int
class MouseEvent(LocationEvent):
button: MouseButton | Literal["up", "down"] | None
key: str | None
step: float
dblclick: bool
def __init__(
self,
name: str,
canvas: FigureCanvasBase,
x: int,
y: int,
button: MouseButton | Literal["up", "down"] | None = ...,
key: str | None = ...,
step: float = ...,
dblclick: bool = ...,
guiEvent: Any | None = ...,
*,
modifiers: Iterable[str] | None = ...,
) -> None: ...
class PickEvent(Event):
mouseevent: MouseEvent
artist: Artist
def __init__(
self,
name: str,
canvas: FigureCanvasBase,
mouseevent: MouseEvent,
artist: Artist,
guiEvent: Any | None = ...,
**kwargs
) -> None: ...
class KeyEvent(LocationEvent):
key: str | None
def __init__(
self,
name: str,
canvas: FigureCanvasBase,
key: str | None,
x: int = ...,
y: int = ...,
guiEvent: Any | None = ...,
) -> None: ...
class FigureCanvasBase:
required_interactive_framework: str | None
@_api.classproperty
def manager_class(cls) -> type[FigureManagerBase]: ...
events: list[str]
fixed_dpi: None | float
filetypes: dict[str, str]
@_api.classproperty
def supports_blit(cls) -> bool: ...
figure: Figure
manager: None | FigureManagerBase
widgetlock: widgets.LockDraw
mouse_grabber: None | Axes
toolbar: None | NavigationToolbar2
def __init__(self, figure: Figure | None = ...) -> None: ...
@property
def callbacks(self) -> cbook.CallbackRegistry: ...
@property
def button_pick_id(self) -> int: ...
@property
def scroll_pick_id(self) -> int: ...
@classmethod
def new_manager(cls, figure: Figure, num: int | str) -> FigureManagerBase: ...
def is_saving(self) -> bool: ...
def blit(self, bbox: BboxBase | None = ...) -> None: ...
def inaxes(self, xy: tuple[float, float]) -> Axes | None: ...
def grab_mouse(self, ax: Axes) -> None: ...
def release_mouse(self, ax: Axes) -> None: ...
def set_cursor(self, cursor: Cursors) -> None: ...
def draw(self, *args, **kwargs) -> None: ...
def draw_idle(self, *args, **kwargs) -> None: ...
@property
def device_pixel_ratio(self) -> float: ...
def get_width_height(self, *, physical: bool = ...) -> tuple[int, int]: ...
@classmethod
def get_supported_filetypes(cls) -> dict[str, str]: ...
@classmethod
def get_supported_filetypes_grouped(cls) -> dict[str, list[str]]: ...
def print_figure(
self,
filename: str | os.PathLike | IO,
dpi: float | None = ...,
facecolor: ColorType | Literal["auto"] | None = ...,
edgecolor: ColorType | Literal["auto"] | None = ...,
orientation: str = ...,
format: str | None = ...,
*,
bbox_inches: Literal["tight"] | Bbox | None = ...,
pad_inches: float | None = ...,
bbox_extra_artists: list[Artist] | None = ...,
backend: str | None = ...,
**kwargs
) -> Any: ...
@classmethod
def get_default_filetype(cls) -> str: ...
def get_default_filename(self) -> str: ...
_T = TypeVar("_T", bound=FigureCanvasBase)
def switch_backends(self, FigureCanvasClass: type[_T]) -> _T: ...
def mpl_connect(self, s: str, func: Callable[[Event], Any]) -> int: ...
def mpl_disconnect(self, cid: int) -> None: ...
def new_timer(
self,
interval: int | None = ...,
callbacks: list[tuple[Callable, tuple, dict[str, Any]]] | None = ...,
) -> TimerBase: ...
def flush_events(self) -> None: ...
def start_event_loop(self, timeout: float = ...) -> None: ...
def stop_event_loop(self) -> None: ...
def key_press_handler(
event: KeyEvent,
canvas: FigureCanvasBase | None = ...,
toolbar: NavigationToolbar2 | None = ...,
) -> None: ...
def button_press_handler(
event: MouseEvent,
canvas: FigureCanvasBase | None = ...,
toolbar: NavigationToolbar2 | None = ...,
) -> None: ...
class NonGuiException(Exception): ...
class FigureManagerBase:
canvas: FigureCanvasBase
num: int | str
key_press_handler_id: int | None
button_press_handler_id: int | None
toolmanager: ToolManager | None
toolbar: NavigationToolbar2 | ToolContainerBase | None
def __init__(self, canvas: FigureCanvasBase, num: int | str) -> None: ...
@classmethod
def create_with_canvas(
cls, canvas_class: type[FigureCanvasBase], figure: Figure, num: int | str
) -> FigureManagerBase: ...
@classmethod
def start_main_loop(cls) -> None: ...
@classmethod
def pyplot_show(cls, *, block: bool | None = ...) -> None: ...
def show(self) -> None: ...
def destroy(self) -> None: ...
def full_screen_toggle(self) -> None: ...
def resize(self, w: int, h: int) -> None: ...
def get_window_title(self) -> str: ...
def set_window_title(self, title: str) -> None: ...
cursors = Cursors
class _Mode(str, Enum):
NONE: str
PAN: str
ZOOM: str
class NavigationToolbar2:
toolitems: tuple[tuple[str, ...] | tuple[None, ...], ...]
canvas: FigureCanvasBase
mode: _Mode
def __init__(self, canvas: FigureCanvasBase) -> None: ...
def set_message(self, s: str) -> None: ...
def draw_rubberband(
self, event: Event, x0: float, y0: float, x1: float, y1: float
) -> None: ...
def remove_rubberband(self) -> None: ...
def home(self, *args) -> None: ...
def back(self, *args) -> None: ...
def forward(self, *args) -> None: ...
def mouse_move(self, event: MouseEvent) -> None: ...
def pan(self, *args) -> None: ...
class _PanInfo(NamedTuple):
button: MouseButton
axes: list[Axes]
cid: int
def press_pan(self, event: Event) -> None: ...
def drag_pan(self, event: Event) -> None: ...
def release_pan(self, event: Event) -> None: ...
def zoom(self, *args) -> None: ...
class _ZoomInfo(NamedTuple):
direction: Literal["in", "out"]
start_xy: tuple[float, float]
axes: list[Axes]
cid: int
cbar: Colorbar
def press_zoom(self, event: Event) -> None: ...
def drag_zoom(self, event: Event) -> None: ...
def release_zoom(self, event: Event) -> None: ...
def push_current(self) -> None: ...
subplot_tool: widgets.SubplotTool
def configure_subplots(self, *args): ...
def save_figure(self, *args) -> None: ...
def update(self) -> None: ...
def set_history_buttons(self) -> None: ...
class ToolContainerBase:
toolmanager: ToolManager
def __init__(self, toolmanager: ToolManager) -> None: ...
def add_tool(self, tool: ToolBase, group: str, position: int = ...) -> None: ...
def trigger_tool(self, name: str) -> None: ...
def add_toolitem(
self,
name: str,
group: str,
position: int,
image: str,
description: str,
toggle: bool,
) -> None: ...
def toggle_toolitem(self, name: str, toggled: bool) -> None: ...
def remove_toolitem(self, name: str) -> None: ...
def set_message(self, s: str) -> None: ...
class _Backend:
backend_version: str
FigureCanvas: type[FigureCanvasBase] | None
FigureManager: type[FigureManagerBase]
mainloop: None | Callable[[], Any]
@classmethod
def new_figure_manager(cls, num: int | str, *args, **kwargs) -> FigureManagerBase: ...
@classmethod
def new_figure_manager_given_figure(cls, num: int | str, figure: Figure) -> FigureManagerBase: ...
@classmethod
def draw_if_interactive(cls) -> None: ...
@classmethod
def show(cls, *, block: bool | None = ...) -> None: ...
@staticmethod
def export(cls) -> type[_Backend]: ...
class ShowBase(_Backend):
def __call__(self, block: bool | None = ...) -> None: ...

View File

@ -0,0 +1,387 @@
from matplotlib import _api, backend_tools, cbook, widgets
class ToolEvent:
"""Event for tool manipulation (add/remove)."""
def __init__(self, name, sender, tool, data=None):
self.name = name
self.sender = sender
self.tool = tool
self.data = data
class ToolTriggerEvent(ToolEvent):
"""Event to inform that a tool has been triggered."""
def __init__(self, name, sender, tool, canvasevent=None, data=None):
super().__init__(name, sender, tool, data)
self.canvasevent = canvasevent
class ToolManagerMessageEvent:
"""
Event carrying messages from toolmanager.
Messages usually get displayed to the user by the toolbar.
"""
def __init__(self, name, sender, message):
self.name = name
self.sender = sender
self.message = message
class ToolManager:
"""
Manager for actions triggered by user interactions (key press, toolbar
clicks, ...) on a Figure.
Attributes
----------
figure : `.Figure`
keypresslock : `~matplotlib.widgets.LockDraw`
`.LockDraw` object to know if the `canvas` key_press_event is locked.
messagelock : `~matplotlib.widgets.LockDraw`
`.LockDraw` object to know if the message is available to write.
"""
def __init__(self, figure=None):
self._key_press_handler_id = None
self._tools = {}
self._keys = {}
self._toggled = {}
self._callbacks = cbook.CallbackRegistry()
# to process keypress event
self.keypresslock = widgets.LockDraw()
self.messagelock = widgets.LockDraw()
self._figure = None
self.set_figure(figure)
@property
def canvas(self):
"""Canvas managed by FigureManager."""
if not self._figure:
return None
return self._figure.canvas
@property
def figure(self):
"""Figure that holds the canvas."""
return self._figure
@figure.setter
def figure(self, figure):
self.set_figure(figure)
def set_figure(self, figure, update_tools=True):
"""
Bind the given figure to the tools.
Parameters
----------
figure : `.Figure`
update_tools : bool, default: True
Force tools to update figure.
"""
if self._key_press_handler_id:
self.canvas.mpl_disconnect(self._key_press_handler_id)
self._figure = figure
if figure:
self._key_press_handler_id = self.canvas.mpl_connect(
'key_press_event', self._key_press)
if update_tools:
for tool in self._tools.values():
tool.figure = figure
def toolmanager_connect(self, s, func):
"""
Connect event with string *s* to *func*.
Parameters
----------
s : str
The name of the event. The following events are recognized:
- 'tool_message_event'
- 'tool_removed_event'
- 'tool_added_event'
For every tool added a new event is created
- 'tool_trigger_TOOLNAME', where TOOLNAME is the id of the tool.
func : callable
Callback function for the toolmanager event with signature::
def func(event: ToolEvent) -> Any
Returns
-------
cid
The callback id for the connection. This can be used in
`.toolmanager_disconnect`.
"""
return self._callbacks.connect(s, func)
def toolmanager_disconnect(self, cid):
"""
Disconnect callback id *cid*.
Example usage::
cid = toolmanager.toolmanager_connect('tool_trigger_zoom', onpress)
#...later
toolmanager.toolmanager_disconnect(cid)
"""
return self._callbacks.disconnect(cid)
def message_event(self, message, sender=None):
"""Emit a `ToolManagerMessageEvent`."""
if sender is None:
sender = self
s = 'tool_message_event'
event = ToolManagerMessageEvent(s, sender, message)
self._callbacks.process(s, event)
@property
def active_toggle(self):
"""Currently toggled tools."""
return self._toggled
def get_tool_keymap(self, name):
"""
Return the keymap associated with the specified tool.
Parameters
----------
name : str
Name of the Tool.
Returns
-------
list of str
List of keys associated with the tool.
"""
keys = [k for k, i in self._keys.items() if i == name]
return keys
def _remove_keys(self, name):
for k in self.get_tool_keymap(name):
del self._keys[k]
def update_keymap(self, name, key):
"""
Set the keymap to associate with the specified tool.
Parameters
----------
name : str
Name of the Tool.
key : str or list of str
Keys to associate with the tool.
"""
if name not in self._tools:
raise KeyError(f'{name!r} not in Tools')
self._remove_keys(name)
if isinstance(key, str):
key = [key]
for k in key:
if k in self._keys:
_api.warn_external(
f'Key {k} changed from {self._keys[k]} to {name}')
self._keys[k] = name
def remove_tool(self, name):
"""
Remove tool named *name*.
Parameters
----------
name : str
Name of the tool.
"""
tool = self.get_tool(name)
if getattr(tool, 'toggled', False): # If it's a toggled toggle tool, untoggle
self.trigger_tool(tool, 'toolmanager')
self._remove_keys(name)
event = ToolEvent('tool_removed_event', self, tool)
self._callbacks.process(event.name, event)
del self._tools[name]
def add_tool(self, name, tool, *args, **kwargs):
"""
Add *tool* to `ToolManager`.
If successful, adds a new event ``tool_trigger_{name}`` where
``{name}`` is the *name* of the tool; the event is fired every time the
tool is triggered.
Parameters
----------
name : str
Name of the tool, treated as the ID, has to be unique.
tool : type
Class of the tool to be added. A subclass will be used
instead if one was registered for the current canvas class.
*args, **kwargs
Passed to the *tool*'s constructor.
See Also
--------
matplotlib.backend_tools.ToolBase : The base class for tools.
"""
tool_cls = backend_tools._find_tool_class(type(self.canvas), tool)
if not tool_cls:
raise ValueError('Impossible to find class for %s' % str(tool))
if name in self._tools:
_api.warn_external('A "Tool class" with the same name already '
'exists, not added')
return self._tools[name]
tool_obj = tool_cls(self, name, *args, **kwargs)
self._tools[name] = tool_obj
if tool_obj.default_keymap is not None:
self.update_keymap(name, tool_obj.default_keymap)
# For toggle tools init the radio_group in self._toggled
if isinstance(tool_obj, backend_tools.ToolToggleBase):
# None group is not mutually exclusive, a set is used to keep track
# of all toggled tools in this group
if tool_obj.radio_group is None:
self._toggled.setdefault(None, set())
else:
self._toggled.setdefault(tool_obj.radio_group, None)
# If initially toggled
if tool_obj.toggled:
self._handle_toggle(tool_obj, None, None)
tool_obj.set_figure(self.figure)
event = ToolEvent('tool_added_event', self, tool_obj)
self._callbacks.process(event.name, event)
return tool_obj
def _handle_toggle(self, tool, canvasevent, data):
"""
Toggle tools, need to untoggle prior to using other Toggle tool.
Called from trigger_tool.
Parameters
----------
tool : `.ToolBase`
canvasevent : Event
Original Canvas event or None.
data : object
Extra data to pass to the tool when triggering.
"""
radio_group = tool.radio_group
# radio_group None is not mutually exclusive
# just keep track of toggled tools in this group
if radio_group is None:
if tool.name in self._toggled[None]:
self._toggled[None].remove(tool.name)
else:
self._toggled[None].add(tool.name)
return
# If the tool already has a toggled state, untoggle it
if self._toggled[radio_group] == tool.name:
toggled = None
# If no tool was toggled in the radio_group
# toggle it
elif self._toggled[radio_group] is None:
toggled = tool.name
# Other tool in the radio_group is toggled
else:
# Untoggle previously toggled tool
self.trigger_tool(self._toggled[radio_group],
self,
canvasevent,
data)
toggled = tool.name
# Keep track of the toggled tool in the radio_group
self._toggled[radio_group] = toggled
def trigger_tool(self, name, sender=None, canvasevent=None, data=None):
"""
Trigger a tool and emit the ``tool_trigger_{name}`` event.
Parameters
----------
name : str
Name of the tool.
sender : object
Object that wishes to trigger the tool.
canvasevent : Event
Original Canvas event or None.
data : object
Extra data to pass to the tool when triggering.
"""
tool = self.get_tool(name)
if tool is None:
return
if sender is None:
sender = self
if isinstance(tool, backend_tools.ToolToggleBase):
self._handle_toggle(tool, canvasevent, data)
tool.trigger(sender, canvasevent, data) # Actually trigger Tool.
s = 'tool_trigger_%s' % name
event = ToolTriggerEvent(s, sender, tool, canvasevent, data)
self._callbacks.process(s, event)
def _key_press(self, event):
if event.key is None or self.keypresslock.locked():
return
name = self._keys.get(event.key, None)
if name is None:
return
self.trigger_tool(name, canvasevent=event)
@property
def tools(self):
"""A dict mapping tool name -> controlled tool."""
return self._tools
def get_tool(self, name, warn=True):
"""
Return the tool object with the given name.
For convenience, this passes tool objects through.
Parameters
----------
name : str or `.ToolBase`
Name of the tool, or the tool itself.
warn : bool, default: True
Whether a warning should be emitted it no tool with the given name
exists.
Returns
-------
`.ToolBase` or None
The tool or None if no tool with the given name exists.
"""
if (isinstance(name, backend_tools.ToolBase)
and name.name in self._tools):
return name
if name not in self._tools:
if warn:
_api.warn_external(
f"ToolManager does not control tool {name!r}")
return None
return self._tools[name]

View File

@ -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

View File

@ -0,0 +1,121 @@
import enum
from matplotlib import cbook
from matplotlib.axes import Axes
from matplotlib.backend_bases import ToolContainerBase, FigureCanvasBase
from matplotlib.backend_managers import ToolManager, ToolEvent
from matplotlib.figure import Figure
from matplotlib.scale import ScaleBase
from typing import Any
class Cursors(enum.IntEnum):
POINTER: int
HAND: int
SELECT_REGION: int
MOVE: int
WAIT: int
RESIZE_HORIZONTAL: int
RESIZE_VERTICAL: int
cursors = Cursors
class ToolBase:
@property
def default_keymap(self) -> list[str] | None: ...
description: str | None
image: str | None
def __init__(self, toolmanager: ToolManager, name: str) -> None: ...
@property
def name(self) -> str: ...
@property
def toolmanager(self) -> ToolManager: ...
@property
def canvas(self) -> FigureCanvasBase | None: ...
@property
def figure(self) -> Figure | None: ...
@figure.setter
def figure(self, figure: Figure | None) -> None: ...
def set_figure(self, figure: Figure | None) -> None: ...
def trigger(self, sender: Any, event: ToolEvent, data: Any = ...) -> None: ...
class ToolToggleBase(ToolBase):
radio_group: str | None
cursor: Cursors | None
default_toggled: bool
def __init__(self, *args, **kwargs) -> None: ...
def enable(self, event: ToolEvent | None = ...) -> None: ...
def disable(self, event: ToolEvent | None = ...) -> None: ...
@property
def toggled(self) -> bool: ...
def set_figure(self, figure: Figure | None) -> None: ...
class ToolSetCursor(ToolBase): ...
class ToolCursorPosition(ToolBase):
def send_message(self, event: ToolEvent) -> None: ...
class RubberbandBase(ToolBase):
def draw_rubberband(self, *data) -> None: ...
def remove_rubberband(self) -> None: ...
class ToolQuit(ToolBase): ...
class ToolQuitAll(ToolBase): ...
class ToolGrid(ToolBase): ...
class ToolMinorGrid(ToolBase): ...
class ToolFullScreen(ToolBase): ...
class AxisScaleBase(ToolToggleBase):
def enable(self, event: ToolEvent | None = ...) -> None: ...
def disable(self, event: ToolEvent | None = ...) -> None: ...
class ToolYScale(AxisScaleBase):
def set_scale(self, ax: Axes, scale: str | ScaleBase) -> None: ...
class ToolXScale(AxisScaleBase):
def set_scale(self, ax, scale: str | ScaleBase) -> None: ...
class ToolViewsPositions(ToolBase):
views: dict[Figure | Axes, cbook.Stack]
positions: dict[Figure | Axes, cbook.Stack]
home_views: dict[Figure, dict[Axes, tuple[float, float, float, float]]]
def add_figure(self, figure: Figure) -> None: ...
def clear(self, figure: Figure) -> None: ...
def update_view(self) -> None: ...
def push_current(self, figure: Figure | None = ...) -> None: ...
def update_home_views(self, figure: Figure | None = ...) -> None: ...
def home(self) -> None: ...
def back(self) -> None: ...
def forward(self) -> None: ...
class ViewsPositionsBase(ToolBase): ...
class ToolHome(ViewsPositionsBase): ...
class ToolBack(ViewsPositionsBase): ...
class ToolForward(ViewsPositionsBase): ...
class ConfigureSubplotsBase(ToolBase): ...
class SaveFigureBase(ToolBase): ...
class ZoomPanBase(ToolToggleBase):
base_scale: float
scrollthresh: float
lastscroll: float
def __init__(self, *args) -> None: ...
def enable(self, event: ToolEvent | None = ...) -> None: ...
def disable(self, event: ToolEvent | None = ...) -> None: ...
def scroll_zoom(self, event: ToolEvent) -> None: ...
class ToolZoom(ZoomPanBase): ...
class ToolPan(ZoomPanBase): ...
class ToolHelpBase(ToolBase):
@staticmethod
def format_shortcut(key_sequence: str) -> str: ...
class ToolCopyToClipboardBase(ToolBase): ...
default_tools: dict[str, ToolBase]
default_toolbar_tools: list[list[str | list[str]]]
def add_tools_to_manager(
toolmanager: ToolManager, tools: dict[str, type[ToolBase]] = ...
) -> None: ...
def add_tools_to_container(container: ToolContainerBase, tools: list[Any] = ...) -> None: ...

View File

@ -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

View File

@ -0,0 +1,332 @@
"""
Common code for GTK3 and GTK4 backends.
"""
import logging
import sys
import matplotlib as mpl
from matplotlib import _api, backend_tools, cbook
from matplotlib._pylab_helpers import Gcf
from matplotlib.backend_bases import (
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
TimerBase)
from matplotlib.backend_tools import Cursors
import gi
# The GTK3/GTK4 backends will have already called `gi.require_version` to set
# the desired GTK.
from gi.repository import Gdk, Gio, GLib, Gtk
try:
gi.require_foreign("cairo")
except ImportError as e:
raise ImportError("Gtk-based backends require cairo") from e
_log = logging.getLogger(__name__)
_application = None # Placeholder
def _shutdown_application(app):
# The application might prematurely shut down if Ctrl-C'd out of IPython,
# so close all windows.
for win in app.get_windows():
win.close()
# The PyGObject wrapper incorrectly thinks that None is not allowed, or we
# would call this:
# Gio.Application.set_default(None)
# Instead, we set this property and ignore default applications with it:
app._created_by_matplotlib = True
global _application
_application = None
def _create_application():
global _application
if _application is None:
app = Gio.Application.get_default()
if app is None or getattr(app, '_created_by_matplotlib', False):
# display_is_valid returns False only if on Linux and neither X11
# nor Wayland display can be opened.
if not mpl._c_internal_utils.display_is_valid():
raise RuntimeError('Invalid DISPLAY variable')
_application = Gtk.Application.new('org.matplotlib.Matplotlib3',
Gio.ApplicationFlags.NON_UNIQUE)
# The activate signal must be connected, but we don't care for
# handling it, since we don't do any remote processing.
_application.connect('activate', lambda *args, **kwargs: None)
_application.connect('shutdown', _shutdown_application)
_application.register()
cbook._setup_new_guiapp()
else:
_application = app
return _application
def mpl_to_gtk_cursor_name(mpl_cursor):
return _api.check_getitem({
Cursors.MOVE: "move",
Cursors.HAND: "pointer",
Cursors.POINTER: "default",
Cursors.SELECT_REGION: "crosshair",
Cursors.WAIT: "wait",
Cursors.RESIZE_HORIZONTAL: "ew-resize",
Cursors.RESIZE_VERTICAL: "ns-resize",
}, cursor=mpl_cursor)
class TimerGTK(TimerBase):
"""Subclass of `.TimerBase` using GTK timer events."""
def __init__(self, *args, **kwargs):
self._timer = None
super().__init__(*args, **kwargs)
def _timer_start(self):
# Need to stop it, otherwise we potentially leak a timer id that will
# never be stopped.
self._timer_stop()
self._timer = GLib.timeout_add(self._interval, self._on_timer)
def _timer_stop(self):
if self._timer is not None:
GLib.source_remove(self._timer)
self._timer = None
def _timer_set_interval(self):
# Only stop and restart it if the timer has already been started.
if self._timer is not None:
self._timer_stop()
self._timer_start()
def _on_timer(self):
super()._on_timer()
# Gtk timeout_add() requires that the callback returns True if it
# is to be called again.
if self.callbacks and not self._single:
return True
else:
self._timer = None
return False
class _FigureCanvasGTK(FigureCanvasBase):
_timer_cls = TimerGTK
class _FigureManagerGTK(FigureManagerBase):
"""
Attributes
----------
canvas : `FigureCanvas`
The FigureCanvas instance
num : int or str
The Figure number
toolbar : Gtk.Toolbar or Gtk.Box
The toolbar
vbox : Gtk.VBox
The Gtk.VBox containing the canvas and toolbar
window : Gtk.Window
The Gtk.Window
"""
def __init__(self, canvas, num):
self._gtk_ver = gtk_ver = Gtk.get_major_version()
app = _create_application()
self.window = Gtk.Window()
app.add_window(self.window)
super().__init__(canvas, num)
if gtk_ver == 3:
self.window.set_wmclass("matplotlib", "Matplotlib")
icon_ext = "png" if sys.platform == "win32" else "svg"
self.window.set_icon_from_file(
str(cbook._get_data_path(f"images/matplotlib.{icon_ext}")))
self.vbox = Gtk.Box()
self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
if gtk_ver == 3:
self.window.add(self.vbox)
self.vbox.show()
self.canvas.show()
self.vbox.pack_start(self.canvas, True, True, 0)
elif gtk_ver == 4:
self.window.set_child(self.vbox)
self.vbox.prepend(self.canvas)
# calculate size for window
w, h = self.canvas.get_width_height()
if self.toolbar is not None:
if gtk_ver == 3:
self.toolbar.show()
self.vbox.pack_end(self.toolbar, False, False, 0)
elif gtk_ver == 4:
sw = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER)
sw.set_child(self.toolbar)
self.vbox.append(sw)
min_size, nat_size = self.toolbar.get_preferred_size()
h += nat_size.height
self.window.set_default_size(w, h)
self._destroying = False
self.window.connect("destroy", lambda *args: Gcf.destroy(self))
self.window.connect({3: "delete_event", 4: "close-request"}[gtk_ver],
lambda *args: Gcf.destroy(self))
if mpl.is_interactive():
self.window.show()
self.canvas.draw_idle()
self.canvas.grab_focus()
def destroy(self, *args):
if self._destroying:
# Otherwise, this can be called twice when the user presses 'q',
# which calls Gcf.destroy(self), then this destroy(), then triggers
# Gcf.destroy(self) once again via
# `connect("destroy", lambda *args: Gcf.destroy(self))`.
return
self._destroying = True
self.window.destroy()
self.canvas.destroy()
@classmethod
def start_main_loop(cls):
global _application
if _application is None:
return
try:
_application.run() # Quits when all added windows close.
except KeyboardInterrupt:
# Ensure all windows can process their close event from
# _shutdown_application.
context = GLib.MainContext.default()
while context.pending():
context.iteration(True)
raise
finally:
# Running after quit is undefined, so create a new one next time.
_application = None
def show(self):
# show the figure window
self.window.show()
self.canvas.draw()
if mpl.rcParams["figure.raise_window"]:
meth_name = {3: "get_window", 4: "get_surface"}[self._gtk_ver]
if getattr(self.window, meth_name)():
self.window.present()
else:
# If this is called by a callback early during init,
# self.window (a GtkWindow) may not have an associated
# low-level GdkWindow (on GTK3) or GdkSurface (on GTK4) yet,
# and present() would crash.
_api.warn_external("Cannot raise window yet to be setup")
def full_screen_toggle(self):
is_fullscreen = {
3: lambda w: (w.get_window().get_state()
& Gdk.WindowState.FULLSCREEN),
4: lambda w: w.is_fullscreen(),
}[self._gtk_ver]
if is_fullscreen(self.window):
self.window.unfullscreen()
else:
self.window.fullscreen()
def get_window_title(self):
return self.window.get_title()
def set_window_title(self, title):
self.window.set_title(title)
def resize(self, width, height):
width = int(width / self.canvas.device_pixel_ratio)
height = int(height / self.canvas.device_pixel_ratio)
if self.toolbar:
min_size, nat_size = self.toolbar.get_preferred_size()
height += nat_size.height
canvas_size = self.canvas.get_allocation()
if self._gtk_ver >= 4 or canvas_size.width == canvas_size.height == 1:
# A canvas size of (1, 1) cannot exist in most cases, because
# window decorations would prevent such a small window. This call
# must be before the window has been mapped and widgets have been
# sized, so just change the window's starting size.
self.window.set_default_size(width, height)
else:
self.window.resize(width, height)
class _NavigationToolbar2GTK(NavigationToolbar2):
# Must be implemented in GTK3/GTK4 backends:
# * __init__
# * save_figure
def set_message(self, s):
escaped = GLib.markup_escape_text(s)
self.message.set_markup(f'<small>{escaped}</small>')
def draw_rubberband(self, event, x0, y0, x1, y1):
height = self.canvas.figure.bbox.height
y1 = height - y1
y0 = height - y0
rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)]
self.canvas._draw_rubberband(rect)
def remove_rubberband(self):
self.canvas._draw_rubberband(None)
def _update_buttons_checked(self):
for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]:
button = self._gtk_ids.get(name)
if button:
with button.handler_block(button._signal_handler):
button.set_active(self.mode.name == active)
def pan(self, *args):
super().pan(*args)
self._update_buttons_checked()
def zoom(self, *args):
super().zoom(*args)
self._update_buttons_checked()
def set_history_buttons(self):
can_backward = self._nav_stack._pos > 0
can_forward = self._nav_stack._pos < len(self._nav_stack) - 1
if 'Back' in self._gtk_ids:
self._gtk_ids['Back'].set_sensitive(can_backward)
if 'Forward' in self._gtk_ids:
self._gtk_ids['Forward'].set_sensitive(can_forward)
class RubberbandGTK(backend_tools.RubberbandBase):
def draw_rubberband(self, x0, y0, x1, y1):
_NavigationToolbar2GTK.draw_rubberband(
self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1)
def remove_rubberband(self):
_NavigationToolbar2GTK.remove_rubberband(
self._make_classic_style_pseudo_toolbar())
class ConfigureSubplotsGTK(backend_tools.ConfigureSubplotsBase):
def trigger(self, *args):
_NavigationToolbar2GTK.configure_subplots(self, None)
class _BackendGTK(_Backend):
backend_version = "{}.{}.{}".format(
Gtk.get_major_version(),
Gtk.get_minor_version(),
Gtk.get_micro_version(),
)
mainloop = _FigureManagerGTK.start_main_loop

View File

@ -0,0 +1,145 @@
"""
Common functionality between the PDF and PS backends.
"""
from io import BytesIO
import functools
from fontTools import subset
import matplotlib as mpl
from .. import font_manager, ft2font
from .._afm import AFM
from ..backend_bases import RendererBase
@functools.lru_cache(50)
def _cached_get_afm_from_fname(fname):
with open(fname, "rb") as fh:
return AFM(fh)
def get_glyphs_subset(fontfile, characters):
"""
Subset a TTF font
Reads the named fontfile and restricts the font to the characters.
Returns a serialization of the subset font as file-like object.
Parameters
----------
fontfile : str
Path to the font file
characters : str
Continuous set of characters to include in subset
"""
options = subset.Options(glyph_names=True, recommended_glyphs=True)
# Prevent subsetting extra tables.
options.drop_tables += [
'FFTM', # FontForge Timestamp.
'PfEd', # FontForge personal table.
'BDF', # X11 BDF header.
'meta', # Metadata stores design/supported languages (meaningless for subsets).
]
# if fontfile is a ttc, specify font number
if fontfile.endswith(".ttc"):
options.font_number = 0
with subset.load_font(fontfile, options) as font:
subsetter = subset.Subsetter(options=options)
subsetter.populate(text=characters)
subsetter.subset(font)
fh = BytesIO()
font.save(fh, reorderTables=False)
return fh
class CharacterTracker:
"""
Helper for font subsetting by the pdf and ps backends.
Maintains a mapping of font paths to the set of character codepoints that
are being used from that font.
"""
def __init__(self):
self.used = {}
def track(self, font, s):
"""Record that string *s* is being typeset using font *font*."""
char_to_font = font._get_fontmap(s)
for _c, _f in char_to_font.items():
self.used.setdefault(_f.fname, set()).add(ord(_c))
def track_glyph(self, font, glyph):
"""Record that codepoint *glyph* is being typeset using font *font*."""
self.used.setdefault(font.fname, set()).add(glyph)
class RendererPDFPSBase(RendererBase):
# The following attributes must be defined by the subclasses:
# - _afm_font_dir
# - _use_afm_rc_name
def __init__(self, width, height):
super().__init__()
self.width = width
self.height = height
def flipy(self):
# docstring inherited
return False # y increases from bottom to top.
def option_scale_image(self):
# docstring inherited
return True # PDF and PS support arbitrary image scaling.
def option_image_nocomposite(self):
# docstring inherited
# Decide whether to composite image based on rcParam value.
return not mpl.rcParams["image.composite_image"]
def get_canvas_width_height(self):
# docstring inherited
return self.width * 72.0, self.height * 72.0
def get_text_width_height_descent(self, s, prop, ismath):
# docstring inherited
if ismath == "TeX":
return super().get_text_width_height_descent(s, prop, ismath)
elif ismath:
parse = self._text2path.mathtext_parser.parse(s, 72, prop)
return parse.width, parse.height, parse.depth
elif mpl.rcParams[self._use_afm_rc_name]:
font = self._get_font_afm(prop)
l, b, w, h, d = font.get_str_bbox_and_descent(s)
scale = prop.get_size_in_points() / 1000
w *= scale
h *= scale
d *= scale
return w, h, d
else:
font = self._get_font_ttf(prop)
font.set_text(s, 0.0, flags=ft2font.LOAD_NO_HINTING)
w, h = font.get_width_height()
d = font.get_descent()
scale = 1 / 64
w *= scale
h *= scale
d *= scale
return w, h, d
def _get_font_afm(self, prop):
fname = font_manager.findfont(
prop, fontext="afm", directory=self._afm_font_dir)
return _cached_get_afm_from_fname(fname)
def _get_font_ttf(self, prop):
fnames = font_manager.fontManager._find_fonts_by_props(prop)
font = font_manager.get_font(fnames)
font.clear()
font.set_size(prop.get_size_in_points(), 72)
return font

File diff suppressed because it is too large Load Diff

View File

@ -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: ...

View File

@ -0,0 +1,543 @@
"""
An `Anti-Grain Geometry`_ (AGG) backend.
Features that are implemented:
* capstyles and join styles
* dashes
* linewidth
* lines, rectangles, ellipses
* clipping to a rectangle
* output to RGBA and Pillow-supported image formats
* alpha blending
* DPI scaling properly - everything scales properly (dashes, linewidths, etc)
* draw polygon
* freetype2 w/ ft2font
Still TODO:
* integrate screen dpi w/ ppi and text
.. _Anti-Grain Geometry: http://agg.sourceforge.net/antigrain.com
"""
from contextlib import nullcontext
from math import radians, cos, sin
import numpy as np
import matplotlib as mpl
from matplotlib import _api, cbook
from matplotlib.backend_bases import (
_Backend, FigureCanvasBase, FigureManagerBase, RendererBase)
from matplotlib.font_manager import fontManager as _fontManager, get_font
from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING,
LOAD_DEFAULT, LOAD_NO_AUTOHINT)
from matplotlib.mathtext import MathTextParser
from matplotlib.path import Path
from matplotlib.transforms import Bbox, BboxBase
from matplotlib.backends._backend_agg import RendererAgg as _RendererAgg
def get_hinting_flag():
mapping = {
'default': LOAD_DEFAULT,
'no_autohint': LOAD_NO_AUTOHINT,
'force_autohint': LOAD_FORCE_AUTOHINT,
'no_hinting': LOAD_NO_HINTING,
True: LOAD_FORCE_AUTOHINT,
False: LOAD_NO_HINTING,
'either': LOAD_DEFAULT,
'native': LOAD_NO_AUTOHINT,
'auto': LOAD_FORCE_AUTOHINT,
'none': LOAD_NO_HINTING,
}
return mapping[mpl.rcParams['text.hinting']]
class RendererAgg(RendererBase):
"""
The renderer handles all the drawing primitives using a graphics
context instance that controls the colors/styles
"""
def __init__(self, width, height, dpi):
super().__init__()
self.dpi = dpi
self.width = width
self.height = height
self._renderer = _RendererAgg(int(width), int(height), dpi)
self._filter_renderers = []
self._update_methods()
self.mathtext_parser = MathTextParser('agg')
self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
def __getstate__(self):
# We only want to preserve the init keywords of the Renderer.
# Anything else can be re-created.
return {'width': self.width, 'height': self.height, 'dpi': self.dpi}
def __setstate__(self, state):
self.__init__(state['width'], state['height'], state['dpi'])
def _update_methods(self):
self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles
self.draw_image = self._renderer.draw_image
self.draw_markers = self._renderer.draw_markers
self.draw_path_collection = self._renderer.draw_path_collection
self.draw_quad_mesh = self._renderer.draw_quad_mesh
self.copy_from_bbox = self._renderer.copy_from_bbox
def draw_path(self, gc, path, transform, rgbFace=None):
# docstring inherited
nmax = mpl.rcParams['agg.path.chunksize'] # here at least for testing
npts = path.vertices.shape[0]
if (npts > nmax > 100 and path.should_simplify and
rgbFace is None and gc.get_hatch() is None):
nch = np.ceil(npts / nmax)
chsize = int(np.ceil(npts / nch))
i0 = np.arange(0, npts, chsize)
i1 = np.zeros_like(i0)
i1[:-1] = i0[1:] - 1
i1[-1] = npts
for ii0, ii1 in zip(i0, i1):
v = path.vertices[ii0:ii1, :]
c = path.codes
if c is not None:
c = c[ii0:ii1]
c[0] = Path.MOVETO # move to end of last chunk
p = Path(v, c)
p.simplify_threshold = path.simplify_threshold
try:
self._renderer.draw_path(gc, p, transform, rgbFace)
except OverflowError:
msg = (
"Exceeded cell block limit in Agg.\n\n"
"Please reduce the value of "
f"rcParams['agg.path.chunksize'] (currently {nmax}) "
"or increase the path simplification threshold"
"(rcParams['path.simplify_threshold'] = "
f"{mpl.rcParams['path.simplify_threshold']:.2f} by "
"default and path.simplify_threshold = "
f"{path.simplify_threshold:.2f} on the input)."
)
raise OverflowError(msg) from None
else:
try:
self._renderer.draw_path(gc, path, transform, rgbFace)
except OverflowError:
cant_chunk = ''
if rgbFace is not None:
cant_chunk += "- cannot split filled path\n"
if gc.get_hatch() is not None:
cant_chunk += "- cannot split hatched path\n"
if not path.should_simplify:
cant_chunk += "- path.should_simplify is False\n"
if len(cant_chunk):
msg = (
"Exceeded cell block limit in Agg, however for the "
"following reasons:\n\n"
f"{cant_chunk}\n"
"we cannot automatically split up this path to draw."
"\n\nPlease manually simplify your path."
)
else:
inc_threshold = (
"or increase the path simplification threshold"
"(rcParams['path.simplify_threshold'] = "
f"{mpl.rcParams['path.simplify_threshold']} "
"by default and path.simplify_threshold "
f"= {path.simplify_threshold} "
"on the input)."
)
if nmax > 100:
msg = (
"Exceeded cell block limit in Agg. Please reduce "
"the value of rcParams['agg.path.chunksize'] "
f"(currently {nmax}) {inc_threshold}"
)
else:
msg = (
"Exceeded cell block limit in Agg. Please set "
"the value of rcParams['agg.path.chunksize'], "
f"(currently {nmax}) to be greater than 100 "
+ inc_threshold
)
raise OverflowError(msg) from None
def draw_mathtext(self, gc, x, y, s, prop, angle):
"""Draw mathtext using :mod:`matplotlib.mathtext`."""
ox, oy, width, height, descent, font_image = \
self.mathtext_parser.parse(s, self.dpi, prop,
antialiased=gc.get_antialiased())
xd = descent * sin(radians(angle))
yd = descent * cos(radians(angle))
x = round(x + ox + xd)
y = round(y - oy + yd)
self._renderer.draw_text_image(font_image, x, y + 1, angle, gc)
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
# docstring inherited
if ismath:
return self.draw_mathtext(gc, x, y, s, prop, angle)
font = self._prepare_font(prop)
# We pass '0' for angle here, since it will be rotated (in raster
# space) in the following call to draw_text_image).
font.set_text(s, 0, flags=get_hinting_flag())
font.draw_glyphs_to_bitmap(
antialiased=gc.get_antialiased())
d = font.get_descent() / 64.0
# The descent needs to be adjusted for the angle.
xo, yo = font.get_bitmap_offset()
xo /= 64.0
yo /= 64.0
xd = d * sin(radians(angle))
yd = d * cos(radians(angle))
x = round(x + xo + xd)
y = round(y + yo + yd)
self._renderer.draw_text_image(font, x, y + 1, angle, gc)
def get_text_width_height_descent(self, s, prop, ismath):
# docstring inherited
_api.check_in_list(["TeX", True, False], ismath=ismath)
if ismath == "TeX":
return super().get_text_width_height_descent(s, prop, ismath)
if ismath:
ox, oy, width, height, descent, font_image = \
self.mathtext_parser.parse(s, self.dpi, prop)
return width, height, descent
font = self._prepare_font(prop)
font.set_text(s, 0.0, flags=get_hinting_flag())
w, h = font.get_width_height() # width and height of unrotated string
d = font.get_descent()
w /= 64.0 # convert from subpixels
h /= 64.0
d /= 64.0
return w, h, d
def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
# docstring inherited
# todo, handle props, angle, origins
size = prop.get_size_in_points()
texmanager = self.get_texmanager()
Z = texmanager.get_grey(s, size, self.dpi)
Z = np.array(Z * 255.0, np.uint8)
w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX")
xd = d * sin(radians(angle))
yd = d * cos(radians(angle))
x = round(x + xd)
y = round(y + yd)
self._renderer.draw_text_image(Z, x, y, angle, gc)
def get_canvas_width_height(self):
# docstring inherited
return self.width, self.height
def _prepare_font(self, font_prop):
"""
Get the `.FT2Font` for *font_prop*, clear its buffer, and set its size.
"""
font = get_font(_fontManager._find_fonts_by_props(font_prop))
font.clear()
size = font_prop.get_size_in_points()
font.set_size(size, self.dpi)
return font
def points_to_pixels(self, points):
# docstring inherited
return points * self.dpi / 72
def buffer_rgba(self):
return memoryview(self._renderer)
def tostring_argb(self):
return np.asarray(self._renderer).take([3, 0, 1, 2], axis=2).tobytes()
@_api.deprecated("3.8", alternative="buffer_rgba")
def tostring_rgb(self):
return np.asarray(self._renderer).take([0, 1, 2], axis=2).tobytes()
def clear(self):
self._renderer.clear()
def option_image_nocomposite(self):
# docstring inherited
# It is generally faster to composite each image directly to
# the Figure, and there's no file size benefit to compositing
# with the Agg backend
return True
def option_scale_image(self):
# docstring inherited
return False
def restore_region(self, region, bbox=None, xy=None):
"""
Restore the saved region. If bbox (instance of BboxBase, or
its extents) is given, only the region specified by the bbox
will be restored. *xy* (a pair of floats) optionally
specifies the new position (the LLC of the original region,
not the LLC of the bbox) where the region will be restored.
>>> region = renderer.copy_from_bbox()
>>> x1, y1, x2, y2 = region.get_extents()
>>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2),
... xy=(x1-dx, y1))
"""
if bbox is not None or xy is not None:
if bbox is None:
x1, y1, x2, y2 = region.get_extents()
elif isinstance(bbox, BboxBase):
x1, y1, x2, y2 = bbox.extents
else:
x1, y1, x2, y2 = bbox
if xy is None:
ox, oy = x1, y1
else:
ox, oy = xy
# The incoming data is float, but the _renderer type-checking wants
# to see integers.
self._renderer.restore_region(region, int(x1), int(y1),
int(x2), int(y2), int(ox), int(oy))
else:
self._renderer.restore_region(region)
def start_filter(self):
"""
Start filtering. It simply creates a new canvas (the old one is saved).
"""
self._filter_renderers.append(self._renderer)
self._renderer = _RendererAgg(int(self.width), int(self.height),
self.dpi)
self._update_methods()
def stop_filter(self, post_processing):
"""
Save the current canvas as an image and apply post processing.
The *post_processing* function::
def post_processing(image, dpi):
# ny, nx, depth = image.shape
# image (numpy array) has RGBA channels and has a depth of 4.
...
# create a new_image (numpy array of 4 channels, size can be
# different). The resulting image may have offsets from
# lower-left corner of the original image
return new_image, offset_x, offset_y
The saved renderer is restored and the returned image from
post_processing is plotted (using draw_image) on it.
"""
orig_img = np.asarray(self.buffer_rgba())
slice_y, slice_x = cbook._get_nonzero_slices(orig_img[..., 3])
cropped_img = orig_img[slice_y, slice_x]
self._renderer = self._filter_renderers.pop()
self._update_methods()
if cropped_img.size:
img, ox, oy = post_processing(cropped_img / 255, self.dpi)
gc = self.new_gc()
if img.dtype.kind == 'f':
img = np.asarray(img * 255., np.uint8)
self._renderer.draw_image(
gc, slice_x.start + ox, int(self.height) - slice_y.stop + oy,
img[::-1])
class FigureCanvasAgg(FigureCanvasBase):
# docstring inherited
_lastKey = None # Overwritten per-instance on the first draw.
def copy_from_bbox(self, bbox):
renderer = self.get_renderer()
return renderer.copy_from_bbox(bbox)
def restore_region(self, region, bbox=None, xy=None):
renderer = self.get_renderer()
return renderer.restore_region(region, bbox, xy)
def draw(self):
# docstring inherited
self.renderer = self.get_renderer()
self.renderer.clear()
# Acquire a lock on the shared font cache.
with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
else nullcontext()):
self.figure.draw(self.renderer)
# A GUI class may be need to update a window using this draw, so
# don't forget to call the superclass.
super().draw()
def get_renderer(self):
w, h = self.figure.bbox.size
key = w, h, self.figure.dpi
reuse_renderer = (self._lastKey == key)
if not reuse_renderer:
self.renderer = RendererAgg(w, h, self.figure.dpi)
self._lastKey = key
return self.renderer
@_api.deprecated("3.8", alternative="buffer_rgba")
def tostring_rgb(self):
"""
Get the image as RGB `bytes`.
`draw` must be called at least once before this function will work and
to update the renderer for any subsequent changes to the Figure.
"""
return self.renderer.tostring_rgb()
def tostring_argb(self):
"""
Get the image as ARGB `bytes`.
`draw` must be called at least once before this function will work and
to update the renderer for any subsequent changes to the Figure.
"""
return self.renderer.tostring_argb()
def buffer_rgba(self):
"""
Get the image as a `memoryview` to the renderer's buffer.
`draw` must be called at least once before this function will work and
to update the renderer for any subsequent changes to the Figure.
"""
return self.renderer.buffer_rgba()
def print_raw(self, filename_or_obj, *, metadata=None):
if metadata is not None:
raise ValueError("metadata not supported for raw/rgba")
FigureCanvasAgg.draw(self)
renderer = self.get_renderer()
with cbook.open_file_cm(filename_or_obj, "wb") as fh:
fh.write(renderer.buffer_rgba())
print_rgba = print_raw
def _print_pil(self, filename_or_obj, fmt, pil_kwargs, metadata=None):
"""
Draw the canvas, then save it using `.image.imsave` (to which
*pil_kwargs* and *metadata* are forwarded).
"""
FigureCanvasAgg.draw(self)
mpl.image.imsave(
filename_or_obj, self.buffer_rgba(), format=fmt, origin="upper",
dpi=self.figure.dpi, metadata=metadata, pil_kwargs=pil_kwargs)
def print_png(self, filename_or_obj, *, metadata=None, pil_kwargs=None):
"""
Write the figure to a PNG file.
Parameters
----------
filename_or_obj : str or path-like or file-like
The file to write to.
metadata : dict, optional
Metadata in the PNG file as key-value pairs of bytes or latin-1
encodable strings.
According to the PNG specification, keys must be shorter than 79
chars.
The `PNG specification`_ defines some common keywords that may be
used as appropriate:
- Title: Short (one line) title or caption for image.
- Author: Name of image's creator.
- Description: Description of image (possibly long).
- Copyright: Copyright notice.
- Creation Time: Time of original image creation
(usually RFC 1123 format).
- Software: Software used to create the image.
- Disclaimer: Legal disclaimer.
- Warning: Warning of nature of content.
- Source: Device used to create the image.
- Comment: Miscellaneous comment;
conversion from other image format.
Other keywords may be invented for other purposes.
If 'Software' is not given, an autogenerated value for Matplotlib
will be used. This can be removed by setting it to *None*.
For more details see the `PNG specification`_.
.. _PNG specification: \
https://www.w3.org/TR/2003/REC-PNG-20031110/#11keywords
pil_kwargs : dict, optional
Keyword arguments passed to `PIL.Image.Image.save`.
If the 'pnginfo' key is present, it completely overrides
*metadata*, including the default 'Software' key.
"""
self._print_pil(filename_or_obj, "png", pil_kwargs, metadata)
def print_to_buffer(self):
FigureCanvasAgg.draw(self)
renderer = self.get_renderer()
return (bytes(renderer.buffer_rgba()),
(int(renderer.width), int(renderer.height)))
# Note that these methods should typically be called via savefig() and
# print_figure(), and the latter ensures that `self.figure.dpi` already
# matches the dpi kwarg (if any).
def print_jpg(self, filename_or_obj, *, metadata=None, pil_kwargs=None):
# savefig() has already applied savefig.facecolor; we now set it to
# white to make imsave() blend semi-transparent figures against an
# assumed white background.
with mpl.rc_context({"savefig.facecolor": "white"}):
self._print_pil(filename_or_obj, "jpeg", pil_kwargs, metadata)
print_jpeg = print_jpg
def print_tif(self, filename_or_obj, *, metadata=None, pil_kwargs=None):
self._print_pil(filename_or_obj, "tiff", pil_kwargs, metadata)
print_tiff = print_tif
def print_webp(self, filename_or_obj, *, metadata=None, pil_kwargs=None):
self._print_pil(filename_or_obj, "webp", pil_kwargs, metadata)
print_jpg.__doc__, print_tif.__doc__, print_webp.__doc__ = map(
"""
Write the figure to a {} file.
Parameters
----------
filename_or_obj : str or path-like or file-like
The file to write to.
pil_kwargs : dict, optional
Additional keyword arguments that are passed to
`PIL.Image.Image.save` when saving the figure.
""".format, ["JPEG", "TIFF", "WebP"])
@_Backend.export
class _BackendAgg(_Backend):
backend_version = 'v2.2'
FigureCanvas = FigureCanvasAgg
FigureManager = FigureManagerBase

View File

@ -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

View File

@ -0,0 +1,582 @@
import functools
import logging
import os
from pathlib import Path
import matplotlib as mpl
from matplotlib import _api, backend_tools, cbook
from matplotlib.backend_bases import (
ToolContainerBase, CloseEvent, KeyEvent, LocationEvent, MouseEvent,
ResizeEvent)
try:
import gi
except ImportError as err:
raise ImportError("The GTK3 backends require PyGObject") from err
try:
# :raises ValueError: If module/version is already loaded, already
# required, or unavailable.
gi.require_version("Gtk", "3.0")
except ValueError as e:
# in this case we want to re-raise as ImportError so the
# auto-backend selection logic correctly skips.
raise ImportError(e) from e
from gi.repository import Gio, GLib, GObject, Gtk, Gdk
from . import _backend_gtk
from ._backend_gtk import ( # noqa: F401 # pylint: disable=W0611
_BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
TimerGTK as TimerGTK3,
)
_log = logging.getLogger(__name__)
@functools.cache
def _mpl_to_gtk_cursor(mpl_cursor):
return Gdk.Cursor.new_from_name(
Gdk.Display.get_default(),
_backend_gtk.mpl_to_gtk_cursor_name(mpl_cursor))
class FigureCanvasGTK3(_FigureCanvasGTK, Gtk.DrawingArea):
required_interactive_framework = "gtk3"
manager_class = _api.classproperty(lambda cls: FigureManagerGTK3)
# Setting this as a static constant prevents
# this resulting expression from leaking
event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK
| Gdk.EventMask.BUTTON_RELEASE_MASK
| Gdk.EventMask.EXPOSURE_MASK
| Gdk.EventMask.KEY_PRESS_MASK
| Gdk.EventMask.KEY_RELEASE_MASK
| Gdk.EventMask.ENTER_NOTIFY_MASK
| Gdk.EventMask.LEAVE_NOTIFY_MASK
| Gdk.EventMask.POINTER_MOTION_MASK
| Gdk.EventMask.SCROLL_MASK)
def __init__(self, figure=None):
super().__init__(figure=figure)
self._idle_draw_id = 0
self._rubberband_rect = None
self.connect('scroll_event', self.scroll_event)
self.connect('button_press_event', self.button_press_event)
self.connect('button_release_event', self.button_release_event)
self.connect('configure_event', self.configure_event)
self.connect('screen-changed', self._update_device_pixel_ratio)
self.connect('notify::scale-factor', self._update_device_pixel_ratio)
self.connect('draw', self.on_draw_event)
self.connect('draw', self._post_draw)
self.connect('key_press_event', self.key_press_event)
self.connect('key_release_event', self.key_release_event)
self.connect('motion_notify_event', self.motion_notify_event)
self.connect('enter_notify_event', self.enter_notify_event)
self.connect('leave_notify_event', self.leave_notify_event)
self.connect('size_allocate', self.size_allocate)
self.set_events(self.__class__.event_mask)
self.set_can_focus(True)
css = Gtk.CssProvider()
css.load_from_data(b".matplotlib-canvas { background-color: white; }")
style_ctx = self.get_style_context()
style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
style_ctx.add_class("matplotlib-canvas")
def destroy(self):
CloseEvent("close_event", self)._process()
def set_cursor(self, cursor):
# docstring inherited
window = self.get_property("window")
if window is not None:
window.set_cursor(_mpl_to_gtk_cursor(cursor))
context = GLib.MainContext.default()
context.iteration(True)
def _mpl_coords(self, event=None):
"""
Convert the position of a GTK event, or of the current cursor position
if *event* is None, to Matplotlib coordinates.
GTK use logical pixels, but the figure is scaled to physical pixels for
rendering. Transform to physical pixels so that all of the down-stream
transforms work as expected.
Also, the origin is different and needs to be corrected.
"""
if event is None:
window = self.get_window()
t, x, y, state = window.get_device_position(
window.get_display().get_device_manager().get_client_pointer())
else:
x, y = event.x, event.y
x = x * self.device_pixel_ratio
# flip y so y=0 is bottom of canvas
y = self.figure.bbox.height - y * self.device_pixel_ratio
return x, y
def scroll_event(self, widget, event):
step = 1 if event.direction == Gdk.ScrollDirection.UP else -1
MouseEvent("scroll_event", self,
*self._mpl_coords(event), step=step,
modifiers=self._mpl_modifiers(event.state),
guiEvent=event)._process()
return False # finish event propagation?
def button_press_event(self, widget, event):
MouseEvent("button_press_event", self,
*self._mpl_coords(event), event.button,
modifiers=self._mpl_modifiers(event.state),
guiEvent=event)._process()
return False # finish event propagation?
def button_release_event(self, widget, event):
MouseEvent("button_release_event", self,
*self._mpl_coords(event), event.button,
modifiers=self._mpl_modifiers(event.state),
guiEvent=event)._process()
return False # finish event propagation?
def key_press_event(self, widget, event):
KeyEvent("key_press_event", self,
self._get_key(event), *self._mpl_coords(),
guiEvent=event)._process()
return True # stop event propagation
def key_release_event(self, widget, event):
KeyEvent("key_release_event", self,
self._get_key(event), *self._mpl_coords(),
guiEvent=event)._process()
return True # stop event propagation
def motion_notify_event(self, widget, event):
MouseEvent("motion_notify_event", self, *self._mpl_coords(event),
modifiers=self._mpl_modifiers(event.state),
guiEvent=event)._process()
return False # finish event propagation?
def enter_notify_event(self, widget, event):
gtk_mods = Gdk.Keymap.get_for_display(
self.get_display()).get_modifier_state()
LocationEvent("figure_enter_event", self, *self._mpl_coords(event),
modifiers=self._mpl_modifiers(gtk_mods),
guiEvent=event)._process()
def leave_notify_event(self, widget, event):
gtk_mods = Gdk.Keymap.get_for_display(
self.get_display()).get_modifier_state()
LocationEvent("figure_leave_event", self, *self._mpl_coords(event),
modifiers=self._mpl_modifiers(gtk_mods),
guiEvent=event)._process()
def size_allocate(self, widget, allocation):
dpival = self.figure.dpi
winch = allocation.width * self.device_pixel_ratio / dpival
hinch = allocation.height * self.device_pixel_ratio / dpival
self.figure.set_size_inches(winch, hinch, forward=False)
ResizeEvent("resize_event", self)._process()
self.draw_idle()
@staticmethod
def _mpl_modifiers(event_state, *, exclude=None):
modifiers = [
("ctrl", Gdk.ModifierType.CONTROL_MASK, "control"),
("alt", Gdk.ModifierType.MOD1_MASK, "alt"),
("shift", Gdk.ModifierType.SHIFT_MASK, "shift"),
("super", Gdk.ModifierType.MOD4_MASK, "super"),
]
return [name for name, mask, key in modifiers
if exclude != key and event_state & mask]
def _get_key(self, event):
unikey = chr(Gdk.keyval_to_unicode(event.keyval))
key = cbook._unikey_or_keysym_to_mplkey(
unikey, Gdk.keyval_name(event.keyval))
mods = self._mpl_modifiers(event.state, exclude=key)
if "shift" in mods and unikey.isprintable():
mods.remove("shift")
return "+".join([*mods, key])
def _update_device_pixel_ratio(self, *args, **kwargs):
# We need to be careful in cases with mixed resolution displays if
# device_pixel_ratio changes.
if self._set_device_pixel_ratio(self.get_scale_factor()):
# The easiest way to resize the canvas is to emit a resize event
# since we implement all the logic for resizing the canvas for that
# event.
self.queue_resize()
self.queue_draw()
def configure_event(self, widget, event):
if widget.get_property("window") is None:
return
w = event.width * self.device_pixel_ratio
h = event.height * self.device_pixel_ratio
if w < 3 or h < 3:
return # empty fig
# resize the figure (in inches)
dpi = self.figure.dpi
self.figure.set_size_inches(w / dpi, h / dpi, forward=False)
return False # finish event propagation?
def _draw_rubberband(self, rect):
self._rubberband_rect = rect
# TODO: Only update the rubberband area.
self.queue_draw()
def _post_draw(self, widget, ctx):
if self._rubberband_rect is None:
return
x0, y0, w, h = (dim / self.device_pixel_ratio
for dim in self._rubberband_rect)
x1 = x0 + w
y1 = y0 + h
# Draw the lines from x0, y0 towards x1, y1 so that the
# dashes don't "jump" when moving the zoom box.
ctx.move_to(x0, y0)
ctx.line_to(x0, y1)
ctx.move_to(x0, y0)
ctx.line_to(x1, y0)
ctx.move_to(x0, y1)
ctx.line_to(x1, y1)
ctx.move_to(x1, y0)
ctx.line_to(x1, y1)
ctx.set_antialias(1)
ctx.set_line_width(1)
ctx.set_dash((3, 3), 0)
ctx.set_source_rgb(0, 0, 0)
ctx.stroke_preserve()
ctx.set_dash((3, 3), 3)
ctx.set_source_rgb(1, 1, 1)
ctx.stroke()
def on_draw_event(self, widget, ctx):
# to be overwritten by GTK3Agg or GTK3Cairo
pass
def draw(self):
# docstring inherited
if self.is_drawable():
self.queue_draw()
def draw_idle(self):
# docstring inherited
if self._idle_draw_id != 0:
return
def idle_draw(*args):
try:
self.draw()
finally:
self._idle_draw_id = 0
return False
self._idle_draw_id = GLib.idle_add(idle_draw)
def flush_events(self):
# docstring inherited
context = GLib.MainContext.default()
while context.pending():
context.iteration(True)
class NavigationToolbar2GTK3(_NavigationToolbar2GTK, Gtk.Toolbar):
def __init__(self, canvas):
GObject.GObject.__init__(self)
self.set_style(Gtk.ToolbarStyle.ICONS)
self._gtk_ids = {}
for text, tooltip_text, image_file, callback in self.toolitems:
if text is None:
self.insert(Gtk.SeparatorToolItem(), -1)
continue
image = Gtk.Image.new_from_gicon(
Gio.Icon.new_for_string(
str(cbook._get_data_path('images',
f'{image_file}-symbolic.svg'))),
Gtk.IconSize.LARGE_TOOLBAR)
self._gtk_ids[text] = button = (
Gtk.ToggleToolButton() if callback in ['zoom', 'pan'] else
Gtk.ToolButton())
button.set_label(text)
button.set_icon_widget(image)
# Save the handler id, so that we can block it as needed.
button._signal_handler = button.connect(
'clicked', getattr(self, callback))
button.set_tooltip_text(tooltip_text)
self.insert(button, -1)
# This filler item ensures the toolbar is always at least two text
# lines high. Otherwise the canvas gets redrawn as the mouse hovers
# over images because those use two-line messages which resize the
# toolbar.
toolitem = Gtk.ToolItem()
self.insert(toolitem, -1)
label = Gtk.Label()
label.set_markup(
'<small>\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}</small>')
toolitem.set_expand(True) # Push real message to the right.
toolitem.add(label)
toolitem = Gtk.ToolItem()
self.insert(toolitem, -1)
self.message = Gtk.Label()
self.message.set_justify(Gtk.Justification.RIGHT)
toolitem.add(self.message)
self.show_all()
_NavigationToolbar2GTK.__init__(self, canvas)
def save_figure(self, *args):
dialog = Gtk.FileChooserDialog(
title="Save the figure",
parent=self.canvas.get_toplevel(),
action=Gtk.FileChooserAction.SAVE,
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE, Gtk.ResponseType.OK),
)
for name, fmts \
in self.canvas.get_supported_filetypes_grouped().items():
ff = Gtk.FileFilter()
ff.set_name(name)
for fmt in fmts:
ff.add_pattern(f'*.{fmt}')
dialog.add_filter(ff)
if self.canvas.get_default_filetype() in fmts:
dialog.set_filter(ff)
@functools.partial(dialog.connect, "notify::filter")
def on_notify_filter(*args):
name = dialog.get_filter().get_name()
fmt = self.canvas.get_supported_filetypes_grouped()[name][0]
dialog.set_current_name(
str(Path(dialog.get_current_name()).with_suffix(f'.{fmt}')))
dialog.set_current_folder(mpl.rcParams["savefig.directory"])
dialog.set_current_name(self.canvas.get_default_filename())
dialog.set_do_overwrite_confirmation(True)
response = dialog.run()
fname = dialog.get_filename()
ff = dialog.get_filter() # Doesn't autoadjust to filename :/
fmt = self.canvas.get_supported_filetypes_grouped()[ff.get_name()][0]
dialog.destroy()
if response != Gtk.ResponseType.OK:
return
# Save dir for next time, unless empty str (which means use cwd).
if mpl.rcParams['savefig.directory']:
mpl.rcParams['savefig.directory'] = os.path.dirname(fname)
try:
self.canvas.figure.savefig(fname, format=fmt)
except Exception as e:
dialog = Gtk.MessageDialog(
parent=self.canvas.get_toplevel(), message_format=str(e),
type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK)
dialog.run()
dialog.destroy()
class ToolbarGTK3(ToolContainerBase, Gtk.Box):
_icon_extension = '-symbolic.svg'
def __init__(self, toolmanager):
ToolContainerBase.__init__(self, toolmanager)
Gtk.Box.__init__(self)
self.set_property('orientation', Gtk.Orientation.HORIZONTAL)
self._message = Gtk.Label()
self._message.set_justify(Gtk.Justification.RIGHT)
self.pack_end(self._message, False, False, 0)
self.show_all()
self._groups = {}
self._toolitems = {}
def add_toolitem(self, name, group, position, image_file, description,
toggle):
if toggle:
button = Gtk.ToggleToolButton()
else:
button = Gtk.ToolButton()
button.set_label(name)
if image_file is not None:
image = Gtk.Image.new_from_gicon(
Gio.Icon.new_for_string(image_file),
Gtk.IconSize.LARGE_TOOLBAR)
button.set_icon_widget(image)
if position is None:
position = -1
self._add_button(button, group, position)
signal = button.connect('clicked', self._call_tool, name)
button.set_tooltip_text(description)
button.show_all()
self._toolitems.setdefault(name, [])
self._toolitems[name].append((button, signal))
def _add_button(self, button, group, position):
if group not in self._groups:
if self._groups:
self._add_separator()
toolbar = Gtk.Toolbar()
toolbar.set_style(Gtk.ToolbarStyle.ICONS)
self.pack_start(toolbar, False, False, 0)
toolbar.show_all()
self._groups[group] = toolbar
self._groups[group].insert(button, position)
def _call_tool(self, btn, name):
self.trigger_tool(name)
def toggle_toolitem(self, name, toggled):
if name not in self._toolitems:
return
for toolitem, signal in self._toolitems[name]:
toolitem.handler_block(signal)
toolitem.set_active(toggled)
toolitem.handler_unblock(signal)
def remove_toolitem(self, name):
for toolitem, _signal in self._toolitems.pop(name, []):
for group in self._groups:
if toolitem in self._groups[group]:
self._groups[group].remove(toolitem)
def _add_separator(self):
sep = Gtk.Separator()
sep.set_property("orientation", Gtk.Orientation.VERTICAL)
self.pack_start(sep, False, True, 0)
sep.show_all()
def set_message(self, s):
self._message.set_label(s)
@backend_tools._register_tool_class(FigureCanvasGTK3)
class SaveFigureGTK3(backend_tools.SaveFigureBase):
def trigger(self, *args, **kwargs):
NavigationToolbar2GTK3.save_figure(
self._make_classic_style_pseudo_toolbar())
@backend_tools._register_tool_class(FigureCanvasGTK3)
class HelpGTK3(backend_tools.ToolHelpBase):
def _normalize_shortcut(self, key):
"""
Convert Matplotlib key presses to GTK+ accelerator identifiers.
Related to `FigureCanvasGTK3._get_key`.
"""
special = {
'backspace': 'BackSpace',
'pagedown': 'Page_Down',
'pageup': 'Page_Up',
'scroll_lock': 'Scroll_Lock',
}
parts = key.split('+')
mods = ['<' + mod + '>' for mod in parts[:-1]]
key = parts[-1]
if key in special:
key = special[key]
elif len(key) > 1:
key = key.capitalize()
elif key.isupper():
mods += ['<shift>']
return ''.join(mods) + key
def _is_valid_shortcut(self, key):
"""
Check for a valid shortcut to be displayed.
- GTK will never send 'cmd+' (see `FigureCanvasGTK3._get_key`).
- The shortcut window only shows keyboard shortcuts, not mouse buttons.
"""
return 'cmd+' not in key and not key.startswith('MouseButton.')
def _show_shortcuts_window(self):
section = Gtk.ShortcutsSection()
for name, tool in sorted(self.toolmanager.tools.items()):
if not tool.description:
continue
# Putting everything in a separate group allows GTK to
# automatically split them into separate columns/pages, which is
# useful because we have lots of shortcuts, some with many keys
# that are very wide.
group = Gtk.ShortcutsGroup()
section.add(group)
# A hack to remove the title since we have no group naming.
group.forall(lambda widget, data: widget.set_visible(False), None)
shortcut = Gtk.ShortcutsShortcut(
accelerator=' '.join(
self._normalize_shortcut(key)
for key in self.toolmanager.get_tool_keymap(name)
if self._is_valid_shortcut(key)),
title=tool.name,
subtitle=tool.description)
group.add(shortcut)
window = Gtk.ShortcutsWindow(
title='Help',
modal=True,
transient_for=self._figure.canvas.get_toplevel())
section.show() # Must be done explicitly before add!
window.add(section)
window.show_all()
def _show_shortcuts_dialog(self):
dialog = Gtk.MessageDialog(
self._figure.canvas.get_toplevel(),
0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, self._get_help_text(),
title="Help")
dialog.run()
dialog.destroy()
def trigger(self, *args):
if Gtk.check_version(3, 20, 0) is None:
self._show_shortcuts_window()
else:
self._show_shortcuts_dialog()
@backend_tools._register_tool_class(FigureCanvasGTK3)
class ToolCopyToClipboardGTK3(backend_tools.ToolCopyToClipboardBase):
def trigger(self, *args, **kwargs):
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
window = self.canvas.get_window()
x, y, width, height = window.get_geometry()
pb = Gdk.pixbuf_get_from_window(window, x, y, width, height)
clipboard.set_image(pb)
Toolbar = ToolbarGTK3
backend_tools._register_tool_class(
FigureCanvasGTK3, _backend_gtk.ConfigureSubplotsGTK)
backend_tools._register_tool_class(
FigureCanvasGTK3, _backend_gtk.RubberbandGTK)
class FigureManagerGTK3(_FigureManagerGTK):
_toolbar2_class = NavigationToolbar2GTK3
_toolmanager_toolbar_class = ToolbarGTK3
@_BackendGTK.export
class _BackendGTK3(_BackendGTK):
FigureCanvas = FigureCanvasGTK3
FigureManager = FigureManagerGTK3

View File

@ -0,0 +1,74 @@
import numpy as np
from .. import cbook, transforms
from . import backend_agg, backend_gtk3
from .backend_gtk3 import GLib, Gtk, _BackendGTK3
import cairo # Presence of cairo is already checked by _backend_gtk.
class FigureCanvasGTK3Agg(backend_agg.FigureCanvasAgg,
backend_gtk3.FigureCanvasGTK3):
def __init__(self, figure):
super().__init__(figure=figure)
self._bbox_queue = []
def on_draw_event(self, widget, ctx):
if self._idle_draw_id:
GLib.source_remove(self._idle_draw_id)
self._idle_draw_id = 0
self.draw()
scale = self.device_pixel_ratio
allocation = self.get_allocation()
w = allocation.width * scale
h = allocation.height * scale
if not len(self._bbox_queue):
Gtk.render_background(
self.get_style_context(), ctx,
allocation.x, allocation.y,
allocation.width, allocation.height)
bbox_queue = [transforms.Bbox([[0, 0], [w, h]])]
else:
bbox_queue = self._bbox_queue
for bbox in bbox_queue:
x = int(bbox.x0)
y = h - int(bbox.y1)
width = int(bbox.x1) - int(bbox.x0)
height = int(bbox.y1) - int(bbox.y0)
buf = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(
np.asarray(self.copy_from_bbox(bbox)))
image = cairo.ImageSurface.create_for_data(
buf.ravel().data, cairo.FORMAT_ARGB32, width, height)
image.set_device_scale(scale, scale)
ctx.set_source_surface(image, x / scale, y / scale)
ctx.paint()
if len(self._bbox_queue):
self._bbox_queue = []
return False
def blit(self, bbox=None):
# If bbox is None, blit the entire canvas to gtk. Otherwise
# blit only the area defined by the bbox.
if bbox is None:
bbox = self.figure.bbox
scale = self.device_pixel_ratio
allocation = self.get_allocation()
x = int(bbox.x0 / scale)
y = allocation.height - int(bbox.y1 / scale)
width = (int(bbox.x1) - int(bbox.x0)) // scale
height = (int(bbox.y1) - int(bbox.y0)) // scale
self._bbox_queue.append(bbox)
self.queue_draw_area(x, y, width, height)
@_BackendGTK3.export
class _BackendGTK3Cairo(_BackendGTK3):
FigureCanvas = FigureCanvasGTK3Agg

View File

@ -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

View File

@ -0,0 +1,595 @@
import functools
import io
import os
import matplotlib as mpl
from matplotlib import _api, backend_tools, cbook
from matplotlib.backend_bases import (
ToolContainerBase, KeyEvent, LocationEvent, MouseEvent, ResizeEvent,
CloseEvent)
try:
import gi
except ImportError as err:
raise ImportError("The GTK4 backends require PyGObject") from err
try:
# :raises ValueError: If module/version is already loaded, already
# required, or unavailable.
gi.require_version("Gtk", "4.0")
except ValueError as e:
# in this case we want to re-raise as ImportError so the
# auto-backend selection logic correctly skips.
raise ImportError(e) from e
from gi.repository import Gio, GLib, Gtk, Gdk, GdkPixbuf
from . import _backend_gtk
from ._backend_gtk import ( # noqa: F401 # pylint: disable=W0611
_BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
TimerGTK as TimerGTK4,
)
class FigureCanvasGTK4(_FigureCanvasGTK, Gtk.DrawingArea):
required_interactive_framework = "gtk4"
supports_blit = False
manager_class = _api.classproperty(lambda cls: FigureManagerGTK4)
def __init__(self, figure=None):
super().__init__(figure=figure)
self.set_hexpand(True)
self.set_vexpand(True)
self._idle_draw_id = 0
self._rubberband_rect = None
self.set_draw_func(self._draw_func)
self.connect('resize', self.resize_event)
self.connect('notify::scale-factor', self._update_device_pixel_ratio)
click = Gtk.GestureClick()
click.set_button(0) # All buttons.
click.connect('pressed', self.button_press_event)
click.connect('released', self.button_release_event)
self.add_controller(click)
key = Gtk.EventControllerKey()
key.connect('key-pressed', self.key_press_event)
key.connect('key-released', self.key_release_event)
self.add_controller(key)
motion = Gtk.EventControllerMotion()
motion.connect('motion', self.motion_notify_event)
motion.connect('enter', self.enter_notify_event)
motion.connect('leave', self.leave_notify_event)
self.add_controller(motion)
scroll = Gtk.EventControllerScroll.new(
Gtk.EventControllerScrollFlags.VERTICAL)
scroll.connect('scroll', self.scroll_event)
self.add_controller(scroll)
self.set_focusable(True)
css = Gtk.CssProvider()
style = '.matplotlib-canvas { background-color: white; }'
if Gtk.check_version(4, 9, 3) is None:
css.load_from_data(style, -1)
else:
css.load_from_data(style.encode('utf-8'))
style_ctx = self.get_style_context()
style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
style_ctx.add_class("matplotlib-canvas")
def destroy(self):
CloseEvent("close_event", self)._process()
def set_cursor(self, cursor):
# docstring inherited
self.set_cursor_from_name(_backend_gtk.mpl_to_gtk_cursor_name(cursor))
def _mpl_coords(self, xy=None):
"""
Convert the *xy* position of a GTK event, or of the current cursor
position if *xy* is None, to Matplotlib coordinates.
GTK use logical pixels, but the figure is scaled to physical pixels for
rendering. Transform to physical pixels so that all of the down-stream
transforms work as expected.
Also, the origin is different and needs to be corrected.
"""
if xy is None:
surface = self.get_native().get_surface()
is_over, x, y, mask = surface.get_device_position(
self.get_display().get_default_seat().get_pointer())
else:
x, y = xy
x = x * self.device_pixel_ratio
# flip y so y=0 is bottom of canvas
y = self.figure.bbox.height - y * self.device_pixel_ratio
return x, y
def scroll_event(self, controller, dx, dy):
MouseEvent(
"scroll_event", self, *self._mpl_coords(), step=dy,
modifiers=self._mpl_modifiers(controller),
)._process()
return True
def button_press_event(self, controller, n_press, x, y):
MouseEvent(
"button_press_event", self, *self._mpl_coords((x, y)),
controller.get_current_button(),
modifiers=self._mpl_modifiers(controller),
)._process()
self.grab_focus()
def button_release_event(self, controller, n_press, x, y):
MouseEvent(
"button_release_event", self, *self._mpl_coords((x, y)),
controller.get_current_button(),
modifiers=self._mpl_modifiers(controller),
)._process()
def key_press_event(self, controller, keyval, keycode, state):
KeyEvent(
"key_press_event", self, self._get_key(keyval, keycode, state),
*self._mpl_coords(),
)._process()
return True
def key_release_event(self, controller, keyval, keycode, state):
KeyEvent(
"key_release_event", self, self._get_key(keyval, keycode, state),
*self._mpl_coords(),
)._process()
return True
def motion_notify_event(self, controller, x, y):
MouseEvent(
"motion_notify_event", self, *self._mpl_coords((x, y)),
modifiers=self._mpl_modifiers(controller),
)._process()
def enter_notify_event(self, controller, x, y):
LocationEvent(
"figure_enter_event", self, *self._mpl_coords((x, y)),
modifiers=self._mpl_modifiers(),
)._process()
def leave_notify_event(self, controller):
LocationEvent(
"figure_leave_event", self, *self._mpl_coords(),
modifiers=self._mpl_modifiers(),
)._process()
def resize_event(self, area, width, height):
self._update_device_pixel_ratio()
dpi = self.figure.dpi
winch = width * self.device_pixel_ratio / dpi
hinch = height * self.device_pixel_ratio / dpi
self.figure.set_size_inches(winch, hinch, forward=False)
ResizeEvent("resize_event", self)._process()
self.draw_idle()
def _mpl_modifiers(self, controller=None):
if controller is None:
surface = self.get_native().get_surface()
is_over, x, y, event_state = surface.get_device_position(
self.get_display().get_default_seat().get_pointer())
else:
event_state = controller.get_current_event_state()
mod_table = [
("ctrl", Gdk.ModifierType.CONTROL_MASK),
("alt", Gdk.ModifierType.ALT_MASK),
("shift", Gdk.ModifierType.SHIFT_MASK),
("super", Gdk.ModifierType.SUPER_MASK),
]
return [name for name, mask in mod_table if event_state & mask]
def _get_key(self, keyval, keycode, state):
unikey = chr(Gdk.keyval_to_unicode(keyval))
key = cbook._unikey_or_keysym_to_mplkey(
unikey,
Gdk.keyval_name(keyval))
modifiers = [
("ctrl", Gdk.ModifierType.CONTROL_MASK, "control"),
("alt", Gdk.ModifierType.ALT_MASK, "alt"),
("shift", Gdk.ModifierType.SHIFT_MASK, "shift"),
("super", Gdk.ModifierType.SUPER_MASK, "super"),
]
mods = [
mod for mod, mask, mod_key in modifiers
if (mod_key != key and state & mask
and not (mod == "shift" and unikey.isprintable()))]
return "+".join([*mods, key])
def _update_device_pixel_ratio(self, *args, **kwargs):
# We need to be careful in cases with mixed resolution displays if
# device_pixel_ratio changes.
if self._set_device_pixel_ratio(self.get_scale_factor()):
self.draw()
def _draw_rubberband(self, rect):
self._rubberband_rect = rect
# TODO: Only update the rubberband area.
self.queue_draw()
def _draw_func(self, drawing_area, ctx, width, height):
self.on_draw_event(self, ctx)
self._post_draw(self, ctx)
def _post_draw(self, widget, ctx):
if self._rubberband_rect is None:
return
lw = 1
dash = 3
x0, y0, w, h = (dim / self.device_pixel_ratio
for dim in self._rubberband_rect)
x1 = x0 + w
y1 = y0 + h
# Draw the lines from x0, y0 towards x1, y1 so that the
# dashes don't "jump" when moving the zoom box.
ctx.move_to(x0, y0)
ctx.line_to(x0, y1)
ctx.move_to(x0, y0)
ctx.line_to(x1, y0)
ctx.move_to(x0, y1)
ctx.line_to(x1, y1)
ctx.move_to(x1, y0)
ctx.line_to(x1, y1)
ctx.set_antialias(1)
ctx.set_line_width(lw)
ctx.set_dash((dash, dash), 0)
ctx.set_source_rgb(0, 0, 0)
ctx.stroke_preserve()
ctx.set_dash((dash, dash), dash)
ctx.set_source_rgb(1, 1, 1)
ctx.stroke()
def on_draw_event(self, widget, ctx):
# to be overwritten by GTK4Agg or GTK4Cairo
pass
def draw(self):
# docstring inherited
if self.is_drawable():
self.queue_draw()
def draw_idle(self):
# docstring inherited
if self._idle_draw_id != 0:
return
def idle_draw(*args):
try:
self.draw()
finally:
self._idle_draw_id = 0
return False
self._idle_draw_id = GLib.idle_add(idle_draw)
def flush_events(self):
# docstring inherited
context = GLib.MainContext.default()
while context.pending():
context.iteration(True)
class NavigationToolbar2GTK4(_NavigationToolbar2GTK, Gtk.Box):
def __init__(self, canvas):
Gtk.Box.__init__(self)
self.add_css_class('toolbar')
self._gtk_ids = {}
for text, tooltip_text, image_file, callback in self.toolitems:
if text is None:
self.append(Gtk.Separator())
continue
image = Gtk.Image.new_from_gicon(
Gio.Icon.new_for_string(
str(cbook._get_data_path('images',
f'{image_file}-symbolic.svg'))))
self._gtk_ids[text] = button = (
Gtk.ToggleButton() if callback in ['zoom', 'pan'] else
Gtk.Button())
button.set_child(image)
button.add_css_class('flat')
button.add_css_class('image-button')
# Save the handler id, so that we can block it as needed.
button._signal_handler = button.connect(
'clicked', getattr(self, callback))
button.set_tooltip_text(tooltip_text)
self.append(button)
# This filler item ensures the toolbar is always at least two text
# lines high. Otherwise the canvas gets redrawn as the mouse hovers
# over images because those use two-line messages which resize the
# toolbar.
label = Gtk.Label()
label.set_markup(
'<small>\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}</small>')
label.set_hexpand(True) # Push real message to the right.
self.append(label)
self.message = Gtk.Label()
self.message.set_justify(Gtk.Justification.RIGHT)
self.append(self.message)
_NavigationToolbar2GTK.__init__(self, canvas)
def save_figure(self, *args):
dialog = Gtk.FileChooserNative(
title='Save the figure',
transient_for=self.canvas.get_root(),
action=Gtk.FileChooserAction.SAVE,
modal=True)
self._save_dialog = dialog # Must keep a reference.
ff = Gtk.FileFilter()
ff.set_name('All files')
ff.add_pattern('*')
dialog.add_filter(ff)
dialog.set_filter(ff)
formats = []
default_format = None
for i, (name, fmts) in enumerate(
self.canvas.get_supported_filetypes_grouped().items()):
ff = Gtk.FileFilter()
ff.set_name(name)
for fmt in fmts:
ff.add_pattern(f'*.{fmt}')
dialog.add_filter(ff)
formats.append(name)
if self.canvas.get_default_filetype() in fmts:
default_format = i
# Setting the choice doesn't always work, so make sure the default
# format is first.
formats = [formats[default_format], *formats[:default_format],
*formats[default_format+1:]]
dialog.add_choice('format', 'File format', formats, formats)
dialog.set_choice('format', formats[0])
dialog.set_current_folder(Gio.File.new_for_path(
os.path.expanduser(mpl.rcParams['savefig.directory'])))
dialog.set_current_name(self.canvas.get_default_filename())
@functools.partial(dialog.connect, 'response')
def on_response(dialog, response):
file = dialog.get_file()
fmt = dialog.get_choice('format')
fmt = self.canvas.get_supported_filetypes_grouped()[fmt][0]
dialog.destroy()
self._save_dialog = None
if response != Gtk.ResponseType.ACCEPT:
return
# Save dir for next time, unless empty str (which means use cwd).
if mpl.rcParams['savefig.directory']:
parent = file.get_parent()
mpl.rcParams['savefig.directory'] = parent.get_path()
try:
self.canvas.figure.savefig(file.get_path(), format=fmt)
except Exception as e:
msg = Gtk.MessageDialog(
transient_for=self.canvas.get_root(),
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK, modal=True,
text=str(e))
msg.show()
dialog.show()
class ToolbarGTK4(ToolContainerBase, Gtk.Box):
_icon_extension = '-symbolic.svg'
def __init__(self, toolmanager):
ToolContainerBase.__init__(self, toolmanager)
Gtk.Box.__init__(self)
self.set_property('orientation', Gtk.Orientation.HORIZONTAL)
# Tool items are created later, but must appear before the message.
self._tool_box = Gtk.Box()
self.append(self._tool_box)
self._groups = {}
self._toolitems = {}
# This filler item ensures the toolbar is always at least two text
# lines high. Otherwise the canvas gets redrawn as the mouse hovers
# over images because those use two-line messages which resize the
# toolbar.
label = Gtk.Label()
label.set_markup(
'<small>\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}</small>')
label.set_hexpand(True) # Push real message to the right.
self.append(label)
self._message = Gtk.Label()
self._message.set_justify(Gtk.Justification.RIGHT)
self.append(self._message)
def add_toolitem(self, name, group, position, image_file, description,
toggle):
if toggle:
button = Gtk.ToggleButton()
else:
button = Gtk.Button()
button.set_label(name)
button.add_css_class('flat')
if image_file is not None:
image = Gtk.Image.new_from_gicon(
Gio.Icon.new_for_string(image_file))
button.set_child(image)
button.add_css_class('image-button')
if position is None:
position = -1
self._add_button(button, group, position)
signal = button.connect('clicked', self._call_tool, name)
button.set_tooltip_text(description)
self._toolitems.setdefault(name, [])
self._toolitems[name].append((button, signal))
def _find_child_at_position(self, group, position):
children = [None]
child = self._groups[group].get_first_child()
while child is not None:
children.append(child)
child = child.get_next_sibling()
return children[position]
def _add_button(self, button, group, position):
if group not in self._groups:
if self._groups:
self._add_separator()
group_box = Gtk.Box()
self._tool_box.append(group_box)
self._groups[group] = group_box
self._groups[group].insert_child_after(
button, self._find_child_at_position(group, position))
def _call_tool(self, btn, name):
self.trigger_tool(name)
def toggle_toolitem(self, name, toggled):
if name not in self._toolitems:
return
for toolitem, signal in self._toolitems[name]:
toolitem.handler_block(signal)
toolitem.set_active(toggled)
toolitem.handler_unblock(signal)
def remove_toolitem(self, name):
for toolitem, _signal in self._toolitems.pop(name, []):
for group in self._groups:
if toolitem in self._groups[group]:
self._groups[group].remove(toolitem)
def _add_separator(self):
sep = Gtk.Separator()
sep.set_property("orientation", Gtk.Orientation.VERTICAL)
self._tool_box.append(sep)
def set_message(self, s):
self._message.set_label(s)
@backend_tools._register_tool_class(FigureCanvasGTK4)
class SaveFigureGTK4(backend_tools.SaveFigureBase):
def trigger(self, *args, **kwargs):
NavigationToolbar2GTK4.save_figure(
self._make_classic_style_pseudo_toolbar())
@backend_tools._register_tool_class(FigureCanvasGTK4)
class HelpGTK4(backend_tools.ToolHelpBase):
def _normalize_shortcut(self, key):
"""
Convert Matplotlib key presses to GTK+ accelerator identifiers.
Related to `FigureCanvasGTK4._get_key`.
"""
special = {
'backspace': 'BackSpace',
'pagedown': 'Page_Down',
'pageup': 'Page_Up',
'scroll_lock': 'Scroll_Lock',
}
parts = key.split('+')
mods = ['<' + mod + '>' for mod in parts[:-1]]
key = parts[-1]
if key in special:
key = special[key]
elif len(key) > 1:
key = key.capitalize()
elif key.isupper():
mods += ['<shift>']
return ''.join(mods) + key
def _is_valid_shortcut(self, key):
"""
Check for a valid shortcut to be displayed.
- GTK will never send 'cmd+' (see `FigureCanvasGTK4._get_key`).
- The shortcut window only shows keyboard shortcuts, not mouse buttons.
"""
return 'cmd+' not in key and not key.startswith('MouseButton.')
def trigger(self, *args):
section = Gtk.ShortcutsSection()
for name, tool in sorted(self.toolmanager.tools.items()):
if not tool.description:
continue
# Putting everything in a separate group allows GTK to
# automatically split them into separate columns/pages, which is
# useful because we have lots of shortcuts, some with many keys
# that are very wide.
group = Gtk.ShortcutsGroup()
section.append(group)
# A hack to remove the title since we have no group naming.
child = group.get_first_child()
while child is not None:
child.set_visible(False)
child = child.get_next_sibling()
shortcut = Gtk.ShortcutsShortcut(
accelerator=' '.join(
self._normalize_shortcut(key)
for key in self.toolmanager.get_tool_keymap(name)
if self._is_valid_shortcut(key)),
title=tool.name,
subtitle=tool.description)
group.append(shortcut)
window = Gtk.ShortcutsWindow(
title='Help',
modal=True,
transient_for=self._figure.canvas.get_root())
window.set_child(section)
window.show()
@backend_tools._register_tool_class(FigureCanvasGTK4)
class ToolCopyToClipboardGTK4(backend_tools.ToolCopyToClipboardBase):
def trigger(self, *args, **kwargs):
with io.BytesIO() as f:
self.canvas.print_rgba(f)
w, h = self.canvas.get_width_height()
pb = GdkPixbuf.Pixbuf.new_from_data(f.getbuffer(),
GdkPixbuf.Colorspace.RGB, True,
8, w, h, w*4)
clipboard = self.canvas.get_clipboard()
clipboard.set(pb)
backend_tools._register_tool_class(
FigureCanvasGTK4, _backend_gtk.ConfigureSubplotsGTK)
backend_tools._register_tool_class(
FigureCanvasGTK4, _backend_gtk.RubberbandGTK)
Toolbar = ToolbarGTK4
class FigureManagerGTK4(_FigureManagerGTK):
_toolbar2_class = NavigationToolbar2GTK4
_toolmanager_toolbar_class = ToolbarGTK4
@_BackendGTK.export
class _BackendGTK4(_BackendGTK):
FigureCanvas = FigureCanvasGTK4
FigureManager = FigureManagerGTK4

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,195 @@
import os
import matplotlib as mpl
from matplotlib import _api, cbook
from matplotlib._pylab_helpers import Gcf
from . import _macosx
from .backend_agg import FigureCanvasAgg
from matplotlib.backend_bases import (
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
ResizeEvent, TimerBase, _allow_interrupt)
class TimerMac(_macosx.Timer, TimerBase):
"""Subclass of `.TimerBase` using CFRunLoop timer events."""
# completely implemented at the C-level (in _macosx.Timer)
def _allow_interrupt_macos():
"""A context manager that allows terminating a plot by sending a SIGINT."""
return _allow_interrupt(
lambda rsock: _macosx.wake_on_fd_write(rsock.fileno()), _macosx.stop)
class FigureCanvasMac(FigureCanvasAgg, _macosx.FigureCanvas, FigureCanvasBase):
# docstring inherited
# Ideally this class would be `class FCMacAgg(FCAgg, FCMac)`
# (FC=FigureCanvas) where FCMac would be an ObjC-implemented mac-specific
# class also inheriting from FCBase (this is the approach with other GUI
# toolkits). However, writing an extension type inheriting from a Python
# base class is slightly tricky (the extension type must be a heap type),
# and we can just as well lift the FCBase base up one level, keeping it *at
# the end* to have the right method resolution order.
# Events such as button presses, mouse movements, and key presses are
# handled in C and events (MouseEvent, etc.) are triggered from there.
required_interactive_framework = "macosx"
_timer_cls = TimerMac
manager_class = _api.classproperty(lambda cls: FigureManagerMac)
def __init__(self, figure):
super().__init__(figure=figure)
self._draw_pending = False
self._is_drawing = False
# Keep track of the timers that are alive
self._timers = set()
def draw(self):
"""Render the figure and update the macosx canvas."""
# The renderer draw is done here; delaying causes problems with code
# that uses the result of the draw() to update plot elements.
if self._is_drawing:
return
with cbook._setattr_cm(self, _is_drawing=True):
super().draw()
self.update()
def draw_idle(self):
# docstring inherited
if not (getattr(self, '_draw_pending', False) or
getattr(self, '_is_drawing', False)):
self._draw_pending = True
# Add a singleshot timer to the eventloop that will call back
# into the Python method _draw_idle to take care of the draw
self._single_shot_timer(self._draw_idle)
def _single_shot_timer(self, callback):
"""Add a single shot timer with the given callback"""
def callback_func(callback, timer):
callback()
self._timers.remove(timer)
timer = self.new_timer(interval=0)
timer.single_shot = True
timer.add_callback(callback_func, callback, timer)
self._timers.add(timer)
timer.start()
def _draw_idle(self):
"""
Draw method for singleshot timer
This draw method can be added to a singleshot timer, which can
accumulate draws while the eventloop is spinning. This method will
then only draw the first time and short-circuit the others.
"""
with self._idle_draw_cntx():
if not self._draw_pending:
# Short-circuit because our draw request has already been
# taken care of
return
self._draw_pending = False
self.draw()
def blit(self, bbox=None):
# docstring inherited
super().blit(bbox)
self.update()
def resize(self, width, height):
# Size from macOS is logical pixels, dpi is physical.
scale = self.figure.dpi / self.device_pixel_ratio
width /= scale
height /= scale
self.figure.set_size_inches(width, height, forward=False)
ResizeEvent("resize_event", self)._process()
self.draw_idle()
def start_event_loop(self, timeout=0):
# docstring inherited
# Set up a SIGINT handler to allow terminating a plot via CTRL-C.
with _allow_interrupt_macos():
self._start_event_loop(timeout=timeout) # Forward to ObjC implementation.
class NavigationToolbar2Mac(_macosx.NavigationToolbar2, NavigationToolbar2):
def __init__(self, canvas):
data_path = cbook._get_data_path('images')
_, tooltips, image_names, _ = zip(*NavigationToolbar2.toolitems)
_macosx.NavigationToolbar2.__init__(
self, canvas,
tuple(str(data_path / image_name) + ".pdf"
for image_name in image_names if image_name is not None),
tuple(tooltip for tooltip in tooltips if tooltip is not None))
NavigationToolbar2.__init__(self, canvas)
def draw_rubberband(self, event, x0, y0, x1, y1):
self.canvas.set_rubberband(int(x0), int(y0), int(x1), int(y1))
def remove_rubberband(self):
self.canvas.remove_rubberband()
def save_figure(self, *args):
directory = os.path.expanduser(mpl.rcParams['savefig.directory'])
filename = _macosx.choose_save_file('Save the figure',
directory,
self.canvas.get_default_filename())
if filename is None: # Cancel
return
# Save dir for next time, unless empty str (which means use cwd).
if mpl.rcParams['savefig.directory']:
mpl.rcParams['savefig.directory'] = os.path.dirname(filename)
self.canvas.figure.savefig(filename)
class FigureManagerMac(_macosx.FigureManager, FigureManagerBase):
_toolbar2_class = NavigationToolbar2Mac
def __init__(self, canvas, num):
self._shown = False
_macosx.FigureManager.__init__(self, canvas)
icon_path = str(cbook._get_data_path('images/matplotlib.pdf'))
_macosx.FigureManager.set_icon(icon_path)
FigureManagerBase.__init__(self, canvas, num)
self._set_window_mode(mpl.rcParams["macosx.window_mode"])
if self.toolbar is not None:
self.toolbar.update()
if mpl.is_interactive():
self.show()
self.canvas.draw_idle()
def _close_button_pressed(self):
Gcf.destroy(self)
self.canvas.flush_events()
def destroy(self):
# We need to clear any pending timers that never fired, otherwise
# we get a memory leak from the timer callbacks holding a reference
while self.canvas._timers:
timer = self.canvas._timers.pop()
timer.stop()
super().destroy()
@classmethod
def start_main_loop(cls):
# Set up a SIGINT handler to allow terminating a plot via CTRL-C.
with _allow_interrupt_macos():
_macosx.show()
def show(self):
if self.canvas.figure.stale:
self.canvas.draw_idle()
if not self._shown:
self._show()
self._shown = True
if mpl.rcParams["figure.raise_window"]:
self._raise()
@_Backend.export
class _BackendMac(_Backend):
FigureCanvas = FigureCanvasMac
FigureManager = FigureManagerMac
mainloop = FigureManagerMac.start_main_loop

View File

@ -0,0 +1,119 @@
import numpy as np
from matplotlib import cbook
from .backend_agg import RendererAgg
from matplotlib._tight_bbox import process_figure_for_rasterizing
class MixedModeRenderer:
"""
A helper class to implement a renderer that switches between
vector and raster drawing. An example may be a PDF writer, where
most things are drawn with PDF vector commands, but some very
complex objects, such as quad meshes, are rasterised and then
output as images.
"""
def __init__(self, figure, width, height, dpi, vector_renderer,
raster_renderer_class=None,
bbox_inches_restore=None):
"""
Parameters
----------
figure : `~matplotlib.figure.Figure`
The figure instance.
width : scalar
The width of the canvas in logical units
height : scalar
The height of the canvas in logical units
dpi : float
The dpi of the canvas
vector_renderer : `~matplotlib.backend_bases.RendererBase`
An instance of a subclass of
`~matplotlib.backend_bases.RendererBase` that will be used for the
vector drawing.
raster_renderer_class : `~matplotlib.backend_bases.RendererBase`
The renderer class to use for the raster drawing. If not provided,
this will use the Agg backend (which is currently the only viable
option anyway.)
"""
if raster_renderer_class is None:
raster_renderer_class = RendererAgg
self._raster_renderer_class = raster_renderer_class
self._width = width
self._height = height
self.dpi = dpi
self._vector_renderer = vector_renderer
self._raster_renderer = None
# A reference to the figure is needed as we need to change
# the figure dpi before and after the rasterization. Although
# this looks ugly, I couldn't find a better solution. -JJL
self.figure = figure
self._figdpi = figure.dpi
self._bbox_inches_restore = bbox_inches_restore
self._renderer = vector_renderer
def __getattr__(self, attr):
# Proxy everything that hasn't been overridden to the base
# renderer. Things that *are* overridden can call methods
# on self._renderer directly, but must not cache/store
# methods (because things like RendererAgg change their
# methods on the fly in order to optimise proxying down
# to the underlying C implementation).
return getattr(self._renderer, attr)
def start_rasterizing(self):
"""
Enter "raster" mode. All subsequent drawing commands (until
`stop_rasterizing` is called) will be drawn with the raster backend.
"""
# change the dpi of the figure temporarily.
self.figure.dpi = self.dpi
if self._bbox_inches_restore: # when tight bbox is used
r = process_figure_for_rasterizing(self.figure,
self._bbox_inches_restore)
self._bbox_inches_restore = r
self._raster_renderer = self._raster_renderer_class(
self._width*self.dpi, self._height*self.dpi, self.dpi)
self._renderer = self._raster_renderer
def stop_rasterizing(self):
"""
Exit "raster" mode. All of the drawing that was done since
the last `start_rasterizing` call will be copied to the
vector backend by calling draw_image.
"""
self._renderer = self._vector_renderer
height = self._height * self.dpi
img = np.asarray(self._raster_renderer.buffer_rgba())
slice_y, slice_x = cbook._get_nonzero_slices(img[..., 3])
cropped_img = img[slice_y, slice_x]
if cropped_img.size:
gc = self._renderer.new_gc()
# TODO: If the mixedmode resolution differs from the figure's
# dpi, the image must be scaled (dpi->_figdpi). Not all
# backends support this.
self._renderer.draw_image(
gc,
slice_x.start * self._figdpi / self.dpi,
(height - slice_y.stop) * self._figdpi / self.dpi,
cropped_img[::-1])
self._raster_renderer = None
# restore the figure dpi.
self.figure.dpi = self._figdpi
if self._bbox_inches_restore: # when tight bbox is used
r = process_figure_for_rasterizing(self.figure,
self._bbox_inches_restore,
self._figdpi)
self._bbox_inches_restore = r

View File

@ -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

View File

@ -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}")

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,328 @@
"""Displays Agg images in the browser, with interactivity."""
# The WebAgg backend is divided into two modules:
#
# - `backend_webagg_core.py` contains code necessary to embed a WebAgg
# plot inside of a web application, and communicate in an abstract
# way over a web socket.
#
# - `backend_webagg.py` contains a concrete implementation of a basic
# application, implemented with tornado.
from contextlib import contextmanager
import errno
from io import BytesIO
import json
import mimetypes
from pathlib import Path
import random
import sys
import signal
import threading
try:
import tornado
except ImportError as err:
raise RuntimeError("The WebAgg backend requires Tornado.") from err
import tornado.web
import tornado.ioloop
import tornado.websocket
import matplotlib as mpl
from matplotlib.backend_bases import _Backend
from matplotlib._pylab_helpers import Gcf
from . import backend_webagg_core as core
from .backend_webagg_core import ( # noqa: F401 # pylint: disable=W0611
TimerAsyncio, TimerTornado)
webagg_server_thread = threading.Thread(
target=lambda: tornado.ioloop.IOLoop.instance().start())
class FigureManagerWebAgg(core.FigureManagerWebAgg):
_toolbar2_class = core.NavigationToolbar2WebAgg
@classmethod
def pyplot_show(cls, *, block=None):
WebAggApplication.initialize()
url = "http://{address}:{port}{prefix}".format(
address=WebAggApplication.address,
port=WebAggApplication.port,
prefix=WebAggApplication.url_prefix)
if mpl.rcParams['webagg.open_in_browser']:
import webbrowser
if not webbrowser.open(url):
print(f"To view figure, visit {url}")
else:
print(f"To view figure, visit {url}")
WebAggApplication.start()
class FigureCanvasWebAgg(core.FigureCanvasWebAggCore):
manager_class = FigureManagerWebAgg
class WebAggApplication(tornado.web.Application):
initialized = False
started = False
class FavIcon(tornado.web.RequestHandler):
def get(self):
self.set_header('Content-Type', 'image/png')
self.write(Path(mpl.get_data_path(),
'images/matplotlib.png').read_bytes())
class SingleFigurePage(tornado.web.RequestHandler):
def __init__(self, application, request, *, url_prefix='', **kwargs):
self.url_prefix = url_prefix
super().__init__(application, request, **kwargs)
def get(self, fignum):
fignum = int(fignum)
manager = Gcf.get_fig_manager(fignum)
ws_uri = f'ws://{self.request.host}{self.url_prefix}/'
self.render(
"single_figure.html",
prefix=self.url_prefix,
ws_uri=ws_uri,
fig_id=fignum,
toolitems=core.NavigationToolbar2WebAgg.toolitems,
canvas=manager.canvas)
class AllFiguresPage(tornado.web.RequestHandler):
def __init__(self, application, request, *, url_prefix='', **kwargs):
self.url_prefix = url_prefix
super().__init__(application, request, **kwargs)
def get(self):
ws_uri = f'ws://{self.request.host}{self.url_prefix}/'
self.render(
"all_figures.html",
prefix=self.url_prefix,
ws_uri=ws_uri,
figures=sorted(Gcf.figs.items()),
toolitems=core.NavigationToolbar2WebAgg.toolitems)
class MplJs(tornado.web.RequestHandler):
def get(self):
self.set_header('Content-Type', 'application/javascript')
js_content = core.FigureManagerWebAgg.get_javascript()
self.write(js_content)
class Download(tornado.web.RequestHandler):
def get(self, fignum, fmt):
fignum = int(fignum)
manager = Gcf.get_fig_manager(fignum)
self.set_header(
'Content-Type', mimetypes.types_map.get(fmt, 'binary'))
buff = BytesIO()
manager.canvas.figure.savefig(buff, format=fmt)
self.write(buff.getvalue())
class WebSocket(tornado.websocket.WebSocketHandler):
supports_binary = True
def open(self, fignum):
self.fignum = int(fignum)
self.manager = Gcf.get_fig_manager(self.fignum)
self.manager.add_web_socket(self)
if hasattr(self, 'set_nodelay'):
self.set_nodelay(True)
def on_close(self):
self.manager.remove_web_socket(self)
def on_message(self, message):
message = json.loads(message)
# The 'supports_binary' message is on a client-by-client
# basis. The others affect the (shared) canvas as a
# whole.
if message['type'] == 'supports_binary':
self.supports_binary = message['value']
else:
manager = Gcf.get_fig_manager(self.fignum)
# It is possible for a figure to be closed,
# but a stale figure UI is still sending messages
# from the browser.
if manager is not None:
manager.handle_json(message)
def send_json(self, content):
self.write_message(json.dumps(content))
def send_binary(self, blob):
if self.supports_binary:
self.write_message(blob, binary=True)
else:
data_uri = "data:image/png;base64,{}".format(
blob.encode('base64').replace('\n', ''))
self.write_message(data_uri)
def __init__(self, url_prefix=''):
if url_prefix:
assert url_prefix[0] == '/' and url_prefix[-1] != '/', \
'url_prefix must start with a "/" and not end with one.'
super().__init__(
[
# Static files for the CSS and JS
(url_prefix + r'/_static/(.*)',
tornado.web.StaticFileHandler,
{'path': core.FigureManagerWebAgg.get_static_file_path()}),
# Static images for the toolbar
(url_prefix + r'/_images/(.*)',
tornado.web.StaticFileHandler,
{'path': Path(mpl.get_data_path(), 'images')}),
# A Matplotlib favicon
(url_prefix + r'/favicon.ico', self.FavIcon),
# The page that contains all of the pieces
(url_prefix + r'/([0-9]+)', self.SingleFigurePage,
{'url_prefix': url_prefix}),
# The page that contains all of the figures
(url_prefix + r'/?', self.AllFiguresPage,
{'url_prefix': url_prefix}),
(url_prefix + r'/js/mpl.js', self.MplJs),
# Sends images and events to the browser, and receives
# events from the browser
(url_prefix + r'/([0-9]+)/ws', self.WebSocket),
# Handles the downloading (i.e., saving) of static images
(url_prefix + r'/([0-9]+)/download.([a-z0-9.]+)',
self.Download),
],
template_path=core.FigureManagerWebAgg.get_static_file_path())
@classmethod
def initialize(cls, url_prefix='', port=None, address=None):
if cls.initialized:
return
# Create the class instance
app = cls(url_prefix=url_prefix)
cls.url_prefix = url_prefix
# This port selection algorithm is borrowed, more or less
# verbatim, from IPython.
def random_ports(port, n):
"""
Generate a list of n random ports near the given port.
The first 5 ports will be sequential, and the remaining n-5 will be
randomly selected in the range [port-2*n, port+2*n].
"""
for i in range(min(5, n)):
yield port + i
for i in range(n - 5):
yield port + random.randint(-2 * n, 2 * n)
if address is None:
cls.address = mpl.rcParams['webagg.address']
else:
cls.address = address
cls.port = mpl.rcParams['webagg.port']
for port in random_ports(cls.port,
mpl.rcParams['webagg.port_retries']):
try:
app.listen(port, cls.address)
except OSError as e:
if e.errno != errno.EADDRINUSE:
raise
else:
cls.port = port
break
else:
raise SystemExit(
"The webagg server could not be started because an available "
"port could not be found")
cls.initialized = True
@classmethod
def start(cls):
import asyncio
try:
asyncio.get_running_loop()
except RuntimeError:
pass
else:
cls.started = True
if cls.started:
return
"""
IOLoop.running() was removed as of Tornado 2.4; see for example
https://groups.google.com/forum/#!topic/python-tornado/QLMzkpQBGOY
Thus there is no correct way to check if the loop has already been
launched. We may end up with two concurrently running loops in that
unlucky case with all the expected consequences.
"""
ioloop = tornado.ioloop.IOLoop.instance()
def shutdown():
ioloop.stop()
print("Server is stopped")
sys.stdout.flush()
cls.started = False
@contextmanager
def catch_sigint():
old_handler = signal.signal(
signal.SIGINT,
lambda sig, frame: ioloop.add_callback_from_signal(shutdown))
try:
yield
finally:
signal.signal(signal.SIGINT, old_handler)
# Set the flag to True *before* blocking on ioloop.start()
cls.started = True
print("Press Ctrl+C to stop WebAgg server")
sys.stdout.flush()
with catch_sigint():
ioloop.start()
def ipython_inline_display(figure):
import tornado.template
WebAggApplication.initialize()
import asyncio
try:
asyncio.get_running_loop()
except RuntimeError:
if not webagg_server_thread.is_alive():
webagg_server_thread.start()
fignum = figure.number
tpl = Path(core.FigureManagerWebAgg.get_static_file_path(),
"ipython_inline_figure.html").read_text()
t = tornado.template.Template(tpl)
return t.generate(
prefix=WebAggApplication.url_prefix,
fig_id=fignum,
toolitems=core.NavigationToolbar2WebAgg.toolitems,
canvas=figure.canvas,
port=WebAggApplication.port).decode('utf-8')
@_Backend.export
class _BackendWebAgg(_Backend):
FigureCanvas = FigureCanvasWebAgg
FigureManager = FigureManagerWebAgg

View File

@ -0,0 +1,517 @@
"""Displays Agg images in the browser, with interactivity."""
# The WebAgg backend is divided into two modules:
#
# - `backend_webagg_core.py` contains code necessary to embed a WebAgg
# plot inside of a web application, and communicate in an abstract
# way over a web socket.
#
# - `backend_webagg.py` contains a concrete implementation of a basic
# application, implemented with asyncio.
import asyncio
import datetime
from io import BytesIO, StringIO
import json
import logging
import os
from pathlib import Path
import numpy as np
from PIL import Image
from matplotlib import _api, backend_bases, backend_tools
from matplotlib.backends import backend_agg
from matplotlib.backend_bases import (
_Backend, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
_log = logging.getLogger(__name__)
_SPECIAL_KEYS_LUT = {'Alt': 'alt',
'AltGraph': 'alt',
'CapsLock': 'caps_lock',
'Control': 'control',
'Meta': 'meta',
'NumLock': 'num_lock',
'ScrollLock': 'scroll_lock',
'Shift': 'shift',
'Super': 'super',
'Enter': 'enter',
'Tab': 'tab',
'ArrowDown': 'down',
'ArrowLeft': 'left',
'ArrowRight': 'right',
'ArrowUp': 'up',
'End': 'end',
'Home': 'home',
'PageDown': 'pagedown',
'PageUp': 'pageup',
'Backspace': 'backspace',
'Delete': 'delete',
'Insert': 'insert',
'Escape': 'escape',
'Pause': 'pause',
'Select': 'select',
'Dead': 'dead',
'F1': 'f1',
'F2': 'f2',
'F3': 'f3',
'F4': 'f4',
'F5': 'f5',
'F6': 'f6',
'F7': 'f7',
'F8': 'f8',
'F9': 'f9',
'F10': 'f10',
'F11': 'f11',
'F12': 'f12'}
def _handle_key(key):
"""Handle key values"""
value = key[key.index('k') + 1:]
if 'shift+' in key:
if len(value) == 1:
key = key.replace('shift+', '')
if value in _SPECIAL_KEYS_LUT:
value = _SPECIAL_KEYS_LUT[value]
key = key[:key.index('k')] + value
return key
class TimerTornado(backend_bases.TimerBase):
def __init__(self, *args, **kwargs):
self._timer = None
super().__init__(*args, **kwargs)
def _timer_start(self):
import tornado
self._timer_stop()
if self._single:
ioloop = tornado.ioloop.IOLoop.instance()
self._timer = ioloop.add_timeout(
datetime.timedelta(milliseconds=self.interval),
self._on_timer)
else:
self._timer = tornado.ioloop.PeriodicCallback(
self._on_timer,
max(self.interval, 1e-6))
self._timer.start()
def _timer_stop(self):
import tornado
if self._timer is None:
return
elif self._single:
ioloop = tornado.ioloop.IOLoop.instance()
ioloop.remove_timeout(self._timer)
else:
self._timer.stop()
self._timer = None
def _timer_set_interval(self):
# Only stop and restart it if the timer has already been started
if self._timer is not None:
self._timer_stop()
self._timer_start()
class TimerAsyncio(backend_bases.TimerBase):
def __init__(self, *args, **kwargs):
self._task = None
super().__init__(*args, **kwargs)
async def _timer_task(self, interval):
while True:
try:
await asyncio.sleep(interval)
self._on_timer()
if self._single:
break
except asyncio.CancelledError:
break
def _timer_start(self):
self._timer_stop()
self._task = asyncio.ensure_future(
self._timer_task(max(self.interval / 1_000., 1e-6))
)
def _timer_stop(self):
if self._task is not None:
self._task.cancel()
self._task = None
def _timer_set_interval(self):
# Only stop and restart it if the timer has already been started
if self._task is not None:
self._timer_stop()
self._timer_start()
class FigureCanvasWebAggCore(backend_agg.FigureCanvasAgg):
manager_class = _api.classproperty(lambda cls: FigureManagerWebAgg)
_timer_cls = TimerAsyncio
# Webagg and friends having the right methods, but still
# having bugs in practice. Do not advertise that it works until
# we can debug this.
supports_blit = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Set to True when the renderer contains data that is newer
# than the PNG buffer.
self._png_is_old = True
# Set to True by the `refresh` message so that the next frame
# sent to the clients will be a full frame.
self._force_full = True
# The last buffer, for diff mode.
self._last_buff = np.empty((0, 0))
# Store the current image mode so that at any point, clients can
# request the information. This should be changed by calling
# self.set_image_mode(mode) so that the notification can be given
# to the connected clients.
self._current_image_mode = 'full'
# Track mouse events to fill in the x, y position of key events.
self._last_mouse_xy = (None, None)
def show(self):
# show the figure window
from matplotlib.pyplot import show
show()
def draw(self):
self._png_is_old = True
try:
super().draw()
finally:
self.manager.refresh_all() # Swap the frames.
def blit(self, bbox=None):
self._png_is_old = True
self.manager.refresh_all()
def draw_idle(self):
self.send_event("draw")
def set_cursor(self, cursor):
# docstring inherited
cursor = _api.check_getitem({
backend_tools.Cursors.HAND: 'pointer',
backend_tools.Cursors.POINTER: 'default',
backend_tools.Cursors.SELECT_REGION: 'crosshair',
backend_tools.Cursors.MOVE: 'move',
backend_tools.Cursors.WAIT: 'wait',
backend_tools.Cursors.RESIZE_HORIZONTAL: 'ew-resize',
backend_tools.Cursors.RESIZE_VERTICAL: 'ns-resize',
}, cursor=cursor)
self.send_event('cursor', cursor=cursor)
def set_image_mode(self, mode):
"""
Set the image mode for any subsequent images which will be sent
to the clients. The modes may currently be either 'full' or 'diff'.
Note: diff images may not contain transparency, therefore upon
draw this mode may be changed if the resulting image has any
transparent component.
"""
_api.check_in_list(['full', 'diff'], mode=mode)
if self._current_image_mode != mode:
self._current_image_mode = mode
self.handle_send_image_mode(None)
def get_diff_image(self):
if self._png_is_old:
renderer = self.get_renderer()
pixels = np.asarray(renderer.buffer_rgba())
# The buffer is created as type uint32 so that entire
# pixels can be compared in one numpy call, rather than
# needing to compare each plane separately.
buff = pixels.view(np.uint32).squeeze(2)
if (self._force_full
# If the buffer has changed size we need to do a full draw.
or buff.shape != self._last_buff.shape
# If any pixels have transparency, we need to force a full
# draw as we cannot overlay new on top of old.
or (pixels[:, :, 3] != 255).any()):
self.set_image_mode('full')
output = buff
else:
self.set_image_mode('diff')
diff = buff != self._last_buff
output = np.where(diff, buff, 0)
# Store the current buffer so we can compute the next diff.
self._last_buff = buff.copy()
self._force_full = False
self._png_is_old = False
data = output.view(dtype=np.uint8).reshape((*output.shape, 4))
with BytesIO() as png:
Image.fromarray(data).save(png, format="png")
return png.getvalue()
def handle_event(self, event):
e_type = event['type']
handler = getattr(self, f'handle_{e_type}',
self.handle_unknown_event)
return handler(event)
def handle_unknown_event(self, event):
_log.warning('Unhandled message type %s. %s', event["type"], event)
def handle_ack(self, event):
# Network latency tends to decrease if traffic is flowing
# in both directions. Therefore, the browser sends back
# an "ack" message after each image frame is received.
# This could also be used as a simple sanity check in the
# future, but for now the performance increase is enough
# to justify it, even if the server does nothing with it.
pass
def handle_draw(self, event):
self.draw()
def _handle_mouse(self, event):
x = event['x']
y = event['y']
y = self.get_renderer().height - y
self._last_mouse_xy = x, y
# JavaScript button numbers and Matplotlib button numbers are off by 1.
button = event['button'] + 1
e_type = event['type']
modifiers = event['modifiers']
guiEvent = event.get('guiEvent')
if e_type in ['button_press', 'button_release']:
MouseEvent(e_type + '_event', self, x, y, button,
modifiers=modifiers, guiEvent=guiEvent)._process()
elif e_type == 'dblclick':
MouseEvent('button_press_event', self, x, y, button, dblclick=True,
modifiers=modifiers, guiEvent=guiEvent)._process()
elif e_type == 'scroll':
MouseEvent('scroll_event', self, x, y, step=event['step'],
modifiers=modifiers, guiEvent=guiEvent)._process()
elif e_type == 'motion_notify':
MouseEvent(e_type + '_event', self, x, y,
modifiers=modifiers, guiEvent=guiEvent)._process()
elif e_type in ['figure_enter', 'figure_leave']:
LocationEvent(e_type + '_event', self, x, y,
modifiers=modifiers, guiEvent=guiEvent)._process()
handle_button_press = handle_button_release = handle_dblclick = \
handle_figure_enter = handle_figure_leave = handle_motion_notify = \
handle_scroll = _handle_mouse
def _handle_key(self, event):
KeyEvent(event['type'] + '_event', self,
_handle_key(event['key']), *self._last_mouse_xy,
guiEvent=event.get('guiEvent'))._process()
handle_key_press = handle_key_release = _handle_key
def handle_toolbar_button(self, event):
# TODO: Be more suspicious of the input
getattr(self.toolbar, event['name'])()
def handle_refresh(self, event):
figure_label = self.figure.get_label()
if not figure_label:
figure_label = f"Figure {self.manager.num}"
self.send_event('figure_label', label=figure_label)
self._force_full = True
if self.toolbar:
# Normal toolbar init would refresh this, but it happens before the
# browser canvas is set up.
self.toolbar.set_history_buttons()
self.draw_idle()
def handle_resize(self, event):
x = int(event.get('width', 800)) * self.device_pixel_ratio
y = int(event.get('height', 800)) * self.device_pixel_ratio
fig = self.figure
# An attempt at approximating the figure size in pixels.
fig.set_size_inches(x / fig.dpi, y / fig.dpi, forward=False)
# Acknowledge the resize, and force the viewer to update the
# canvas size to the figure's new size (which is hopefully
# identical or within a pixel or so).
self._png_is_old = True
self.manager.resize(*fig.bbox.size, forward=False)
ResizeEvent('resize_event', self)._process()
self.draw_idle()
def handle_send_image_mode(self, event):
# The client requests notification of what the current image mode is.
self.send_event('image_mode', mode=self._current_image_mode)
def handle_set_device_pixel_ratio(self, event):
self._handle_set_device_pixel_ratio(event.get('device_pixel_ratio', 1))
def handle_set_dpi_ratio(self, event):
# This handler is for backwards-compatibility with older ipympl.
self._handle_set_device_pixel_ratio(event.get('dpi_ratio', 1))
def _handle_set_device_pixel_ratio(self, device_pixel_ratio):
if self._set_device_pixel_ratio(device_pixel_ratio):
self._force_full = True
self.draw_idle()
def send_event(self, event_type, **kwargs):
if self.manager:
self.manager._send_event(event_type, **kwargs)
_ALLOWED_TOOL_ITEMS = {
'home',
'back',
'forward',
'pan',
'zoom',
'download',
None,
}
class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2):
# Use the standard toolbar items + download button
toolitems = [
(text, tooltip_text, image_file, name_of_method)
for text, tooltip_text, image_file, name_of_method
in (*backend_bases.NavigationToolbar2.toolitems,
('Download', 'Download plot', 'filesave', 'download'))
if name_of_method in _ALLOWED_TOOL_ITEMS
]
def __init__(self, canvas):
self.message = ''
super().__init__(canvas)
def set_message(self, message):
if message != self.message:
self.canvas.send_event("message", message=message)
self.message = message
def draw_rubberband(self, event, x0, y0, x1, y1):
self.canvas.send_event("rubberband", x0=x0, y0=y0, x1=x1, y1=y1)
def remove_rubberband(self):
self.canvas.send_event("rubberband", x0=-1, y0=-1, x1=-1, y1=-1)
def save_figure(self, *args):
"""Save the current figure"""
self.canvas.send_event('save')
def pan(self):
super().pan()
self.canvas.send_event('navigate_mode', mode=self.mode.name)
def zoom(self):
super().zoom()
self.canvas.send_event('navigate_mode', mode=self.mode.name)
def set_history_buttons(self):
can_backward = self._nav_stack._pos > 0
can_forward = self._nav_stack._pos < len(self._nav_stack) - 1
self.canvas.send_event('history_buttons',
Back=can_backward, Forward=can_forward)
class FigureManagerWebAgg(backend_bases.FigureManagerBase):
# This must be None to not break ipympl
_toolbar2_class = None
ToolbarCls = NavigationToolbar2WebAgg
_window_title = "Matplotlib"
def __init__(self, canvas, num):
self.web_sockets = set()
super().__init__(canvas, num)
def show(self):
pass
def resize(self, w, h, forward=True):
self._send_event(
'resize',
size=(w / self.canvas.device_pixel_ratio,
h / self.canvas.device_pixel_ratio),
forward=forward)
def set_window_title(self, title):
self._send_event('figure_label', label=title)
self._window_title = title
def get_window_title(self):
return self._window_title
# The following methods are specific to FigureManagerWebAgg
def add_web_socket(self, web_socket):
assert hasattr(web_socket, 'send_binary')
assert hasattr(web_socket, 'send_json')
self.web_sockets.add(web_socket)
self.resize(*self.canvas.figure.bbox.size)
self._send_event('refresh')
def remove_web_socket(self, web_socket):
self.web_sockets.remove(web_socket)
def handle_json(self, content):
self.canvas.handle_event(content)
def refresh_all(self):
if self.web_sockets:
diff = self.canvas.get_diff_image()
if diff is not None:
for s in self.web_sockets:
s.send_binary(diff)
@classmethod
def get_javascript(cls, stream=None):
if stream is None:
output = StringIO()
else:
output = stream
output.write((Path(__file__).parent / "web_backend/js/mpl.js")
.read_text(encoding="utf-8"))
toolitems = []
for name, tooltip, image, method in cls.ToolbarCls.toolitems:
if name is None:
toolitems.append(['', '', '', ''])
else:
toolitems.append([name, tooltip, image, method])
output.write(f"mpl.toolbar_items = {json.dumps(toolitems)};\n\n")
extensions = []
for filetype, ext in sorted(FigureCanvasWebAggCore.
get_supported_filetypes_grouped().
items()):
extensions.append(ext[0])
output.write(f"mpl.extensions = {json.dumps(extensions)};\n\n")
output.write("mpl.default_extension = {};".format(
json.dumps(FigureCanvasWebAggCore.get_default_filetype())))
if stream is None:
return output.getvalue()
@classmethod
def get_static_file_path(cls):
return os.path.join(os.path.dirname(__file__), 'web_backend')
def _send_event(self, event_type, **kwargs):
payload = {'type': event_type, **kwargs}
for s in self.web_sockets:
s.send_json(payload)
@_Backend.export
class _BackendWebAggCoreAgg(_Backend):
FigureCanvas = FigureCanvasWebAggCore
FigureManager = FigureManagerWebAgg

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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