| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- """
- Core functions and attributes for the matplotlib style library:
- ``use``
- Select style sheet to override the current matplotlib settings.
- ``context``
- Context manager to use a style sheet temporarily.
- ``available``
- List available style sheets.
- ``library``
- A dictionary of style names and matplotlib settings.
- """
- import contextlib
- import importlib.resources
- import logging
- import os
- from pathlib import Path
- import warnings
- import matplotlib as mpl
- from matplotlib import _api, _docstring, _rc_params_in_file, rcParamsDefault
- _log = logging.getLogger(__name__)
- __all__ = ['use', 'context', 'available', 'library', 'reload_library']
- BASE_LIBRARY_PATH = os.path.join(mpl.get_data_path(), 'stylelib')
- # Users may want multiple library paths, so store a list of paths.
- USER_LIBRARY_PATHS = [os.path.join(mpl.get_configdir(), 'stylelib')]
- STYLE_EXTENSION = 'mplstyle'
- # A list of rcParams that should not be applied from styles
- STYLE_BLACKLIST = {
- 'interactive', 'backend', 'webagg.port', 'webagg.address',
- 'webagg.port_retries', 'webagg.open_in_browser', 'backend_fallback',
- 'toolbar', 'timezone', 'figure.max_open_warning',
- 'figure.raise_window', 'savefig.directory', 'tk.window_focus',
- 'docstring.hardcopy', 'date.epoch'}
- @_docstring.Substitution(
- "\n".join(map("- {}".format, sorted(STYLE_BLACKLIST, key=str.lower)))
- )
- def use(style):
- """
- Use Matplotlib style settings from a style specification.
- The style name of 'default' is reserved for reverting back to
- the default style settings.
- .. note::
- This updates the `.rcParams` with the settings from the style.
- `.rcParams` not defined in the style are kept.
- Parameters
- ----------
- style : str, dict, Path or list
- A style specification. Valid options are:
- str
- - One of the style names in `.style.available` (a builtin style or
- a style installed in the user library path).
- - A dotted name of the form "package.style_name"; in that case,
- "package" should be an importable Python package name, e.g. at
- ``/path/to/package/__init__.py``; the loaded style file is
- ``/path/to/package/style_name.mplstyle``. (Style files in
- subpackages are likewise supported.)
- - The path or URL to a style file, which gets loaded by
- `.rc_params_from_file`.
- dict
- A mapping of key/value pairs for `matplotlib.rcParams`.
- Path
- The path to a style file, which gets loaded by
- `.rc_params_from_file`.
- list
- A list of style specifiers (str, Path or dict), which are applied
- from first to last in the list.
- Notes
- -----
- The following `.rcParams` are not related to style and will be ignored if
- found in a style specification:
- %s
- """
- if isinstance(style, (str, Path)) or hasattr(style, 'keys'):
- # If name is a single str, Path or dict, make it a single element list.
- styles = [style]
- else:
- styles = style
- style_alias = {'mpl20': 'default', 'mpl15': 'classic'}
- for style in styles:
- if isinstance(style, str):
- style = style_alias.get(style, style)
- if style == "default":
- # Deprecation warnings were already handled when creating
- # rcParamsDefault, no need to reemit them here.
- with _api.suppress_matplotlib_deprecation_warning():
- # don't trigger RcParams.__getitem__('backend')
- style = {k: rcParamsDefault[k] for k in rcParamsDefault
- if k not in STYLE_BLACKLIST}
- elif style in library:
- style = library[style]
- elif "." in style:
- pkg, _, name = style.rpartition(".")
- try:
- path = importlib.resources.files(pkg) / f"{name}.{STYLE_EXTENSION}"
- style = _rc_params_in_file(path)
- except (ModuleNotFoundError, OSError, TypeError) as exc:
- # There is an ambiguity whether a dotted name refers to a
- # package.style_name or to a dotted file path. Currently,
- # we silently try the first form and then the second one;
- # in the future, we may consider forcing file paths to
- # either use Path objects or be prepended with "./" and use
- # the slash as marker for file paths.
- pass
- if isinstance(style, (str, Path)):
- try:
- style = _rc_params_in_file(style)
- except OSError as err:
- raise OSError(
- f"{style!r} is not a valid package style, path of style "
- f"file, URL of style file, or library style name (library "
- f"styles are listed in `style.available`)") from err
- filtered = {}
- for k in style: # don't trigger RcParams.__getitem__('backend')
- if k in STYLE_BLACKLIST:
- _api.warn_external(
- f"Style includes a parameter, {k!r}, that is not "
- f"related to style. Ignoring this parameter.")
- else:
- filtered[k] = style[k]
- mpl.rcParams.update(filtered)
- @contextlib.contextmanager
- def context(style, after_reset=False):
- """
- Context manager for using style settings temporarily.
- Parameters
- ----------
- style : str, dict, Path or list
- A style specification. Valid options are:
- str
- - One of the style names in `.style.available` (a builtin style or
- a style installed in the user library path).
- - A dotted name of the form "package.style_name"; in that case,
- "package" should be an importable Python package name, e.g. at
- ``/path/to/package/__init__.py``; the loaded style file is
- ``/path/to/package/style_name.mplstyle``. (Style files in
- subpackages are likewise supported.)
- - The path or URL to a style file, which gets loaded by
- `.rc_params_from_file`.
- dict
- A mapping of key/value pairs for `matplotlib.rcParams`.
- Path
- The path to a style file, which gets loaded by
- `.rc_params_from_file`.
- list
- A list of style specifiers (str, Path or dict), which are applied
- from first to last in the list.
- after_reset : bool
- If True, apply style after resetting settings to their defaults;
- otherwise, apply style on top of the current settings.
- """
- with mpl.rc_context():
- if after_reset:
- mpl.rcdefaults()
- use(style)
- yield
- def update_user_library(library):
- """Update style library with user-defined rc files."""
- for stylelib_path in map(os.path.expanduser, USER_LIBRARY_PATHS):
- styles = read_style_directory(stylelib_path)
- update_nested_dict(library, styles)
- return library
- def read_style_directory(style_dir):
- """Return dictionary of styles defined in *style_dir*."""
- styles = dict()
- for path in Path(style_dir).glob(f"*.{STYLE_EXTENSION}"):
- with warnings.catch_warnings(record=True) as warns:
- styles[path.stem] = _rc_params_in_file(path)
- for w in warns:
- _log.warning('In %s: %s', path, w.message)
- return styles
- def update_nested_dict(main_dict, new_dict):
- """
- Update nested dict (only level of nesting) with new values.
- Unlike `dict.update`, this assumes that the values of the parent dict are
- dicts (or dict-like), so you shouldn't replace the nested dict if it
- already exists. Instead you should update the sub-dict.
- """
- # update named styles specified by user
- for name, rc_dict in new_dict.items():
- main_dict.setdefault(name, {}).update(rc_dict)
- return main_dict
- # Load style library
- # ==================
- _base_library = read_style_directory(BASE_LIBRARY_PATH)
- library = {}
- available = []
- def reload_library():
- """Reload the style library."""
- library.clear()
- library.update(update_user_library(_base_library))
- available[:] = sorted(library.keys())
- reload_library()
|