Coverage for /usr/lib/python3/dist-packages/matplotlib/cbook/__init__.py: 43%
914 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""
2A collection of utility functions and classes. Originally, many
3(but not all) were from the Python Cookbook -- hence the name cbook.
5This module is safe to import from anywhere within Matplotlib;
6it imports Matplotlib only at runtime.
7"""
9import collections
10import collections.abc
11import contextlib
12import functools
13import gzip
14import itertools
15import math
16import operator
17import os
18from pathlib import Path
19import shlex
20import subprocess
21import sys
22import time
23import traceback
24import types
25import weakref
27import numpy as np
29import matplotlib
30from matplotlib import _api, _c_internal_utils
33@_api.caching_module_getattr
34class __getattr__:
35 # module-level deprecations
36 MatplotlibDeprecationWarning = _api.deprecated(
37 "3.6", obj_type="",
38 alternative="matplotlib.MatplotlibDeprecationWarning")(
39 property(lambda self: _api.deprecation.MatplotlibDeprecationWarning))
40 mplDeprecation = _api.deprecated(
41 "3.6", obj_type="",
42 alternative="matplotlib.MatplotlibDeprecationWarning")(
43 property(lambda self: _api.deprecation.MatplotlibDeprecationWarning))
46def _get_running_interactive_framework():
47 """
48 Return the interactive framework whose event loop is currently running, if
49 any, or "headless" if no event loop can be started, or None.
51 Returns
52 -------
53 Optional[str]
54 One of the following values: "qt", "gtk3", "gtk4", "wx", "tk",
55 "macosx", "headless", ``None``.
56 """
57 # Use ``sys.modules.get(name)`` rather than ``name in sys.modules`` as
58 # entries can also have been explicitly set to None.
59 QtWidgets = (
60 sys.modules.get("PyQt6.QtWidgets")
61 or sys.modules.get("PySide6.QtWidgets")
62 or sys.modules.get("PyQt5.QtWidgets")
63 or sys.modules.get("PySide2.QtWidgets")
64 )
65 if QtWidgets and QtWidgets.QApplication.instance():
66 return "qt"
67 Gtk = sys.modules.get("gi.repository.Gtk")
68 if Gtk:
69 if Gtk.MAJOR_VERSION == 4:
70 from gi.repository import GLib
71 if GLib.main_depth():
72 return "gtk4"
73 if Gtk.MAJOR_VERSION == 3 and Gtk.main_level():
74 return "gtk3"
75 wx = sys.modules.get("wx")
76 if wx and wx.GetApp():
77 return "wx"
78 tkinter = sys.modules.get("tkinter")
79 if tkinter:
80 codes = {tkinter.mainloop.__code__, tkinter.Misc.mainloop.__code__}
81 for frame in sys._current_frames().values():
82 while frame:
83 if frame.f_code in codes:
84 return "tk"
85 frame = frame.f_back
86 macosx = sys.modules.get("matplotlib.backends._macosx")
87 if macosx and macosx.event_loop_is_running():
88 return "macosx"
89 if not _c_internal_utils.display_is_valid():
90 return "headless"
91 return None
94def _exception_printer(exc):
95 if _get_running_interactive_framework() in ["headless", None]:
96 raise exc
97 else:
98 traceback.print_exc()
101class _StrongRef:
102 """
103 Wrapper similar to a weakref, but keeping a strong reference to the object.
104 """
106 def __init__(self, obj):
107 self._obj = obj
109 def __call__(self):
110 return self._obj
112 def __eq__(self, other):
113 return isinstance(other, _StrongRef) and self._obj == other._obj
115 def __hash__(self):
116 return hash(self._obj)
119def _weak_or_strong_ref(func, callback):
120 """
121 Return a `WeakMethod` wrapping *func* if possible, else a `_StrongRef`.
122 """
123 try:
124 return weakref.WeakMethod(func, callback)
125 except TypeError:
126 return _StrongRef(func)
129class CallbackRegistry:
130 """
131 Handle registering, processing, blocking, and disconnecting
132 for a set of signals and callbacks:
134 >>> def oneat(x):
135 ... print('eat', x)
136 >>> def ondrink(x):
137 ... print('drink', x)
139 >>> from matplotlib.cbook import CallbackRegistry
140 >>> callbacks = CallbackRegistry()
142 >>> id_eat = callbacks.connect('eat', oneat)
143 >>> id_drink = callbacks.connect('drink', ondrink)
145 >>> callbacks.process('drink', 123)
146 drink 123
147 >>> callbacks.process('eat', 456)
148 eat 456
149 >>> callbacks.process('be merry', 456) # nothing will be called
151 >>> callbacks.disconnect(id_eat)
152 >>> callbacks.process('eat', 456) # nothing will be called
154 >>> with callbacks.blocked(signal='drink'):
155 ... callbacks.process('drink', 123) # nothing will be called
156 >>> callbacks.process('drink', 123)
157 drink 123
159 In practice, one should always disconnect all callbacks when they are
160 no longer needed to avoid dangling references (and thus memory leaks).
161 However, real code in Matplotlib rarely does so, and due to its design,
162 it is rather difficult to place this kind of code. To get around this,
163 and prevent this class of memory leaks, we instead store weak references
164 to bound methods only, so when the destination object needs to die, the
165 CallbackRegistry won't keep it alive.
167 Parameters
168 ----------
169 exception_handler : callable, optional
170 If not None, *exception_handler* must be a function that takes an
171 `Exception` as single parameter. It gets called with any `Exception`
172 raised by the callbacks during `CallbackRegistry.process`, and may
173 either re-raise the exception or handle it in another manner.
175 The default handler prints the exception (with `traceback.print_exc`) if
176 an interactive event loop is running; it re-raises the exception if no
177 interactive event loop is running.
179 signals : list, optional
180 If not None, *signals* is a list of signals that this registry handles:
181 attempting to `process` or to `connect` to a signal not in the list
182 throws a `ValueError`. The default, None, does not restrict the
183 handled signals.
184 """
186 # We maintain two mappings:
187 # callbacks: signal -> {cid -> weakref-to-callback}
188 # _func_cid_map: signal -> {weakref-to-callback -> cid}
190 def __init__(self, exception_handler=_exception_printer, *, signals=None):
191 self._signals = None if signals is None else list(signals) # Copy it.
192 self.exception_handler = exception_handler
193 self.callbacks = {}
194 self._cid_gen = itertools.count()
195 self._func_cid_map = {}
196 # A hidden variable that marks cids that need to be pickled.
197 self._pickled_cids = set()
199 def __getstate__(self):
200 return {
201 **vars(self),
202 # In general, callbacks may not be pickled, so we just drop them,
203 # unless directed otherwise by self._pickled_cids.
204 "callbacks": {s: {cid: proxy() for cid, proxy in d.items()
205 if cid in self._pickled_cids}
206 for s, d in self.callbacks.items()},
207 # It is simpler to reconstruct this from callbacks in __setstate__.
208 "_func_cid_map": None,
209 }
211 def __setstate__(self, state):
212 vars(self).update(state)
213 self.callbacks = {
214 s: {cid: _weak_or_strong_ref(func, self._remove_proxy)
215 for cid, func in d.items()}
216 for s, d in self.callbacks.items()}
217 self._func_cid_map = {
218 s: {proxy: cid for cid, proxy in d.items()}
219 for s, d in self.callbacks.items()}
221 def connect(self, signal, func):
222 """Register *func* to be called when signal *signal* is generated."""
223 if signal == "units finalize":
224 _api.warn_deprecated(
225 "3.5", name=signal, obj_type="signal", alternative="units")
226 if self._signals is not None:
227 _api.check_in_list(self._signals, signal=signal)
228 self._func_cid_map.setdefault(signal, {})
229 proxy = _weak_or_strong_ref(func, self._remove_proxy)
230 if proxy in self._func_cid_map[signal]:
231 return self._func_cid_map[signal][proxy]
232 cid = next(self._cid_gen)
233 self._func_cid_map[signal][proxy] = cid
234 self.callbacks.setdefault(signal, {})
235 self.callbacks[signal][cid] = proxy
236 return cid
238 def _connect_picklable(self, signal, func):
239 """
240 Like `.connect`, but the callback is kept when pickling/unpickling.
242 Currently internal-use only.
243 """
244 cid = self.connect(signal, func)
245 self._pickled_cids.add(cid)
246 return cid
248 # Keep a reference to sys.is_finalizing, as sys may have been cleared out
249 # at that point.
250 def _remove_proxy(self, proxy, *, _is_finalizing=sys.is_finalizing):
251 if _is_finalizing():
252 # Weakrefs can't be properly torn down at that point anymore.
253 return
254 for signal, proxy_to_cid in list(self._func_cid_map.items()):
255 cid = proxy_to_cid.pop(proxy, None)
256 if cid is not None:
257 del self.callbacks[signal][cid]
258 self._pickled_cids.discard(cid)
259 break
260 else:
261 # Not found
262 return
263 # Clean up empty dicts
264 if len(self.callbacks[signal]) == 0:
265 del self.callbacks[signal]
266 del self._func_cid_map[signal]
268 def disconnect(self, cid):
269 """
270 Disconnect the callback registered with callback id *cid*.
272 No error is raised if such a callback does not exist.
273 """
274 self._pickled_cids.discard(cid)
275 # Clean up callbacks
276 for signal, cid_to_proxy in list(self.callbacks.items()):
277 proxy = cid_to_proxy.pop(cid, None)
278 if proxy is not None:
279 break
280 else:
281 # Not found
282 return
284 proxy_to_cid = self._func_cid_map[signal]
285 for current_proxy, current_cid in list(proxy_to_cid.items()):
286 if current_cid == cid:
287 assert proxy is current_proxy
288 del proxy_to_cid[current_proxy]
289 # Clean up empty dicts
290 if len(self.callbacks[signal]) == 0:
291 del self.callbacks[signal]
292 del self._func_cid_map[signal]
294 def process(self, s, *args, **kwargs):
295 """
296 Process signal *s*.
298 All of the functions registered to receive callbacks on *s* will be
299 called with ``*args`` and ``**kwargs``.
300 """
301 if self._signals is not None:
302 _api.check_in_list(self._signals, signal=s)
303 for cid, ref in list(self.callbacks.get(s, {}).items()):
304 func = ref()
305 if func is not None:
306 try:
307 func(*args, **kwargs)
308 # this does not capture KeyboardInterrupt, SystemExit,
309 # and GeneratorExit
310 except Exception as exc:
311 if self.exception_handler is not None:
312 self.exception_handler(exc)
313 else:
314 raise
316 @contextlib.contextmanager
317 def blocked(self, *, signal=None):
318 """
319 Block callback signals from being processed.
321 A context manager to temporarily block/disable callback signals
322 from being processed by the registered listeners.
324 Parameters
325 ----------
326 signal : str, optional
327 The callback signal to block. The default is to block all signals.
328 """
329 orig = self.callbacks
330 try:
331 if signal is None:
332 # Empty out the callbacks
333 self.callbacks = {}
334 else:
335 # Only remove the specific signal
336 self.callbacks = {k: orig[k] for k in orig if k != signal}
337 yield
338 finally:
339 self.callbacks = orig
342class silent_list(list):
343 """
344 A list with a short ``repr()``.
346 This is meant to be used for a homogeneous list of artists, so that they
347 don't cause long, meaningless output.
349 Instead of ::
351 [<matplotlib.lines.Line2D object at 0x7f5749fed3c8>,
352 <matplotlib.lines.Line2D object at 0x7f5749fed4e0>,
353 <matplotlib.lines.Line2D object at 0x7f5758016550>]
355 one will get ::
357 <a list of 3 Line2D objects>
359 If ``self.type`` is None, the type name is obtained from the first item in
360 the list (if any).
361 """
363 def __init__(self, type, seq=None):
364 self.type = type
365 if seq is not None:
366 self.extend(seq)
368 def __repr__(self):
369 if self.type is not None or len(self) != 0:
370 tp = self.type if self.type is not None else type(self[0]).__name__
371 return f"<a list of {len(self)} {tp} objects>"
372 else:
373 return "<an empty list>"
376def _local_over_kwdict(
377 local_var, kwargs, *keys,
378 warning_cls=_api.MatplotlibDeprecationWarning):
379 out = local_var
380 for key in keys:
381 kwarg_val = kwargs.pop(key, None)
382 if kwarg_val is not None:
383 if out is None:
384 out = kwarg_val
385 else:
386 _api.warn_external(f'"{key}" keyword argument will be ignored',
387 warning_cls)
388 return out
391def strip_math(s):
392 """
393 Remove latex formatting from mathtext.
395 Only handles fully math and fully non-math strings.
396 """
397 if len(s) >= 2 and s[0] == s[-1] == "$":
398 s = s[1:-1]
399 for tex, plain in [
400 (r"\times", "x"), # Specifically for Formatter support.
401 (r"\mathdefault", ""),
402 (r"\rm", ""),
403 (r"\cal", ""),
404 (r"\tt", ""),
405 (r"\it", ""),
406 ("\\", ""),
407 ("{", ""),
408 ("}", ""),
409 ]:
410 s = s.replace(tex, plain)
411 return s
414def _strip_comment(s):
415 """Strip everything from the first unquoted #."""
416 pos = 0
417 while True:
418 quote_pos = s.find('"', pos)
419 hash_pos = s.find('#', pos)
420 if quote_pos < 0:
421 without_comment = s if hash_pos < 0 else s[:hash_pos]
422 return without_comment.strip()
423 elif 0 <= hash_pos < quote_pos:
424 return s[:hash_pos].strip()
425 else:
426 closing_quote_pos = s.find('"', quote_pos + 1)
427 if closing_quote_pos < 0:
428 raise ValueError(
429 f"Missing closing quote in: {s!r}. If you need a double-"
430 'quote inside a string, use escaping: e.g. "the \" char"')
431 pos = closing_quote_pos + 1 # behind closing quote
434def is_writable_file_like(obj):
435 """Return whether *obj* looks like a file object with a *write* method."""
436 return callable(getattr(obj, 'write', None))
439def file_requires_unicode(x):
440 """
441 Return whether the given writable file-like object requires Unicode to be
442 written to it.
443 """
444 try:
445 x.write(b'')
446 except TypeError:
447 return True
448 else:
449 return False
452def to_filehandle(fname, flag='r', return_opened=False, encoding=None):
453 """
454 Convert a path to an open file handle or pass-through a file-like object.
456 Consider using `open_file_cm` instead, as it allows one to properly close
457 newly created file objects more easily.
459 Parameters
460 ----------
461 fname : str or path-like or file-like
462 If `str` or `os.PathLike`, the file is opened using the flags specified
463 by *flag* and *encoding*. If a file-like object, it is passed through.
464 flag : str, default: 'r'
465 Passed as the *mode* argument to `open` when *fname* is `str` or
466 `os.PathLike`; ignored if *fname* is file-like.
467 return_opened : bool, default: False
468 If True, return both the file object and a boolean indicating whether
469 this was a new file (that the caller needs to close). If False, return
470 only the new file.
471 encoding : str or None, default: None
472 Passed as the *mode* argument to `open` when *fname* is `str` or
473 `os.PathLike`; ignored if *fname* is file-like.
475 Returns
476 -------
477 fh : file-like
478 opened : bool
479 *opened* is only returned if *return_opened* is True.
480 """
481 if isinstance(fname, os.PathLike):
482 fname = os.fspath(fname)
483 if isinstance(fname, str):
484 if fname.endswith('.gz'):
485 fh = gzip.open(fname, flag)
486 elif fname.endswith('.bz2'):
487 # python may not be compiled with bz2 support,
488 # bury import until we need it
489 import bz2
490 fh = bz2.BZ2File(fname, flag)
491 else:
492 fh = open(fname, flag, encoding=encoding)
493 opened = True
494 elif hasattr(fname, 'seek'):
495 fh = fname
496 opened = False
497 else:
498 raise ValueError('fname must be a PathLike or file handle')
499 if return_opened:
500 return fh, opened
501 return fh
504def open_file_cm(path_or_file, mode="r", encoding=None):
505 r"""Pass through file objects and context-manage path-likes."""
506 fh, opened = to_filehandle(path_or_file, mode, True, encoding)
507 return fh if opened else contextlib.nullcontext(fh)
510def is_scalar_or_string(val):
511 """Return whether the given object is a scalar or string like."""
512 return isinstance(val, str) or not np.iterable(val)
515def get_sample_data(fname, asfileobj=True, *, np_load=False):
516 """
517 Return a sample data file. *fname* is a path relative to the
518 :file:`mpl-data/sample_data` directory. If *asfileobj* is `True`
519 return a file object, otherwise just a file path.
521 Sample data files are stored in the 'mpl-data/sample_data' directory within
522 the Matplotlib package.
524 If the filename ends in .gz, the file is implicitly ungzipped. If the
525 filename ends with .npy or .npz, *asfileobj* is True, and *np_load* is
526 True, the file is loaded with `numpy.load`. *np_load* currently defaults
527 to False but will default to True in a future release.
528 """
529 path = _get_data_path('sample_data', fname)
530 if asfileobj:
531 suffix = path.suffix.lower()
532 if suffix == '.gz':
533 return gzip.open(path)
534 elif suffix in ['.npy', '.npz']:
535 if np_load:
536 return np.load(path)
537 else:
538 _api.warn_deprecated(
539 "3.3", message="In a future release, get_sample_data "
540 "will automatically load numpy arrays. Set np_load to "
541 "True to get the array and suppress this warning. Set "
542 "asfileobj to False to get the path to the data file and "
543 "suppress this warning.")
544 return path.open('rb')
545 elif suffix in ['.csv', '.xrc', '.txt']:
546 return path.open('r')
547 else:
548 return path.open('rb')
549 else:
550 return str(path)
553def _get_data_path(*args):
554 """
555 Return the `pathlib.Path` to a resource file provided by Matplotlib.
557 ``*args`` specify a path relative to the base data path.
558 """
559 return Path(matplotlib.get_data_path(), *args)
562def flatten(seq, scalarp=is_scalar_or_string):
563 """
564 Return a generator of flattened nested containers.
566 For example:
568 >>> from matplotlib.cbook import flatten
569 >>> l = (('John', ['Hunter']), (1, 23), [[([42, (5, 23)], )]])
570 >>> print(list(flatten(l)))
571 ['John', 'Hunter', 1, 23, 42, 5, 23]
573 By: Composite of Holger Krekel and Luther Blissett
574 From: https://code.activestate.com/recipes/121294/
575 and Recipe 1.12 in cookbook
576 """
577 for item in seq:
578 if scalarp(item) or item is None:
579 yield item
580 else:
581 yield from flatten(item, scalarp)
584@_api.deprecated("3.6", alternative="functools.lru_cache")
585class maxdict(dict):
586 """
587 A dictionary with a maximum size.
589 Notes
590 -----
591 This doesn't override all the relevant methods to constrain the size,
592 just ``__setitem__``, so use with caution.
593 """
595 def __init__(self, maxsize):
596 super().__init__()
597 self.maxsize = maxsize
599 def __setitem__(self, k, v):
600 super().__setitem__(k, v)
601 while len(self) >= self.maxsize:
602 del self[next(iter(self))]
605class Stack:
606 """
607 Stack of elements with a movable cursor.
609 Mimics home/back/forward in a web browser.
610 """
612 def __init__(self, default=None):
613 self.clear()
614 self._default = default
616 def __call__(self):
617 """Return the current element, or None."""
618 if not self._elements:
619 return self._default
620 else:
621 return self._elements[self._pos]
623 def __len__(self):
624 return len(self._elements)
626 def __getitem__(self, ind):
627 return self._elements[ind]
629 def forward(self):
630 """Move the position forward and return the current element."""
631 self._pos = min(self._pos + 1, len(self._elements) - 1)
632 return self()
634 def back(self):
635 """Move the position back and return the current element."""
636 if self._pos > 0:
637 self._pos -= 1
638 return self()
640 def push(self, o):
641 """
642 Push *o* to the stack at current position. Discard all later elements.
644 *o* is returned.
645 """
646 self._elements = self._elements[:self._pos + 1] + [o]
647 self._pos = len(self._elements) - 1
648 return self()
650 def home(self):
651 """
652 Push the first element onto the top of the stack.
654 The first element is returned.
655 """
656 if not self._elements:
657 return
658 self.push(self._elements[0])
659 return self()
661 def empty(self):
662 """Return whether the stack is empty."""
663 return len(self._elements) == 0
665 def clear(self):
666 """Empty the stack."""
667 self._pos = -1
668 self._elements = []
670 def bubble(self, o):
671 """
672 Raise all references of *o* to the top of the stack, and return it.
674 Raises
675 ------
676 ValueError
677 If *o* is not in the stack.
678 """
679 if o not in self._elements:
680 raise ValueError('Given element not contained in the stack')
681 old_elements = self._elements.copy()
682 self.clear()
683 top_elements = []
684 for elem in old_elements:
685 if elem == o:
686 top_elements.append(elem)
687 else:
688 self.push(elem)
689 for _ in top_elements:
690 self.push(o)
691 return o
693 def remove(self, o):
694 """
695 Remove *o* from the stack.
697 Raises
698 ------
699 ValueError
700 If *o* is not in the stack.
701 """
702 if o not in self._elements:
703 raise ValueError('Given element not contained in the stack')
704 old_elements = self._elements.copy()
705 self.clear()
706 for elem in old_elements:
707 if elem != o:
708 self.push(elem)
711@_api.deprecated("3.5", alternative="psutil.virtual_memory")
712def report_memory(i=0): # argument may go away
713 """Return the memory consumed by the process."""
714 def call(command, os_name):
715 try:
716 return subprocess.check_output(command)
717 except subprocess.CalledProcessError as err:
718 raise NotImplementedError(
719 "report_memory works on %s only if "
720 "the '%s' program is found" % (os_name, command[0])
721 ) from err
723 pid = os.getpid()
724 if sys.platform == 'sunos5':
725 lines = call(['ps', '-p', '%d' % pid, '-o', 'osz'], 'Sun OS')
726 mem = int(lines[-1].strip())
727 elif sys.platform == 'linux':
728 lines = call(['ps', '-p', '%d' % pid, '-o', 'rss,sz'], 'Linux')
729 mem = int(lines[1].split()[1])
730 elif sys.platform == 'darwin':
731 lines = call(['ps', '-p', '%d' % pid, '-o', 'rss,vsz'], 'Mac OS')
732 mem = int(lines[1].split()[0])
733 elif sys.platform == 'win32':
734 lines = call(["tasklist", "/nh", "/fi", "pid eq %d" % pid], 'Windows')
735 mem = int(lines.strip().split()[-2].replace(',', ''))
736 else:
737 raise NotImplementedError(
738 "We don't have a memory monitor for %s" % sys.platform)
739 return mem
742def safe_masked_invalid(x, copy=False):
743 x = np.array(x, subok=True, copy=copy)
744 if not x.dtype.isnative:
745 # If we have already made a copy, do the byteswap in place, else make a
746 # copy with the byte order swapped.
747 x = x.byteswap(inplace=copy).newbyteorder('N') # Swap to native order.
748 try:
749 xm = np.ma.masked_invalid(x, copy=False)
750 xm.shrink_mask()
751 except TypeError:
752 return x
753 return xm
756def print_cycles(objects, outstream=sys.stdout, show_progress=False):
757 """
758 Print loops of cyclic references in the given *objects*.
760 It is often useful to pass in ``gc.garbage`` to find the cycles that are
761 preventing some objects from being garbage collected.
763 Parameters
764 ----------
765 objects
766 A list of objects to find cycles in.
767 outstream
768 The stream for output.
769 show_progress : bool
770 If True, print the number of objects reached as they are found.
771 """
772 import gc
774 def print_path(path):
775 for i, step in enumerate(path):
776 # next "wraps around"
777 next = path[(i + 1) % len(path)]
779 outstream.write(" %s -- " % type(step))
780 if isinstance(step, dict):
781 for key, val in step.items():
782 if val is next:
783 outstream.write("[{!r}]".format(key))
784 break
785 if key is next:
786 outstream.write("[key] = {!r}".format(val))
787 break
788 elif isinstance(step, list):
789 outstream.write("[%d]" % step.index(next))
790 elif isinstance(step, tuple):
791 outstream.write("( tuple )")
792 else:
793 outstream.write(repr(step))
794 outstream.write(" ->\n")
795 outstream.write("\n")
797 def recurse(obj, start, all, current_path):
798 if show_progress:
799 outstream.write("%d\r" % len(all))
801 all[id(obj)] = None
803 referents = gc.get_referents(obj)
804 for referent in referents:
805 # If we've found our way back to the start, this is
806 # a cycle, so print it out
807 if referent is start:
808 print_path(current_path)
810 # Don't go back through the original list of objects, or
811 # through temporary references to the object, since those
812 # are just an artifact of the cycle detector itself.
813 elif referent is objects or isinstance(referent, types.FrameType):
814 continue
816 # We haven't seen this object before, so recurse
817 elif id(referent) not in all:
818 recurse(referent, start, all, current_path + [obj])
820 for obj in objects:
821 outstream.write(f"Examining: {obj!r}\n")
822 recurse(obj, obj, {}, [])
825class Grouper:
826 """
827 A disjoint-set data structure.
829 Objects can be joined using :meth:`join`, tested for connectedness
830 using :meth:`joined`, and all disjoint sets can be retrieved by
831 using the object as an iterator.
833 The objects being joined must be hashable and weak-referenceable.
835 Examples
836 --------
837 >>> from matplotlib.cbook import Grouper
838 >>> class Foo:
839 ... def __init__(self, s):
840 ... self.s = s
841 ... def __repr__(self):
842 ... return self.s
843 ...
844 >>> a, b, c, d, e, f = [Foo(x) for x in 'abcdef']
845 >>> grp = Grouper()
846 >>> grp.join(a, b)
847 >>> grp.join(b, c)
848 >>> grp.join(d, e)
849 >>> list(grp)
850 [[a, b, c], [d, e]]
851 >>> grp.joined(a, b)
852 True
853 >>> grp.joined(a, c)
854 True
855 >>> grp.joined(a, d)
856 False
857 """
859 def __init__(self, init=()):
860 self._mapping = {weakref.ref(x): [weakref.ref(x)] for x in init}
862 def __contains__(self, item):
863 return weakref.ref(item) in self._mapping
865 def clean(self):
866 """Clean dead weak references from the dictionary."""
867 mapping = self._mapping
868 to_drop = [key for key in mapping if key() is None]
869 for key in to_drop:
870 val = mapping.pop(key)
871 val.remove(key)
873 def join(self, a, *args):
874 """
875 Join given arguments into the same set. Accepts one or more arguments.
876 """
877 mapping = self._mapping
878 set_a = mapping.setdefault(weakref.ref(a), [weakref.ref(a)])
880 for arg in args:
881 set_b = mapping.get(weakref.ref(arg), [weakref.ref(arg)])
882 if set_b is not set_a:
883 if len(set_b) > len(set_a):
884 set_a, set_b = set_b, set_a
885 set_a.extend(set_b)
886 for elem in set_b:
887 mapping[elem] = set_a
889 self.clean()
891 def joined(self, a, b):
892 """Return whether *a* and *b* are members of the same set."""
893 self.clean()
894 return (self._mapping.get(weakref.ref(a), object())
895 is self._mapping.get(weakref.ref(b)))
897 def remove(self, a):
898 self.clean()
899 set_a = self._mapping.pop(weakref.ref(a), None)
900 if set_a:
901 set_a.remove(weakref.ref(a))
903 def __iter__(self):
904 """
905 Iterate over each of the disjoint sets as a list.
907 The iterator is invalid if interleaved with calls to join().
908 """
909 self.clean()
910 unique_groups = {id(group): group for group in self._mapping.values()}
911 for group in unique_groups.values():
912 yield [x() for x in group]
914 def get_siblings(self, a):
915 """Return all of the items joined with *a*, including itself."""
916 self.clean()
917 siblings = self._mapping.get(weakref.ref(a), [weakref.ref(a)])
918 return [x() for x in siblings]
921class GrouperView:
922 """Immutable view over a `.Grouper`."""
924 def __init__(self, grouper):
925 self._grouper = grouper
927 class _GrouperMethodForwarder:
928 def __init__(self, deprecated_kw=None):
929 self._deprecated_kw = deprecated_kw
931 def __set_name__(self, owner, name):
932 wrapped = getattr(Grouper, name)
933 forwarder = functools.wraps(wrapped)(
934 lambda self, *args, **kwargs: wrapped(
935 self._grouper, *args, **kwargs))
936 if self._deprecated_kw:
937 forwarder = _api.deprecated(**self._deprecated_kw)(forwarder)
938 setattr(owner, name, forwarder)
940 __contains__ = _GrouperMethodForwarder()
941 __iter__ = _GrouperMethodForwarder()
942 joined = _GrouperMethodForwarder()
943 get_siblings = _GrouperMethodForwarder()
944 clean = _GrouperMethodForwarder(deprecated_kw=dict(since="3.6"))
945 join = _GrouperMethodForwarder(deprecated_kw=dict(since="3.6"))
946 remove = _GrouperMethodForwarder(deprecated_kw=dict(since="3.6"))
949def simple_linear_interpolation(a, steps):
950 """
951 Resample an array with ``steps - 1`` points between original point pairs.
953 Along each column of *a*, ``(steps - 1)`` points are introduced between
954 each original values; the values are linearly interpolated.
956 Parameters
957 ----------
958 a : array, shape (n, ...)
959 steps : int
961 Returns
962 -------
963 array
964 shape ``((n - 1) * steps + 1, ...)``
965 """
966 fps = a.reshape((len(a), -1))
967 xp = np.arange(len(a)) * steps
968 x = np.arange((len(a) - 1) * steps + 1)
969 return (np.column_stack([np.interp(x, xp, fp) for fp in fps.T])
970 .reshape((len(x),) + a.shape[1:]))
973def delete_masked_points(*args):
974 """
975 Find all masked and/or non-finite points in a set of arguments,
976 and return the arguments with only the unmasked points remaining.
978 Arguments can be in any of 5 categories:
980 1) 1-D masked arrays
981 2) 1-D ndarrays
982 3) ndarrays with more than one dimension
983 4) other non-string iterables
984 5) anything else
986 The first argument must be in one of the first four categories;
987 any argument with a length differing from that of the first
988 argument (and hence anything in category 5) then will be
989 passed through unchanged.
991 Masks are obtained from all arguments of the correct length
992 in categories 1, 2, and 4; a point is bad if masked in a masked
993 array or if it is a nan or inf. No attempt is made to
994 extract a mask from categories 2, 3, and 4 if `numpy.isfinite`
995 does not yield a Boolean array.
997 All input arguments that are not passed unchanged are returned
998 as ndarrays after removing the points or rows corresponding to
999 masks in any of the arguments.
1001 A vastly simpler version of this function was originally
1002 written as a helper for Axes.scatter().
1004 """
1005 if not len(args):
1006 return ()
1007 if is_scalar_or_string(args[0]):
1008 raise ValueError("First argument must be a sequence")
1009 nrecs = len(args[0])
1010 margs = []
1011 seqlist = [False] * len(args)
1012 for i, x in enumerate(args):
1013 if not isinstance(x, str) and np.iterable(x) and len(x) == nrecs:
1014 seqlist[i] = True
1015 if isinstance(x, np.ma.MaskedArray):
1016 if x.ndim > 1:
1017 raise ValueError("Masked arrays must be 1-D")
1018 else:
1019 x = np.asarray(x)
1020 margs.append(x)
1021 masks = [] # List of masks that are True where good.
1022 for i, x in enumerate(margs):
1023 if seqlist[i]:
1024 if x.ndim > 1:
1025 continue # Don't try to get nan locations unless 1-D.
1026 if isinstance(x, np.ma.MaskedArray):
1027 masks.append(~np.ma.getmaskarray(x)) # invert the mask
1028 xd = x.data
1029 else:
1030 xd = x
1031 try:
1032 mask = np.isfinite(xd)
1033 if isinstance(mask, np.ndarray):
1034 masks.append(mask)
1035 except Exception: # Fixme: put in tuple of possible exceptions?
1036 pass
1037 if len(masks):
1038 mask = np.logical_and.reduce(masks)
1039 igood = mask.nonzero()[0]
1040 if len(igood) < nrecs:
1041 for i, x in enumerate(margs):
1042 if seqlist[i]:
1043 margs[i] = x[igood]
1044 for i, x in enumerate(margs):
1045 if seqlist[i] and isinstance(x, np.ma.MaskedArray):
1046 margs[i] = x.filled()
1047 return margs
1050def _combine_masks(*args):
1051 """
1052 Find all masked and/or non-finite points in a set of arguments,
1053 and return the arguments as masked arrays with a common mask.
1055 Arguments can be in any of 5 categories:
1057 1) 1-D masked arrays
1058 2) 1-D ndarrays
1059 3) ndarrays with more than one dimension
1060 4) other non-string iterables
1061 5) anything else
1063 The first argument must be in one of the first four categories;
1064 any argument with a length differing from that of the first
1065 argument (and hence anything in category 5) then will be
1066 passed through unchanged.
1068 Masks are obtained from all arguments of the correct length
1069 in categories 1, 2, and 4; a point is bad if masked in a masked
1070 array or if it is a nan or inf. No attempt is made to
1071 extract a mask from categories 2 and 4 if `numpy.isfinite`
1072 does not yield a Boolean array. Category 3 is included to
1073 support RGB or RGBA ndarrays, which are assumed to have only
1074 valid values and which are passed through unchanged.
1076 All input arguments that are not passed unchanged are returned
1077 as masked arrays if any masked points are found, otherwise as
1078 ndarrays.
1080 """
1081 if not len(args):
1082 return ()
1083 if is_scalar_or_string(args[0]):
1084 raise ValueError("First argument must be a sequence")
1085 nrecs = len(args[0])
1086 margs = [] # Output args; some may be modified.
1087 seqlist = [False] * len(args) # Flags: True if output will be masked.
1088 masks = [] # List of masks.
1089 for i, x in enumerate(args):
1090 if is_scalar_or_string(x) or len(x) != nrecs:
1091 margs.append(x) # Leave it unmodified.
1092 else:
1093 if isinstance(x, np.ma.MaskedArray) and x.ndim > 1:
1094 raise ValueError("Masked arrays must be 1-D")
1095 try:
1096 x = np.asanyarray(x)
1097 except (np.VisibleDeprecationWarning, ValueError):
1098 # NumPy 1.19 raises a warning about ragged arrays, but we want
1099 # to accept basically anything here.
1100 x = np.asanyarray(x, dtype=object)
1101 if x.ndim == 1:
1102 x = safe_masked_invalid(x)
1103 seqlist[i] = True
1104 if np.ma.is_masked(x):
1105 masks.append(np.ma.getmaskarray(x))
1106 margs.append(x) # Possibly modified.
1107 if len(masks):
1108 mask = np.logical_or.reduce(masks)
1109 for i, x in enumerate(margs):
1110 if seqlist[i]:
1111 margs[i] = np.ma.array(x, mask=mask)
1112 return margs
1115def boxplot_stats(X, whis=1.5, bootstrap=None, labels=None,
1116 autorange=False):
1117 r"""
1118 Return a list of dictionaries of statistics used to draw a series of box
1119 and whisker plots using `~.Axes.bxp`.
1121 Parameters
1122 ----------
1123 X : array-like
1124 Data that will be represented in the boxplots. Should have 2 or
1125 fewer dimensions.
1127 whis : float or (float, float), default: 1.5
1128 The position of the whiskers.
1130 If a float, the lower whisker is at the lowest datum above
1131 ``Q1 - whis*(Q3-Q1)``, and the upper whisker at the highest datum below
1132 ``Q3 + whis*(Q3-Q1)``, where Q1 and Q3 are the first and third
1133 quartiles. The default value of ``whis = 1.5`` corresponds to Tukey's
1134 original definition of boxplots.
1136 If a pair of floats, they indicate the percentiles at which to draw the
1137 whiskers (e.g., (5, 95)). In particular, setting this to (0, 100)
1138 results in whiskers covering the whole range of the data.
1140 In the edge case where ``Q1 == Q3``, *whis* is automatically set to
1141 (0, 100) (cover the whole range of the data) if *autorange* is True.
1143 Beyond the whiskers, data are considered outliers and are plotted as
1144 individual points.
1146 bootstrap : int, optional
1147 Number of times the confidence intervals around the median
1148 should be bootstrapped (percentile method).
1150 labels : array-like, optional
1151 Labels for each dataset. Length must be compatible with
1152 dimensions of *X*.
1154 autorange : bool, optional (False)
1155 When `True` and the data are distributed such that the 25th and 75th
1156 percentiles are equal, ``whis`` is set to (0, 100) such that the
1157 whisker ends are at the minimum and maximum of the data.
1159 Returns
1160 -------
1161 list of dict
1162 A list of dictionaries containing the results for each column
1163 of data. Keys of each dictionary are the following:
1165 ======== ===================================
1166 Key Value Description
1167 ======== ===================================
1168 label tick label for the boxplot
1169 mean arithmetic mean value
1170 med 50th percentile
1171 q1 first quartile (25th percentile)
1172 q3 third quartile (75th percentile)
1173 iqr interquartile range
1174 cilo lower notch around the median
1175 cihi upper notch around the median
1176 whislo end of the lower whisker
1177 whishi end of the upper whisker
1178 fliers outliers
1179 ======== ===================================
1181 Notes
1182 -----
1183 Non-bootstrapping approach to confidence interval uses Gaussian-based
1184 asymptotic approximation:
1186 .. math::
1188 \mathrm{med} \pm 1.57 \times \frac{\mathrm{iqr}}{\sqrt{N}}
1190 General approach from:
1191 McGill, R., Tukey, J.W., and Larsen, W.A. (1978) "Variations of
1192 Boxplots", The American Statistician, 32:12-16.
1193 """
1195 def _bootstrap_median(data, N=5000):
1196 # determine 95% confidence intervals of the median
1197 M = len(data)
1198 percentiles = [2.5, 97.5]
1200 bs_index = np.random.randint(M, size=(N, M))
1201 bsData = data[bs_index]
1202 estimate = np.median(bsData, axis=1, overwrite_input=True)
1204 CI = np.percentile(estimate, percentiles)
1205 return CI
1207 def _compute_conf_interval(data, med, iqr, bootstrap):
1208 if bootstrap is not None:
1209 # Do a bootstrap estimate of notch locations.
1210 # get conf. intervals around median
1211 CI = _bootstrap_median(data, N=bootstrap)
1212 notch_min = CI[0]
1213 notch_max = CI[1]
1214 else:
1216 N = len(data)
1217 notch_min = med - 1.57 * iqr / np.sqrt(N)
1218 notch_max = med + 1.57 * iqr / np.sqrt(N)
1220 return notch_min, notch_max
1222 # output is a list of dicts
1223 bxpstats = []
1225 # convert X to a list of lists
1226 X = _reshape_2D(X, "X")
1228 ncols = len(X)
1229 if labels is None:
1230 labels = itertools.repeat(None)
1231 elif len(labels) != ncols:
1232 raise ValueError("Dimensions of labels and X must be compatible")
1234 input_whis = whis
1235 for ii, (x, label) in enumerate(zip(X, labels)):
1237 # empty dict
1238 stats = {}
1239 if label is not None:
1240 stats['label'] = label
1242 # restore whis to the input values in case it got changed in the loop
1243 whis = input_whis
1245 # note tricksiness, append up here and then mutate below
1246 bxpstats.append(stats)
1248 # if empty, bail
1249 if len(x) == 0:
1250 stats['fliers'] = np.array([])
1251 stats['mean'] = np.nan
1252 stats['med'] = np.nan
1253 stats['q1'] = np.nan
1254 stats['q3'] = np.nan
1255 stats['iqr'] = np.nan
1256 stats['cilo'] = np.nan
1257 stats['cihi'] = np.nan
1258 stats['whislo'] = np.nan
1259 stats['whishi'] = np.nan
1260 continue
1262 # up-convert to an array, just to be safe
1263 x = np.asarray(x)
1265 # arithmetic mean
1266 stats['mean'] = np.mean(x)
1268 # medians and quartiles
1269 q1, med, q3 = np.percentile(x, [25, 50, 75])
1271 # interquartile range
1272 stats['iqr'] = q3 - q1
1273 if stats['iqr'] == 0 and autorange:
1274 whis = (0, 100)
1276 # conf. interval around median
1277 stats['cilo'], stats['cihi'] = _compute_conf_interval(
1278 x, med, stats['iqr'], bootstrap
1279 )
1281 # lowest/highest non-outliers
1282 if np.iterable(whis) and not isinstance(whis, str):
1283 loval, hival = np.percentile(x, whis)
1284 elif np.isreal(whis):
1285 loval = q1 - whis * stats['iqr']
1286 hival = q3 + whis * stats['iqr']
1287 else:
1288 raise ValueError('whis must be a float or list of percentiles')
1290 # get high extreme
1291 wiskhi = x[x <= hival]
1292 if len(wiskhi) == 0 or np.max(wiskhi) < q3:
1293 stats['whishi'] = q3
1294 else:
1295 stats['whishi'] = np.max(wiskhi)
1297 # get low extreme
1298 wisklo = x[x >= loval]
1299 if len(wisklo) == 0 or np.min(wisklo) > q1:
1300 stats['whislo'] = q1
1301 else:
1302 stats['whislo'] = np.min(wisklo)
1304 # compute a single array of outliers
1305 stats['fliers'] = np.concatenate([
1306 x[x < stats['whislo']],
1307 x[x > stats['whishi']],
1308 ])
1310 # add in the remaining stats
1311 stats['q1'], stats['med'], stats['q3'] = q1, med, q3
1313 return bxpstats
1316#: Maps short codes for line style to their full name used by backends.
1317ls_mapper = {'-': 'solid', '--': 'dashed', '-.': 'dashdot', ':': 'dotted'}
1318#: Maps full names for line styles used by backends to their short codes.
1319ls_mapper_r = {v: k for k, v in ls_mapper.items()}
1322def contiguous_regions(mask):
1323 """
1324 Return a list of (ind0, ind1) such that ``mask[ind0:ind1].all()`` is
1325 True and we cover all such regions.
1326 """
1327 mask = np.asarray(mask, dtype=bool)
1329 if not mask.size:
1330 return []
1332 # Find the indices of region changes, and correct offset
1333 idx, = np.nonzero(mask[:-1] != mask[1:])
1334 idx += 1
1336 # List operations are faster for moderately sized arrays
1337 idx = idx.tolist()
1339 # Add first and/or last index if needed
1340 if mask[0]:
1341 idx = [0] + idx
1342 if mask[-1]:
1343 idx.append(len(mask))
1345 return list(zip(idx[::2], idx[1::2]))
1348def is_math_text(s):
1349 """
1350 Return whether the string *s* contains math expressions.
1352 This is done by checking whether *s* contains an even number of
1353 non-escaped dollar signs.
1354 """
1355 s = str(s)
1356 dollar_count = s.count(r'$') - s.count(r'\$')
1357 even_dollars = (dollar_count > 0 and dollar_count % 2 == 0)
1358 return even_dollars
1361def _to_unmasked_float_array(x):
1362 """
1363 Convert a sequence to a float array; if input was a masked array, masked
1364 values are converted to nans.
1365 """
1366 if hasattr(x, 'mask'):
1367 return np.ma.asarray(x, float).filled(np.nan)
1368 else:
1369 return np.asarray(x, float)
1372def _check_1d(x):
1373 """Convert scalars to 1D arrays; pass-through arrays as is."""
1374 # Unpack in case of e.g. Pandas or xarray object
1375 x = _unpack_to_numpy(x)
1376 # plot requires `shape` and `ndim`. If passed an
1377 # object that doesn't provide them, then force to numpy array.
1378 # Note this will strip unit information.
1379 if (not hasattr(x, 'shape') or
1380 not hasattr(x, 'ndim') or
1381 len(x.shape) < 1):
1382 return np.atleast_1d(x)
1383 else:
1384 return x
1387def _reshape_2D(X, name):
1388 """
1389 Use Fortran ordering to convert ndarrays and lists of iterables to lists of
1390 1D arrays.
1392 Lists of iterables are converted by applying `numpy.asanyarray` to each of
1393 their elements. 1D ndarrays are returned in a singleton list containing
1394 them. 2D ndarrays are converted to the list of their *columns*.
1396 *name* is used to generate the error message for invalid inputs.
1397 """
1399 # Unpack in case of e.g. Pandas or xarray object
1400 X = _unpack_to_numpy(X)
1402 # Iterate over columns for ndarrays.
1403 if isinstance(X, np.ndarray):
1404 X = X.T
1406 if len(X) == 0:
1407 return [[]]
1408 elif X.ndim == 1 and np.ndim(X[0]) == 0:
1409 # 1D array of scalars: directly return it.
1410 return [X]
1411 elif X.ndim in [1, 2]:
1412 # 2D array, or 1D array of iterables: flatten them first.
1413 return [np.reshape(x, -1) for x in X]
1414 else:
1415 raise ValueError(f'{name} must have 2 or fewer dimensions')
1417 # Iterate over list of iterables.
1418 if len(X) == 0:
1419 return [[]]
1421 result = []
1422 is_1d = True
1423 for xi in X:
1424 # check if this is iterable, except for strings which we
1425 # treat as singletons.
1426 if not isinstance(xi, str):
1427 try:
1428 iter(xi)
1429 except TypeError:
1430 pass
1431 else:
1432 is_1d = False
1433 xi = np.asanyarray(xi)
1434 nd = np.ndim(xi)
1435 if nd > 1:
1436 raise ValueError(f'{name} must have 2 or fewer dimensions')
1437 result.append(xi.reshape(-1))
1439 if is_1d:
1440 # 1D array of scalars: directly return it.
1441 return [np.reshape(result, -1)]
1442 else:
1443 # 2D array, or 1D array of iterables: use flattened version.
1444 return result
1447def violin_stats(X, method, points=100, quantiles=None):
1448 """
1449 Return a list of dictionaries of data which can be used to draw a series
1450 of violin plots.
1452 See the ``Returns`` section below to view the required keys of the
1453 dictionary.
1455 Users can skip this function and pass a user-defined set of dictionaries
1456 with the same keys to `~.axes.Axes.violinplot` instead of using Matplotlib
1457 to do the calculations. See the *Returns* section below for the keys
1458 that must be present in the dictionaries.
1460 Parameters
1461 ----------
1462 X : array-like
1463 Sample data that will be used to produce the gaussian kernel density
1464 estimates. Must have 2 or fewer dimensions.
1466 method : callable
1467 The method used to calculate the kernel density estimate for each
1468 column of data. When called via ``method(v, coords)``, it should
1469 return a vector of the values of the KDE evaluated at the values
1470 specified in coords.
1472 points : int, default: 100
1473 Defines the number of points to evaluate each of the gaussian kernel
1474 density estimates at.
1476 quantiles : array-like, default: None
1477 Defines (if not None) a list of floats in interval [0, 1] for each
1478 column of data, which represents the quantiles that will be rendered
1479 for that column of data. Must have 2 or fewer dimensions. 1D array will
1480 be treated as a singleton list containing them.
1482 Returns
1483 -------
1484 list of dict
1485 A list of dictionaries containing the results for each column of data.
1486 The dictionaries contain at least the following:
1488 - coords: A list of scalars containing the coordinates this particular
1489 kernel density estimate was evaluated at.
1490 - vals: A list of scalars containing the values of the kernel density
1491 estimate at each of the coordinates given in *coords*.
1492 - mean: The mean value for this column of data.
1493 - median: The median value for this column of data.
1494 - min: The minimum value for this column of data.
1495 - max: The maximum value for this column of data.
1496 - quantiles: The quantile values for this column of data.
1497 """
1499 # List of dictionaries describing each of the violins.
1500 vpstats = []
1502 # Want X to be a list of data sequences
1503 X = _reshape_2D(X, "X")
1505 # Want quantiles to be as the same shape as data sequences
1506 if quantiles is not None and len(quantiles) != 0:
1507 quantiles = _reshape_2D(quantiles, "quantiles")
1508 # Else, mock quantiles if it's none or empty
1509 else:
1510 quantiles = [[]] * len(X)
1512 # quantiles should has the same size as dataset
1513 if len(X) != len(quantiles):
1514 raise ValueError("List of violinplot statistics and quantiles values"
1515 " must have the same length")
1517 # Zip x and quantiles
1518 for (x, q) in zip(X, quantiles):
1519 # Dictionary of results for this distribution
1520 stats = {}
1522 # Calculate basic stats for the distribution
1523 min_val = np.min(x)
1524 max_val = np.max(x)
1525 quantile_val = np.percentile(x, 100 * q)
1527 # Evaluate the kernel density estimate
1528 coords = np.linspace(min_val, max_val, points)
1529 stats['vals'] = method(x, coords)
1530 stats['coords'] = coords
1532 # Store additional statistics for this distribution
1533 stats['mean'] = np.mean(x)
1534 stats['median'] = np.median(x)
1535 stats['min'] = min_val
1536 stats['max'] = max_val
1537 stats['quantiles'] = np.atleast_1d(quantile_val)
1539 # Append to output
1540 vpstats.append(stats)
1542 return vpstats
1545def pts_to_prestep(x, *args):
1546 """
1547 Convert continuous line to pre-steps.
1549 Given a set of ``N`` points, convert to ``2N - 1`` points, which when
1550 connected linearly give a step function which changes values at the
1551 beginning of the intervals.
1553 Parameters
1554 ----------
1555 x : array
1556 The x location of the steps. May be empty.
1558 y1, ..., yp : array
1559 y arrays to be turned into steps; all must be the same length as ``x``.
1561 Returns
1562 -------
1563 array
1564 The x and y values converted to steps in the same order as the input;
1565 can be unpacked as ``x_out, y1_out, ..., yp_out``. If the input is
1566 length ``N``, each of these arrays will be length ``2N + 1``. For
1567 ``N=0``, the length will be 0.
1569 Examples
1570 --------
1571 >>> x_s, y1_s, y2_s = pts_to_prestep(x, y1, y2)
1572 """
1573 steps = np.zeros((1 + len(args), max(2 * len(x) - 1, 0)))
1574 # In all `pts_to_*step` functions, only assign once using *x* and *args*,
1575 # as converting to an array may be expensive.
1576 steps[0, 0::2] = x
1577 steps[0, 1::2] = steps[0, 0:-2:2]
1578 steps[1:, 0::2] = args
1579 steps[1:, 1::2] = steps[1:, 2::2]
1580 return steps
1583def pts_to_poststep(x, *args):
1584 """
1585 Convert continuous line to post-steps.
1587 Given a set of ``N`` points convert to ``2N + 1`` points, which when
1588 connected linearly give a step function which changes values at the end of
1589 the intervals.
1591 Parameters
1592 ----------
1593 x : array
1594 The x location of the steps. May be empty.
1596 y1, ..., yp : array
1597 y arrays to be turned into steps; all must be the same length as ``x``.
1599 Returns
1600 -------
1601 array
1602 The x and y values converted to steps in the same order as the input;
1603 can be unpacked as ``x_out, y1_out, ..., yp_out``. If the input is
1604 length ``N``, each of these arrays will be length ``2N + 1``. For
1605 ``N=0``, the length will be 0.
1607 Examples
1608 --------
1609 >>> x_s, y1_s, y2_s = pts_to_poststep(x, y1, y2)
1610 """
1611 steps = np.zeros((1 + len(args), max(2 * len(x) - 1, 0)))
1612 steps[0, 0::2] = x
1613 steps[0, 1::2] = steps[0, 2::2]
1614 steps[1:, 0::2] = args
1615 steps[1:, 1::2] = steps[1:, 0:-2:2]
1616 return steps
1619def pts_to_midstep(x, *args):
1620 """
1621 Convert continuous line to mid-steps.
1623 Given a set of ``N`` points convert to ``2N`` points which when connected
1624 linearly give a step function which changes values at the middle of the
1625 intervals.
1627 Parameters
1628 ----------
1629 x : array
1630 The x location of the steps. May be empty.
1632 y1, ..., yp : array
1633 y arrays to be turned into steps; all must be the same length as
1634 ``x``.
1636 Returns
1637 -------
1638 array
1639 The x and y values converted to steps in the same order as the input;
1640 can be unpacked as ``x_out, y1_out, ..., yp_out``. If the input is
1641 length ``N``, each of these arrays will be length ``2N``.
1643 Examples
1644 --------
1645 >>> x_s, y1_s, y2_s = pts_to_midstep(x, y1, y2)
1646 """
1647 steps = np.zeros((1 + len(args), 2 * len(x)))
1648 x = np.asanyarray(x)
1649 steps[0, 1:-1:2] = steps[0, 2::2] = (x[:-1] + x[1:]) / 2
1650 steps[0, :1] = x[:1] # Also works for zero-sized input.
1651 steps[0, -1:] = x[-1:]
1652 steps[1:, 0::2] = args
1653 steps[1:, 1::2] = steps[1:, 0::2]
1654 return steps
1657STEP_LOOKUP_MAP = {'default': lambda x, y: (x, y),
1658 'steps': pts_to_prestep,
1659 'steps-pre': pts_to_prestep,
1660 'steps-post': pts_to_poststep,
1661 'steps-mid': pts_to_midstep}
1664def index_of(y):
1665 """
1666 A helper function to create reasonable x values for the given *y*.
1668 This is used for plotting (x, y) if x values are not explicitly given.
1670 First try ``y.index`` (assuming *y* is a `pandas.Series`), if that
1671 fails, use ``range(len(y))``.
1673 This will be extended in the future to deal with more types of
1674 labeled data.
1676 Parameters
1677 ----------
1678 y : float or array-like
1680 Returns
1681 -------
1682 x, y : ndarray
1683 The x and y values to plot.
1684 """
1685 try:
1686 return y.index.to_numpy(), y.to_numpy()
1687 except AttributeError:
1688 pass
1689 try:
1690 y = _check_1d(y)
1691 except (np.VisibleDeprecationWarning, ValueError):
1692 # NumPy 1.19 will warn on ragged input, and we can't actually use it.
1693 pass
1694 else:
1695 return np.arange(y.shape[0], dtype=float), y
1696 raise ValueError('Input could not be cast to an at-least-1D NumPy array')
1699def safe_first_element(obj):
1700 """
1701 Return the first element in *obj*.
1703 This is an type-independent way of obtaining the first element,
1704 supporting both index access and the iterator protocol.
1705 """
1706 return _safe_first_finite(obj, skip_nonfinite=False)
1709def _safe_first_finite(obj, *, skip_nonfinite=True):
1710 """
1711 Return the first non-None (and optionally finite) element in *obj*.
1713 This is a method for internal use.
1715 This is an type-independent way of obtaining the first non-None element,
1716 supporting both index access and the iterator protocol.
1717 The first non-None element will be obtained when skip_none is True.
1718 """
1719 def safe_isfinite(val):
1720 if val is None:
1721 return False
1722 try:
1723 return np.isfinite(val) if np.isscalar(val) else True
1724 except TypeError:
1725 # This is something that numpy can not make heads or tails
1726 # of, assume "finite"
1727 return True
1728 if skip_nonfinite is False:
1729 if isinstance(obj, collections.abc.Iterator):
1730 # needed to accept `array.flat` as input.
1731 # np.flatiter reports as an instance of collections.Iterator
1732 # but can still be indexed via [].
1733 # This has the side effect of re-setting the iterator, but
1734 # that is acceptable.
1735 try:
1736 return obj[0]
1737 except TypeError:
1738 pass
1739 raise RuntimeError("matplotlib does not support generators "
1740 "as input")
1741 return next(iter(obj))
1742 elif isinstance(obj, np.flatiter):
1743 # TODO do the finite filtering on this
1744 return obj[0]
1745 elif isinstance(obj, collections.abc.Iterator):
1746 raise RuntimeError("matplotlib does not "
1747 "support generators as input")
1748 else:
1749 return next(val for val in obj if safe_isfinite(val))
1752def sanitize_sequence(data):
1753 """
1754 Convert dictview objects to list. Other inputs are returned unchanged.
1755 """
1756 return (list(data) if isinstance(data, collections.abc.MappingView)
1757 else data)
1760def normalize_kwargs(kw, alias_mapping=None):
1761 """
1762 Helper function to normalize kwarg inputs.
1764 Parameters
1765 ----------
1766 kw : dict or None
1767 A dict of keyword arguments. None is explicitly supported and treated
1768 as an empty dict, to support functions with an optional parameter of
1769 the form ``props=None``.
1771 alias_mapping : dict or Artist subclass or Artist instance, optional
1772 A mapping between a canonical name to a list of aliases, in order of
1773 precedence from lowest to highest.
1775 If the canonical value is not in the list it is assumed to have the
1776 highest priority.
1778 If an Artist subclass or instance is passed, use its properties alias
1779 mapping.
1781 Raises
1782 ------
1783 TypeError
1784 To match what Python raises if invalid arguments/keyword arguments are
1785 passed to a callable.
1786 """
1787 from matplotlib.artist import Artist
1789 if kw is None:
1790 return {}
1792 # deal with default value of alias_mapping
1793 if alias_mapping is None:
1794 alias_mapping = dict()
1795 elif (isinstance(alias_mapping, type) and issubclass(alias_mapping, Artist)
1796 or isinstance(alias_mapping, Artist)):
1797 alias_mapping = getattr(alias_mapping, "_alias_map", {})
1799 to_canonical = {alias: canonical
1800 for canonical, alias_list in alias_mapping.items()
1801 for alias in alias_list}
1802 canonical_to_seen = {}
1803 ret = {} # output dictionary
1805 for k, v in kw.items():
1806 canonical = to_canonical.get(k, k)
1807 if canonical in canonical_to_seen:
1808 raise TypeError(f"Got both {canonical_to_seen[canonical]!r} and "
1809 f"{k!r}, which are aliases of one another")
1810 canonical_to_seen[canonical] = k
1811 ret[canonical] = v
1813 return ret
1816@contextlib.contextmanager
1817def _lock_path(path):
1818 """
1819 Context manager for locking a path.
1821 Usage::
1823 with _lock_path(path):
1824 ...
1826 Another thread or process that attempts to lock the same path will wait
1827 until this context manager is exited.
1829 The lock is implemented by creating a temporary file in the parent
1830 directory, so that directory must exist and be writable.
1831 """
1832 path = Path(path)
1833 lock_path = path.with_name(path.name + ".matplotlib-lock")
1834 retries = 50
1835 sleeptime = 0.1
1836 for _ in range(retries):
1837 try:
1838 with lock_path.open("xb"):
1839 break
1840 except FileExistsError:
1841 time.sleep(sleeptime)
1842 else:
1843 raise TimeoutError("""\
1844Lock error: Matplotlib failed to acquire the following lock file:
1845 {}
1846This maybe due to another process holding this lock file. If you are sure no
1847other Matplotlib process is running, remove this file and try again.""".format(
1848 lock_path))
1849 try:
1850 yield
1851 finally:
1852 lock_path.unlink()
1855def _topmost_artist(
1856 artists,
1857 _cached_max=functools.partial(max, key=operator.attrgetter("zorder"))):
1858 """
1859 Get the topmost artist of a list.
1861 In case of a tie, return the *last* of the tied artists, as it will be
1862 drawn on top of the others. `max` returns the first maximum in case of
1863 ties, so we need to iterate over the list in reverse order.
1864 """
1865 return _cached_max(reversed(artists))
1868def _str_equal(obj, s):
1869 """
1870 Return whether *obj* is a string equal to string *s*.
1872 This helper solely exists to handle the case where *obj* is a numpy array,
1873 because in such cases, a naive ``obj == s`` would yield an array, which
1874 cannot be used in a boolean context.
1875 """
1876 return isinstance(obj, str) and obj == s
1879def _str_lower_equal(obj, s):
1880 """
1881 Return whether *obj* is a string equal, when lowercased, to string *s*.
1883 This helper solely exists to handle the case where *obj* is a numpy array,
1884 because in such cases, a naive ``obj == s`` would yield an array, which
1885 cannot be used in a boolean context.
1886 """
1887 return isinstance(obj, str) and obj.lower() == s
1890def _array_perimeter(arr):
1891 """
1892 Get the elements on the perimeter of *arr*.
1894 Parameters
1895 ----------
1896 arr : ndarray, shape (M, N)
1897 The input array.
1899 Returns
1900 -------
1901 ndarray, shape (2*(M - 1) + 2*(N - 1),)
1902 The elements on the perimeter of the array::
1904 [arr[0, 0], ..., arr[0, -1], ..., arr[-1, -1], ..., arr[-1, 0], ...]
1906 Examples
1907 --------
1908 >>> i, j = np.ogrid[:3, :4]
1909 >>> a = i*10 + j
1910 >>> a
1911 array([[ 0, 1, 2, 3],
1912 [10, 11, 12, 13],
1913 [20, 21, 22, 23]])
1914 >>> _array_perimeter(a)
1915 array([ 0, 1, 2, 3, 13, 23, 22, 21, 20, 10])
1916 """
1917 # note we use Python's half-open ranges to avoid repeating
1918 # the corners
1919 forward = np.s_[0:-1] # [0 ... -1)
1920 backward = np.s_[-1:0:-1] # [-1 ... 0)
1921 return np.concatenate((
1922 arr[0, forward],
1923 arr[forward, -1],
1924 arr[-1, backward],
1925 arr[backward, 0],
1926 ))
1929def _unfold(arr, axis, size, step):
1930 """
1931 Append an extra dimension containing sliding windows along *axis*.
1933 All windows are of size *size* and begin with every *step* elements.
1935 Parameters
1936 ----------
1937 arr : ndarray, shape (N_1, ..., N_k)
1938 The input array
1939 axis : int
1940 Axis along which the windows are extracted
1941 size : int
1942 Size of the windows
1943 step : int
1944 Stride between first elements of subsequent windows.
1946 Returns
1947 -------
1948 ndarray, shape (N_1, ..., 1 + (N_axis-size)/step, ..., N_k, size)
1950 Examples
1951 --------
1952 >>> i, j = np.ogrid[:3, :7]
1953 >>> a = i*10 + j
1954 >>> a
1955 array([[ 0, 1, 2, 3, 4, 5, 6],
1956 [10, 11, 12, 13, 14, 15, 16],
1957 [20, 21, 22, 23, 24, 25, 26]])
1958 >>> _unfold(a, axis=1, size=3, step=2)
1959 array([[[ 0, 1, 2],
1960 [ 2, 3, 4],
1961 [ 4, 5, 6]],
1962 [[10, 11, 12],
1963 [12, 13, 14],
1964 [14, 15, 16]],
1965 [[20, 21, 22],
1966 [22, 23, 24],
1967 [24, 25, 26]]])
1968 """
1969 new_shape = [*arr.shape, size]
1970 new_strides = [*arr.strides, arr.strides[axis]]
1971 new_shape[axis] = (new_shape[axis] - size) // step + 1
1972 new_strides[axis] = new_strides[axis] * step
1973 return np.lib.stride_tricks.as_strided(arr,
1974 shape=new_shape,
1975 strides=new_strides,
1976 writeable=False)
1979def _array_patch_perimeters(x, rstride, cstride):
1980 """
1981 Extract perimeters of patches from *arr*.
1983 Extracted patches are of size (*rstride* + 1) x (*cstride* + 1) and
1984 share perimeters with their neighbors. The ordering of the vertices matches
1985 that returned by ``_array_perimeter``.
1987 Parameters
1988 ----------
1989 x : ndarray, shape (N, M)
1990 Input array
1991 rstride : int
1992 Vertical (row) stride between corresponding elements of each patch
1993 cstride : int
1994 Horizontal (column) stride between corresponding elements of each patch
1996 Returns
1997 -------
1998 ndarray, shape (N/rstride * M/cstride, 2 * (rstride + cstride))
1999 """
2000 assert rstride > 0 and cstride > 0
2001 assert (x.shape[0] - 1) % rstride == 0
2002 assert (x.shape[1] - 1) % cstride == 0
2003 # We build up each perimeter from four half-open intervals. Here is an
2004 # illustrated explanation for rstride == cstride == 3
2005 #
2006 # T T T R
2007 # L R
2008 # L R
2009 # L B B B
2010 #
2011 # where T means that this element will be in the top array, R for right,
2012 # B for bottom and L for left. Each of the arrays below has a shape of:
2013 #
2014 # (number of perimeters that can be extracted vertically,
2015 # number of perimeters that can be extracted horizontally,
2016 # cstride for top and bottom and rstride for left and right)
2017 #
2018 # Note that _unfold doesn't incur any memory copies, so the only costly
2019 # operation here is the np.concatenate.
2020 top = _unfold(x[:-1:rstride, :-1], 1, cstride, cstride)
2021 bottom = _unfold(x[rstride::rstride, 1:], 1, cstride, cstride)[..., ::-1]
2022 right = _unfold(x[:-1, cstride::cstride], 0, rstride, rstride)
2023 left = _unfold(x[1:, :-1:cstride], 0, rstride, rstride)[..., ::-1]
2024 return (np.concatenate((top, right, bottom, left), axis=2)
2025 .reshape(-1, 2 * (rstride + cstride)))
2028@contextlib.contextmanager
2029def _setattr_cm(obj, **kwargs):
2030 """
2031 Temporarily set some attributes; restore original state at context exit.
2032 """
2033 sentinel = object()
2034 origs = {}
2035 for attr in kwargs:
2036 orig = getattr(obj, attr, sentinel)
2037 if attr in obj.__dict__ or orig is sentinel:
2038 # if we are pulling from the instance dict or the object
2039 # does not have this attribute we can trust the above
2040 origs[attr] = orig
2041 else:
2042 # if the attribute is not in the instance dict it must be
2043 # from the class level
2044 cls_orig = getattr(type(obj), attr)
2045 # if we are dealing with a property (but not a general descriptor)
2046 # we want to set the original value back.
2047 if isinstance(cls_orig, property):
2048 origs[attr] = orig
2049 # otherwise this is _something_ we are going to shadow at
2050 # the instance dict level from higher up in the MRO. We
2051 # are going to assume we can delattr(obj, attr) to clean
2052 # up after ourselves. It is possible that this code will
2053 # fail if used with a non-property custom descriptor which
2054 # implements __set__ (and __delete__ does not act like a
2055 # stack). However, this is an internal tool and we do not
2056 # currently have any custom descriptors.
2057 else:
2058 origs[attr] = sentinel
2060 try:
2061 for attr, val in kwargs.items():
2062 setattr(obj, attr, val)
2063 yield
2064 finally:
2065 for attr, orig in origs.items():
2066 if orig is sentinel:
2067 delattr(obj, attr)
2068 else:
2069 setattr(obj, attr, orig)
2072class _OrderedSet(collections.abc.MutableSet):
2073 def __init__(self):
2074 self._od = collections.OrderedDict()
2076 def __contains__(self, key):
2077 return key in self._od
2079 def __iter__(self):
2080 return iter(self._od)
2082 def __len__(self):
2083 return len(self._od)
2085 def add(self, key):
2086 self._od.pop(key, None)
2087 self._od[key] = None
2089 def discard(self, key):
2090 self._od.pop(key, None)
2093# Agg's buffers are unmultiplied RGBA8888, which neither PyQt<=5.1 nor cairo
2094# support; however, both do support premultiplied ARGB32.
2097def _premultiplied_argb32_to_unmultiplied_rgba8888(buf):
2098 """
2099 Convert a premultiplied ARGB32 buffer to an unmultiplied RGBA8888 buffer.
2100 """
2101 rgba = np.take( # .take() ensures C-contiguity of the result.
2102 buf,
2103 [2, 1, 0, 3] if sys.byteorder == "little" else [1, 2, 3, 0], axis=2)
2104 rgb = rgba[..., :-1]
2105 alpha = rgba[..., -1]
2106 # Un-premultiply alpha. The formula is the same as in cairo-png.c.
2107 mask = alpha != 0
2108 for channel in np.rollaxis(rgb, -1):
2109 channel[mask] = (
2110 (channel[mask].astype(int) * 255 + alpha[mask] // 2)
2111 // alpha[mask])
2112 return rgba
2115def _unmultiplied_rgba8888_to_premultiplied_argb32(rgba8888):
2116 """
2117 Convert an unmultiplied RGBA8888 buffer to a premultiplied ARGB32 buffer.
2118 """
2119 if sys.byteorder == "little":
2120 argb32 = np.take(rgba8888, [2, 1, 0, 3], axis=2)
2121 rgb24 = argb32[..., :-1]
2122 alpha8 = argb32[..., -1:]
2123 else:
2124 argb32 = np.take(rgba8888, [3, 0, 1, 2], axis=2)
2125 alpha8 = argb32[..., :1]
2126 rgb24 = argb32[..., 1:]
2127 # Only bother premultiplying when the alpha channel is not fully opaque,
2128 # as the cost is not negligible. The unsafe cast is needed to do the
2129 # multiplication in-place in an integer buffer.
2130 if alpha8.min() != 0xff:
2131 np.multiply(rgb24, alpha8 / 0xff, out=rgb24, casting="unsafe")
2132 return argb32
2135def _get_nonzero_slices(buf):
2136 """
2137 Return the bounds of the nonzero region of a 2D array as a pair of slices.
2139 ``buf[_get_nonzero_slices(buf)]`` is the smallest sub-rectangle in *buf*
2140 that encloses all non-zero entries in *buf*. If *buf* is fully zero, then
2141 ``(slice(0, 0), slice(0, 0))`` is returned.
2142 """
2143 x_nz, = buf.any(axis=0).nonzero()
2144 y_nz, = buf.any(axis=1).nonzero()
2145 if len(x_nz) and len(y_nz):
2146 l, r = x_nz[[0, -1]]
2147 b, t = y_nz[[0, -1]]
2148 return slice(b, t + 1), slice(l, r + 1)
2149 else:
2150 return slice(0, 0), slice(0, 0)
2153def _pformat_subprocess(command):
2154 """Pretty-format a subprocess command for printing/logging purposes."""
2155 return (command if isinstance(command, str)
2156 else " ".join(shlex.quote(os.fspath(arg)) for arg in command))
2159def _check_and_log_subprocess(command, logger, **kwargs):
2160 """
2161 Run *command*, returning its stdout output if it succeeds.
2163 If it fails (exits with nonzero return code), raise an exception whose text
2164 includes the failed command and captured stdout and stderr output.
2166 Regardless of the return code, the command is logged at DEBUG level on
2167 *logger*. In case of success, the output is likewise logged.
2168 """
2169 logger.debug('%s', _pformat_subprocess(command))
2170 proc = subprocess.run(
2171 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
2172 if proc.returncode:
2173 stdout = proc.stdout
2174 if isinstance(stdout, bytes):
2175 stdout = stdout.decode()
2176 stderr = proc.stderr
2177 if isinstance(stderr, bytes):
2178 stderr = stderr.decode()
2179 raise RuntimeError(
2180 f"The command\n"
2181 f" {_pformat_subprocess(command)}\n"
2182 f"failed and generated the following output:\n"
2183 f"{stdout}\n"
2184 f"and the following error:\n"
2185 f"{stderr}")
2186 if proc.stdout:
2187 logger.debug("stdout:\n%s", proc.stdout)
2188 if proc.stderr:
2189 logger.debug("stderr:\n%s", proc.stderr)
2190 return proc.stdout
2193def _backend_module_name(name):
2194 """
2195 Convert a backend name (either a standard backend -- "Agg", "TkAgg", ... --
2196 or a custom backend -- "module://...") to the corresponding module name).
2197 """
2198 return (name[9:] if name.startswith("module://")
2199 else "matplotlib.backends.backend_{}".format(name.lower()))
2202def _setup_new_guiapp():
2203 """
2204 Perform OS-dependent setup when Matplotlib creates a new GUI application.
2205 """
2206 # Windows: If not explicit app user model id has been set yet (so we're not
2207 # already embedded), then set it to "matplotlib", so that taskbar icons are
2208 # correct.
2209 try:
2210 _c_internal_utils.Win32_GetCurrentProcessExplicitAppUserModelID()
2211 except OSError:
2212 _c_internal_utils.Win32_SetCurrentProcessExplicitAppUserModelID(
2213 "matplotlib")
2216def _format_approx(number, precision):
2217 """
2218 Format the number with at most the number of decimals given as precision.
2219 Remove trailing zeros and possibly the decimal point.
2220 """
2221 return f'{number:.{precision}f}'.rstrip('0').rstrip('.') or '0'
2224def _g_sig_digits(value, delta):
2225 """
2226 Return the number of significant digits to %g-format *value*, assuming that
2227 it is known with an error of *delta*.
2228 """
2229 if delta == 0:
2230 # delta = 0 may occur when trying to format values over a tiny range;
2231 # in that case, replace it by the distance to the closest float.
2232 delta = abs(np.spacing(value))
2233 # If e.g. value = 45.67 and delta = 0.02, then we want to round to 2 digits
2234 # after the decimal point (floor(log10(0.02)) = -2); 45.67 contributes 2
2235 # digits before the decimal point (floor(log10(45.67)) + 1 = 2): the total
2236 # is 4 significant digits. A value of 0 contributes 1 "digit" before the
2237 # decimal point.
2238 # For inf or nan, the precision doesn't matter.
2239 return max(
2240 0,
2241 (math.floor(math.log10(abs(value))) + 1 if value else 1)
2242 - math.floor(math.log10(delta))) if math.isfinite(value) else 0
2245def _unikey_or_keysym_to_mplkey(unikey, keysym):
2246 """
2247 Convert a Unicode key or X keysym to a Matplotlib key name.
2249 The Unicode key is checked first; this avoids having to list most printable
2250 keysyms such as ``EuroSign``.
2251 """
2252 # For non-printable characters, gtk3 passes "\0" whereas tk passes an "".
2253 if unikey and unikey.isprintable():
2254 return unikey
2255 key = keysym.lower()
2256 if key.startswith("kp_"): # keypad_x (including kp_enter).
2257 key = key[3:]
2258 if key.startswith("page_"): # page_{up,down}
2259 key = key.replace("page_", "page")
2260 if key.endswith(("_l", "_r")): # alt_l, ctrl_l, shift_l.
2261 key = key[:-2]
2262 key = {
2263 "return": "enter",
2264 "prior": "pageup", # Used by tk.
2265 "next": "pagedown", # Used by tk.
2266 }.get(key, key)
2267 return key
2270@functools.lru_cache(None)
2271def _make_class_factory(mixin_class, fmt, attr_name=None):
2272 """
2273 Return a function that creates picklable classes inheriting from a mixin.
2275 After ::
2277 factory = _make_class_factory(FooMixin, fmt, attr_name)
2278 FooAxes = factory(Axes)
2280 ``Foo`` is a class that inherits from ``FooMixin`` and ``Axes`` and **is
2281 picklable** (picklability is what differentiates this from a plain call to
2282 `type`). Its ``__name__`` is set to ``fmt.format(Axes.__name__)`` and the
2283 base class is stored in the ``attr_name`` attribute, if not None.
2285 Moreover, the return value of ``factory`` is memoized: calls with the same
2286 ``Axes`` class always return the same subclass.
2287 """
2289 @functools.lru_cache(None)
2290 def class_factory(axes_class):
2291 # if we have already wrapped this class, declare victory!
2292 if issubclass(axes_class, mixin_class):
2293 return axes_class
2295 # The parameter is named "axes_class" for backcompat but is really just
2296 # a base class; no axes semantics are used.
2297 base_class = axes_class
2299 class subcls(mixin_class, base_class):
2300 # Better approximation than __module__ = "matplotlib.cbook".
2301 __module__ = mixin_class.__module__
2303 def __reduce__(self):
2304 return (_picklable_class_constructor,
2305 (mixin_class, fmt, attr_name, base_class),
2306 self.__getstate__())
2308 subcls.__name__ = subcls.__qualname__ = fmt.format(base_class.__name__)
2309 if attr_name is not None:
2310 setattr(subcls, attr_name, base_class)
2311 return subcls
2313 class_factory.__module__ = mixin_class.__module__
2314 return class_factory
2317def _picklable_class_constructor(mixin_class, fmt, attr_name, base_class):
2318 """Internal helper for _make_class_factory."""
2319 factory = _make_class_factory(mixin_class, fmt, attr_name)
2320 cls = factory(base_class)
2321 return cls.__new__(cls)
2324def _unpack_to_numpy(x):
2325 """Internal helper to extract data from e.g. pandas and xarray objects."""
2326 if isinstance(x, np.ndarray):
2327 # If numpy, return directly
2328 return x
2329 if hasattr(x, 'to_numpy'):
2330 # Assume that any function to_numpy() do actually return a numpy array
2331 return x.to_numpy()
2332 if hasattr(x, 'values'):
2333 xtmp = x.values
2334 # For example a dict has a 'values' attribute, but it is not a property
2335 # so in this case we do not want to return a function
2336 if isinstance(xtmp, np.ndarray):
2337 return xtmp
2338 return x