246 lines
8.4 KiB
Python
246 lines
8.4 KiB
Python
|
"""
|
||
|
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 logging
|
||
|
import os
|
||
|
from pathlib import Path
|
||
|
import sys
|
||
|
import warnings
|
||
|
|
||
|
if sys.version_info >= (3, 10):
|
||
|
import importlib.resources as importlib_resources
|
||
|
else:
|
||
|
# Even though Py3.9 has importlib.resources, it doesn't properly handle
|
||
|
# modules added in sys.path.
|
||
|
import importlib_resources
|
||
|
|
||
|
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()
|