#!/usr/bin/env python3
"""
Tools for setting up ProPlot and configuring global settings.
See the :ref:`configuration guide <ug_config>` for details.
"""
# NOTE: The matplotlib analogue to this file is actually __init__.py
# but it makes more sense to have all the setup actions in a separate file
# so the namespace of the top-level module is unpolluted.
# NOTE: Why also load colormaps and cycles in this file and not colors.py?
# Because I think it makes sense to have all the code that "runs" (i.e. not
# just definitions) in the same place, and I was having issues with circular
# dependencies and where import order of __init__.py was affecting behavior.
import logging
import os
import re
from collections import namedtuple
import cycler
import matplotlib as mpl
import matplotlib.colors as mcolors
import matplotlib.font_manager as mfonts
import matplotlib.mathtext # noqa
import matplotlib.rcsetup as msetup
import matplotlib.style.core as mstyle
import numpy as np
from . import colors as pcolors
from .internals import ic # noqa: F401
from .internals import _not_none, docstring, rcsetup, timers, warnings
from .utils import to_xyz, units
try:
from matplotlib.cbook import _suppress_matplotlib_deprecation_warning # mpl<3.4.0
except ImportError:
from matplotlib._api import suppress_matplotlib_deprecation_warning as _suppress_matplotlib_deprecation_warning # noqa: E501
try:
from IPython import get_ipython
except ImportError:
def get_ipython():
return
__all__ = [
'rc',
'RcConfigurator',
'register_cmaps',
'register_cycles',
'register_colors',
'register_fonts',
'config_inline_backend',
'use_style',
'inline_backend_fmt', # deprecated
]
logger = logging.getLogger('matplotlib.mathtext')
logger.setLevel(logging.ERROR) # suppress warnings!
# Dictionaries used to track settings
rc_proplot = rcsetup._rc_proplot_default.copy()
rc_matplotlib = mpl.rcParams # PEP8 4 lyfe
_RcContext = namedtuple('RcContext', ('mode', 'kwargs', 'rc_new', 'rc_old'))
RcParams = mpl.RcParams # the special class
# Misc constants
# TODO: Use explicit validators for specific settings like matplotlib.
REGEX_STRING = re.compile('\\A(\'.*\'|".*")\\Z')
REGEX_POINTS = re.compile(
r'\A(?!font.mono|colorbar|subplots|pdf|ps).*(width|space|size|pad|len)\Z'
)
FONT_KEYS = (
'abc.size', 'axes.labelsize', 'axes.titlesize', 'figure.titlesize',
'xtick.labelsize', 'ytick.labelsize', 'tick.labelsize', 'grid.labelsize',
'bottomlabel.size', 'leftlabel.size', 'rightlabel.size', 'toplabel.size',
'suptitle.size', 'title.size', 'text.labelsize', 'text.titlesize',
'legend.fontsize', 'legend.title_fontsize'
)
COLORS_OPEN = {} # populated during register_colors
COLORS_XKCD = {} # populated during register_colors
COLORS_BASE = {
**mcolors.BASE_COLORS, # shorthand names like 'r', 'g', etc.
'blue': (0, 0, 1),
'green': (0, 0.5, 0),
'red': (1, 0, 0),
'cyan': (0, 0.75, 0.75),
'magenta': (0.75, 0, 0.75),
'yellow': (0.75, 0.75, 0),
'black': (0, 0, 0),
'white': (1, 1, 1),
}
COLORS_REMOVE = ( # filter these out, let's try to be professional here...
'shit', 'poop', 'poo', 'pee', 'piss', 'puke', 'vomit', 'snot',
'booger', 'bile', 'diarrhea',
)
COLORS_TRANSLATE = ( # prevent registering similar-sounding names
('/', ' '),
("'s", ''),
('forrest', 'forest'), # survey typo?
('reddish', 'red'), # remove 'ish'
('purplish', 'purple'),
('bluish', 'blue'),
('ish ', ' '),
('grey', 'gray'), # 'Murica
('pinky', 'pink'),
('greeny', 'green'),
('bluey', 'blue'),
('purply', 'purple'),
('purpley', 'purple'),
('yellowy', 'yellow'),
('robin egg', 'robins egg'),
('egg blue', 'egg'),
('bluegray', 'blue gray'),
('grayblue', 'gray blue'),
('lightblue', 'light blue'),
('yellowgreen', 'yellow green'),
('yelloworange', 'yellow orange'),
)
COLORS_ADD = (
*( # common fancy names or natural names
'charcoal', 'tomato', 'burgundy', 'maroon', 'burgundy', 'lavendar',
'taupe', 'ocre', 'sand', 'stone', 'earth', 'sand brown', 'sienna',
'terracotta', 'moss', 'crimson', 'mauve', 'rose', 'teal', 'forest',
'grass', 'sage', 'pine', 'vermillion', 'russet', 'cerise', 'avocado',
'wine', 'brick', 'umber', 'mahogany', 'puce', 'grape', 'blurple',
'cranberry', 'sand', 'aqua', 'jade', 'coral', 'olive', 'magenta',
'turquoise', 'sea blue', 'royal blue', 'slate blue', 'slate grey',
'baby blue', 'salmon', 'beige', 'peach', 'mustard', 'lime', 'indigo',
'cornflower', 'marine', 'cloudy blue', 'tangerine', 'scarlet', 'navy',
'cool grey', 'warm grey', 'chocolate', 'raspberry', 'denim',
'gunmetal', 'midnight', 'chartreuse', 'ivory', 'khaki', 'plum',
'silver', 'tan', 'wheat', 'buff', 'bisque', 'cerulean',
),
*( # common combos
'red orange', 'yellow orange', 'yellow green',
'blue green', 'blue violet', 'red violet',
),
*( # common names
prefix + color
for color in (
'red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet',
'brown', 'grey'
)
for prefix in ('', 'light ', 'dark ', 'medium ', 'pale ')
)
)
_config_docstring = """
user : bool, optional
Whether to reload user {name}. Default is ``True``.
default : bool, optional
Whether to reload default proplot {name}. Default is ``False``.
"""
docstring.snippets['register_cmaps.params'] = _config_docstring.format(name='colormaps')
docstring.snippets['register_cycles.params'] = _config_docstring.format(name='cycles')
docstring.snippets['register_colors.params'] = _config_docstring.format(name='colors')
docstring.snippets['rc.params'] = """
local : bool, optional
Whether to reload ``.proplotrc`` settings in this directory and parent
directories. Default is ``True``.
user : bool, optional
Whether to reload ``~/.proplotrc`` user settings. Default is ``True``.
default : bool, optional
Whether to reload default proplot settings. Default is ``True``.
"""
docstring.snippets['register.ext_table'] = """
Valid file extensions are as follows:
================== =====================================================================================================================================================================================================================
Extension Description
================== =====================================================================================================================================================================================================================
``.hex`` List of HEX strings in any format (comma-separated, separate lines, with double quotes... anything goes).
``.xml`` XML files with ``<Point .../>`` tags specifying ``x``, ``r``, ``g``, ``b``, and (optionally) ``o`` parameters, where ``x`` is the coordinate and the rest are the red, blue, green, and opacity channel values.
``.rgb``, ``.txt`` 3-4 column table of red, blue, green, and (optionally) opacity channel values, delimited by commas or spaces. If values larger than 1 are detected, they are assumed to be on the 0-255 scale and are divided by 255.
================== =====================================================================================================================================================================================================================
""" # noqa: E501
def _get_data_paths(subfolder, user=True, default=True, reverse=False):
"""
Return data folder paths in reverse order of precedence.
"""
# When loading colormaps, cycles, and colors, files in the latter
# directories overwrite files in the former directories. When loading
# fonts, the resulting paths need to be *reversed*.
paths = []
if user:
paths.append(os.path.join(os.path.dirname(__file__), subfolder))
if default:
paths.append(os.path.join(os.path.expanduser('~'), '.proplot', subfolder))
if reverse:
paths = paths[::-1]
return paths
def _iter_data_paths(subfolder, **kwargs):
"""
Iterate over all files in the data paths. Also yield an index indicating
whether these are default ProPlot files or user files.
"""
for i, path in enumerate(_get_data_paths(subfolder, **kwargs)):
for dirname, dirnames, filenames in os.walk(path):
for filename in filenames:
if filename[0] == '.': # UNIX-style hidden files
continue
yield i, dirname, filename
[docs]class RcConfigurator(object):
"""
Dictionary-like class for managing matplotlib's `builtin settings <rc_matplotlib>`_
and ProPlot's :ref:`added settings <rc_proplot>`. When ProPlot is imported,
this class is instantiated as the `rc` object and the ProPlot default settings
and ``.proplotrc`` user overrides are applied. See the
:ref:`configuration guide <ug_config>` for details.
"""
def __repr__(self):
rcdict = type('rc', (dict,), {})({ # encapsulate params in temporary class
key: value for key, value in rc_proplot.items()
if '.' not in key # show short names
})
string = type(rc_matplotlib).__repr__(rcdict)
return string.strip()[:-2] + ',\n ... <rcParams> ...\n })'
def __str__(self):
rcdict = type('rc', (dict,), {})({
key: value for key, value in rc_proplot.items()
if '.' not in key # show short names
})
string = type(rc_matplotlib).__str__(rcdict)
return string + '\n... <rcParams> ...'
def __iter__(self): # lets us build dict
"""
Iterate over keys and values of matplotlib and proplot settings.
"""
for key in sorted((*rc_proplot, *rc_matplotlib)):
yield key, self[key]
def __contains__(self, key):
"""
Test whether key exists as matplotlib or proplot setting.
"""
return key in rc_proplot or key in rc_matplotlib
@docstring.add_snippets
def __init__(self, local=True, user=True, default=True):
"""
Parameters
----------
%(rc.params)s
"""
self._context = []
self.reset(local=local, user=user, default=default)
def __enter__(self):
"""
Apply settings from the most recent context block.
"""
if not self._context:
raise RuntimeError(
'rc object must be initialized for context block '
'using rc.context().'
)
context = self._context[-1]
kwargs = context.kwargs
rc_new = context.rc_new # used for context-based _get_item
rc_old = context.rc_old # used to re-apply settings without copying whole dict
for key, value in kwargs.items():
kw_proplot, kw_matplotlib = self._get_synced_params(key, value)
for rc_dict, kw_new in zip(
(rc_proplot, rc_matplotlib),
(kw_proplot, kw_matplotlib),
):
for key, value in kw_new.items():
rc_old[key] = rc_dict[key]
rc_new[key] = rc_dict[key] = value
def __exit__(self, *args): # noqa: U100
"""
Restore settings from the most recent context block.
"""
if not self._context:
raise RuntimeError(
'rc object must be initialized for context block '
'using rc.context().'
)
context = self._context[-1]
for key, value in context.rc_old.items():
kw_proplot, kw_matplotlib = self._get_synced_params(key, value)
rc_proplot.update(kw_proplot)
rc_matplotlib.update(kw_matplotlib)
del self._context[-1]
def __delitem__(self, item): # noqa: 100
"""
Raise an error. This enforces pseudo-immutability.
"""
raise RuntimeError('rc settings cannot be deleted.')
def __delattr__(self, item): # noqa: 100
"""
Raise an error. This enforces pseudo-immutability.
"""
raise RuntimeError('rc settings cannot be deleted.')
[docs] def __getattr__(self, attr):
"""
Pass the attribute to `~RcConfigurator.__getitem__` and return
the result.
"""
if attr[:1] == '_':
return super().__getattribute__(attr)
else:
return self[attr]
[docs] def __getitem__(self, key):
"""
Return a `builtin matplotlib setting <rc_matplotlib>`_
or a ProPlot :ref:`added setting <rc_proplot>`.
"""
key = self._sanitize_key(key)
if key is None: # means key was *removed*, warnings was issued
return None
for kw in (rc_proplot, rc_matplotlib):
try:
return kw[key]
except KeyError:
continue
raise KeyError(f'Invalid setting name {key!r}.')
[docs] def __setattr__(self, attr, value):
"""
Pass the attribute and value to `~RcConfigurator.__setitem__`.
"""
if attr[:1] == '_':
super().__setattr__(attr, value)
else:
self.__setitem__(attr, value)
[docs] def __setitem__(self, key, value):
"""
Modify a `builtin matplotlib setting <rc_matplotlib>`_ or
a ProPlot :ref:`added setting <rc_proplot>`.
"""
kw_proplot, kw_matplotlib = self._get_synced_params(key, value)
rc_proplot.update(kw_proplot)
rc_matplotlib.update(kw_matplotlib)
def _get_item(self, key, mode=None):
"""
As with `~RcConfigurator.__getitem__` but the search is limited
based on the context mode and ``None`` is returned if the key is not
found in the dictionaries.
"""
if mode is None:
mode = self._get_context_mode()
cache = tuple(context.rc_new for context in self._context)
if mode == 0:
rcdicts = (*cache, rc_proplot, rc_matplotlib)
elif mode == 1:
rcdicts = (*cache, rc_proplot) # custom only!
elif mode == 2:
rcdicts = (*cache,)
else:
raise KeyError(f'Invalid caching mode {mode!r}.')
for rcdict in rcdicts:
if not rcdict:
continue
try:
return rcdict[key]
except KeyError:
continue
if mode == 0:
raise KeyError(f'Invalid setting name {key!r}.')
else:
return
def _get_context_mode(self):
"""
Return highest (least permissive) context mode.
"""
return max((context.mode for context in self._context), default=0)
def _get_synced_params(self, key, value):
"""
Return dictionaries for updating the `rc_proplot`
and `rc_matplotlib` properties associated with this key.
"""
key = self._sanitize_key(key)
if key is None: # means setting was removed
return {}, {}, {}
keys = (key,) + rcsetup._rc_children.get(key, ()) # settings to change
value = self._sanitize_value(value)
kw_proplot = {} # custom properties that global setting applies to
kw_matplotlib = {} # builtin properties that global setting applies to
# Permit arbitary units for builtin matplotlib params
# Props matching the below strings use the units 'points'.
# See: https://matplotlib.org/users/customizing.html
# TODO: Incorporate into more sophisticated validation system
if REGEX_POINTS.match(key):
if key in FONT_KEYS and value in mfonts.font_scalings:
pass
elif key.startswith('legend') and not key.endswith('fontsize'):
value = units(value, 'em') # scaled by font size
else:
value = units(value, 'pt') # untis points fontsize='10px'
# Special key: configure inline backend
if key == 'inlinefmt':
config_inline_backend(value)
# Special key: apply stylesheet
elif key == 'style':
if value is not None:
kw_matplotlib, kw_proplot = _get_style_dicts(value, infer=True)
# Cycler
elif key == 'cycle':
colors = _get_cycle_colors(value)
kw_matplotlib['patch.facecolor'] = 'C0'
kw_matplotlib['axes.prop_cycle'] = cycler.cycler('color', colors)
# Turning bounding box on should turn border off and vice versa
elif key in ('abc.bbox', 'title.bbox', 'abc.border', 'title.border'):
if value:
name, this = key.split('.')
other = 'border' if this == 'bbox' else 'border'
kw_proplot[name + '.' + other] = False
# Fontsize
# NOTE: Re-application of e.g. size='small' uses the updated 'font.size'
elif key == 'font.size':
kw_proplot.update({
key: value for key, value in rc_proplot.items()
if key in FONT_KEYS and value in mfonts.font_scalings
})
kw_matplotlib.update({
key: value for key, value in rc_matplotlib.items()
if key in FONT_KEYS and value in mfonts.font_scalings
})
# Zero linewidth almost always means zero tick length
# TODO: Document this feature
elif key == 'linewidth' and value == 0:
ikw_proplot, ikw_matplotlib = self._get_synced_params('ticklen', 0)
kw_proplot.update(ikw_proplot)
kw_matplotlib.update(ikw_matplotlib)
# Tick length/major-minor tick length ratio
elif key in ('tick.len', 'tick.lenratio'):
if key == 'tick.len':
ticklen = value
ratio = rc_proplot['tick.lenratio']
else:
ticklen = rc_proplot['tick.len']
ratio = value
kw_matplotlib['xtick.minor.size'] = ticklen * ratio
kw_matplotlib['ytick.minor.size'] = ticklen * ratio
# Spine width/major-minor tick width ratio
elif key in ('linewidth', 'tick.ratio'):
if key == 'linewidth':
tickwidth = value
ratio = rc_proplot['tick.ratio']
else:
tickwidth = rc_proplot['linewidth']
ratio = value
kw_matplotlib['xtick.minor.width'] = tickwidth * ratio
kw_matplotlib['ytick.minor.width'] = tickwidth * ratio
# Gridline width
elif key in ('grid.linewidth', 'grid.ratio'):
if key == 'grid.linewidth':
gridwidth = value
ratio = rc_proplot['grid.ratio']
else:
gridwidth = rc_matplotlib['grid.linewidth']
ratio = value
kw_proplot['gridminor.linewidth'] = gridwidth * ratio
# Gridline toggling, complicated because of the clunky way this is
# implemented in matplotlib. There should be a gridminor setting!
elif key in ('grid', 'gridminor'):
b = value
ovalue = rc_matplotlib['axes.grid']
owhich = rc_matplotlib['axes.grid.which']
# Instruction is to turn off gridlines
if not value:
# Gridlines are already off, or they are on for the particular
# ones that we want to turn off. Instruct to turn both off.
if (
not ovalue
or key == 'grid' and owhich == 'major'
or key == 'gridminor' and owhich == 'minor'
):
which = 'both' # disable both sides
# Gridlines are currently on for major and minor ticks, so we
# instruct to turn on gridlines for the one we *don't* want off
elif owhich == 'both': # and ovalue is True, as already tested
# if gridminor=False, enable major, and vice versa
b = True
which = 'major' if key == 'gridminor' else 'minor'
# Gridlines are on for the ones that we *didn't* instruct to
# turn off, and off for the ones we do want to turn off. This
# just re-asserts the ones that are already on.
else:
b = True
which = owhich
# Instruction is to turn on gridlines
else:
# Gridlines are already both on, or they are off only for the
# ones that we want to turn on. Turn on gridlines for both.
if (
owhich == 'both'
or key == 'grid' and owhich == 'minor'
or key == 'gridminor' and owhich == 'major'
):
which = 'both'
# Gridlines are off for both, or off for the ones that we
# don't want to turn on. We can just turn on these ones.
else:
which = owhich
# Finally apply settings
kw_matplotlib['axes.grid'] = b
kw_matplotlib['axes.grid.which'] = which
# Update original setting and linked settings
for key in keys:
if key in rc_proplot:
kw_proplot[key] = value
elif key in rc_matplotlib:
kw_matplotlib[key] = value
else:
raise KeyError(f'Invalid rc key {key!r}.')
return kw_proplot, kw_matplotlib
@staticmethod
def _get_user_path():
"""
Return location of user proplotrc file.
"""
return os.path.join(os.path.expanduser('~'), '.proplotrc')
@staticmethod
def _get_local_paths():
"""
Return locations of local proplotrc files in this directory
and in parent directories.
"""
cdir = os.getcwd()
paths = []
# Loop until we reach root
while cdir:
# Look for hidden and unhidden proplotrc files
path = os.path.join(cdir, 'proplotrc')
if os.path.exists(path):
paths.append(path)
path = os.path.join(cdir, '.proplotrc')
if os.path.exists(path):
paths.append(path)
# Move on to next parent directory
ndir = os.path.dirname(cdir)
if ndir == cdir: # root
break
cdir = ndir
return paths[::-1] # sort from decreasing to increasing importantce
@staticmethod
def _sanitize_key(key):
"""
Ensure string and convert keys with omitted dots.
"""
if not isinstance(key, str):
raise KeyError(f'Invalid key {key!r}. Must be string.')
# Translate from nodots to 'full' version
if '.' not in key:
key = rcsetup._rc_nodots.get(key, key)
# Handle deprecations
if key in rcsetup._rc_removed:
alternative, version = rcsetup._rc_removed[key]
message = f'rc setting {key!r} was removed in version {version}.'
if alternative: # provide an alternative
message = f'{message} {alternative}'
warnings._warn_proplot(warnings)
key = None
if key in rcsetup._rc_renamed:
key_new, version = rcsetup._rc_renamed[key]
warnings._warn_proplot(
f'rc setting {key!r} was renamed to {key_new} in version {version}.'
)
key = key_new
return key.lower()
@staticmethod
def _sanitize_value(value):
"""
Convert numpy ndarray to list.
"""
if isinstance(value, np.ndarray):
if value.size <= 1:
value = value.item()
else:
value = value.tolist()
return value
@staticmethod
def _scale_font(size):
"""
Translate font size to numeric.
"""
# NOTE: Critical this remains KeyError so except clause
# in _get_synced_params works.
if not isinstance(size, str):
return size
try:
scale = mfonts.font_scalings[size]
except KeyError:
raise KeyError(
f'Invalid font scaling {size!r}. Options are: '
+ ', '.join(
f'{key!r} ({value})'
for key, value in mfonts.font_scalings.items()
) + '.'
)
else:
return rc_matplotlib['font.size'] * scale
[docs] def category(self, cat, *, trimcat=True, context=False):
"""
Return a dictionary of settings beginning with the substring
``cat + '.'``.
Parameters
----------
cat : str, optional
The `rc` setting category.
trimcat : bool, optional
Whether to trim ``cat`` from the key names in the output
dictionary. Default is ``True``.
context : bool, optional
If ``True``, then each category setting that is not found in the
context mode dictionaries is omitted from the output dictionary.
See `~RcConfigurator.context`.
See also
--------
RcConfigurator.get
RcConfigurator.fill
"""
if cat not in rcsetup._rc_categories:
raise ValueError(
f'Invalid rc category {cat!r}. Valid categories are '
+ ', '.join(map(repr, rcsetup._rc_categories)) + '.'
)
kw = {}
mode = 0 if not context else None
for rcdict in (rc_proplot, rc_matplotlib):
for key in rcdict:
if not re.match(fr'\A{cat}\.[^.]+\Z', key):
continue
value = self._get_item(key, mode)
if value is None:
continue
if trimcat:
key = re.sub(fr'\A{cat}\.', '', key)
kw[key] = value
return kw
[docs] def context(self, *args, mode=0, file=None, **kwargs):
"""
Temporarily modify the rc settings in a "with as" block.
Parameters
----------
*args
Dictionaries of `rc` names and values.
file : str, optional
Filename from which settings should be loaded.
**kwargs
`rc` names and values passed as keyword arguments. If the
name has dots, simply omit them.
Other parameters
----------------
mode : {0, 1, 2}, optional
The context mode. Dictates the behavior of `~RcConfigurator.get`,
`~RcConfigurator.fill`, and `~RcConfigurator.category` within a
"with as" block when called with ``context=True``.
The options are as follows:
0. Matplotlib's `builtin settings <rc_matplotlib>`_ and ProPlot's
:ref:`added settings <rc_proplot>` are all returned,
whether or not `~RcConfigurator.context` has changed them.
1. *Unchanged* `matplotlib settings <rc_matplotlib>`_ return ``None``.
All of ProPlot's :ref:`added settings <rc_proplot>` are returned
whether or not `~RcConfigurator.context` has changed them.
This is used in the `~proplot.axes.Axes.__init__` call to
`~proplot.axes.Axes.format`. When a lookup returns ``None``,
`~proplot.axes.Axes.format` does not apply it.
2. All unchanged settings return ``None``. This is used during
user calls to `~proplot.axes.Axes.format`.
Note
----
This is used by ProPlot internally but may also be useful for power users.
It was invented to prevent successive calls to `~proplot.axes.Axes.format`
from constantly looking up and re-applying unchanged settings. These
gratuitous lookups increased runtime significantly, and resulted in successive
calls to `~proplot.axes.Axes.format` overwriting the previous calls.
Example
-------
The below applies settings to axes in a specific figure using
`~RcConfigurator.context`.
>>> import proplot as pplt
>>> with pplt.rc.context(linewidth=2, ticklen=5):
>>> fig, ax = pplt.subplots()
>>> ax.plot(data)
The below applies settings to a specific axes using `~proplot.axes.Axes.format`,
which uses `~RcConfigurator.context` internally.
>>> import proplot as pplt
>>> fig, ax = pplt.subplots()
>>> ax.format(linewidth=2, ticklen=5)
"""
# Add input dictionaries
for arg in args:
if not isinstance(arg, dict):
raise ValueError('Non-dictionary argument {arg!r}.')
kwargs.update(arg)
# Add settings from file
# TODO: Incoporate with matplotlib 'stylesheets'
if file is not None:
kw_proplot, kw_matplotlib = self._load_file(file)
kwargs.update(kw_proplot)
kwargs.update(kw_matplotlib)
# Activate context object
if mode not in range(3):
raise ValueError(f'Invalid mode {mode!r}.')
context = _RcContext(mode=mode, kwargs=kwargs, rc_new={}, rc_old={})
self._context.append(context)
return self
[docs] def get(self, key, *, context=False):
"""
Return a single setting.
Parameters
----------
key : str
The setting name.
context : bool, optional
If ``True``, then ``None`` is returned if the setting is not found
in the context mode dictionaries. See `~RcConfigurator.context`.
See also
--------
RcConfigurator.category
RcConfigurator.fill
"""
mode = 0 if not context else None
return self._get_item(key, mode)
[docs] def fill(self, props, *, context=False):
"""
Return a dictionary filled with settings whose names match the
string values in the input dictionary.
Parameters
----------
props : dict-like
Dictionary whose values are `rc` setting names.
context : bool, optional
If ``True``, then each setting that is not found in the
context mode dictionaries is omitted from the output dictionary.
See `~RcConfigurator.context`.
See also
--------
RcConfigurator.category
RcConfigurator.get
"""
kw = {}
mode = 0 if not context else None
for key, value in props.items():
item = self._get_item(value, mode)
if item is not None:
kw[key] = item
return kw
[docs] def update(self, *args, **kwargs):
"""
Update several settings at once with a dictionary and/or
keyword arguments.
Parameters
----------
*args : str, dict, or (str, dict), optional
A dictionary containing `rc` keys and values. You can also
pass a "category" name as the first argument, in which case all
settings are prepended with ``'category.'``. For example,
``rc.update('axes', labelsize=20, titlesize=20)`` changes the
:rcraw:`axes.labelsize` and :rcraw:`axes.titlesize` properties.
**kwargs, optional
`rc` keys and values passed as keyword arguments. If the
name has dots, simply omit them.
See also
--------
RcConfigurator.fill
"""
# Parse args
kw = {}
prefix = ''
if len(args) > 2:
raise ValueError(
f'rc.update() accepts 1-2 arguments, got {len(args)}. Usage '
'is rc.update(kw), rc.update(category, kw), '
'rc.update(**kwargs), or rc.update(category, **kwargs).'
)
elif len(args) == 2:
prefix = args[0]
kw = args[1]
elif len(args) == 1:
if isinstance(args[0], str):
prefix = args[0]
else:
kw = args[0]
# Apply settings
if prefix:
prefix = prefix + '.'
kw.update(kwargs)
for key, value in kw.items():
self.__setitem__(prefix + key, value)
[docs] @docstring.add_snippets
def reset(self, local=True, user=True, default=True):
"""
Reset the configurator to its initial state.
Parameters
----------
%(rc.params)s
"""
# Always remove context objects
self._context.clear()
# Update from default settings
# NOTE: see _remove_blacklisted_style_params bugfix
if default:
rc_matplotlib.update(_get_style_dicts('original', filter=False))
rc_matplotlib.update(rcsetup._rc_matplotlib_default)
rc_proplot.update(rcsetup._rc_proplot_default)
for key, value in rc_proplot.items():
kw_proplot, kw_matplotlib = self._get_synced_params(key, value)
rc_matplotlib.update(kw_matplotlib)
rc_proplot.update(kw_proplot)
# Update from user home
user_path = None
if user:
user_path = self._get_user_path()
if os.path.isfile(user_path):
self.load_file(user_path)
# Update from local paths
if local:
local_paths = self._get_local_paths()
for path in local_paths:
if path == user_path: # local files always have precedence
continue
self.load_file(path)
def _load_file(self, path):
"""
Return dictionaries of proplot and matplotlib settings loaded from the file.
"""
added = set()
path = os.path.expanduser(path)
kw_proplot = {}
kw_matplotlib = {}
with open(path, 'r') as fh:
for cnt, line in enumerate(fh):
# Parse line and ignore comments
stripped = line.split('#', 1)[0].strip()
if not stripped:
continue
pair = stripped.split(':', 1)
if len(pair) != 2:
warnings._warn_proplot(
f'Illegal line #{cnt + 1} in file {path!r}:\n{line!r}"'
)
continue
# Get key value pair
key, val = pair
key = key.strip()
val = val.strip()
if key in added:
warnings._warn_proplot(
f'Duplicate key {key!r} on line #{cnt + 1} in file {path!r}.'
)
added.add(key)
# *Very primitive* type conversion system for proplot settings.
# Matplotlib does this automatically.
if REGEX_STRING.match(val): # also do this for matplotlib settings
val = val[1:-1] # remove quotes from string
if key in rc_proplot:
if not val or val == 'None':
val = None # older proplot versions supported this
elif val == 'True':
val = True
elif val == 'False':
val = False
else:
try:
val = float(val) if '.' in val else int(val)
except ValueError:
pass
# Add to dictionaries
try:
ikw_proplot, ikw_matplotlib = self._get_synced_params(key, val)
kw_proplot.update(ikw_proplot)
kw_matplotlib.update(ikw_matplotlib)
except KeyError:
warnings._warn_proplot(
f'Invalid key {key!r} on line #{cnt} in file {path!r}.'
)
return kw_proplot, kw_matplotlib
[docs] def load_file(self, path):
"""
Load settings from the specified file.
Parameters
----------
path : str
The file path.
See also
--------
RcConfigurator.save
"""
kw_proplot, kw_matplotlib = self._load_file(path)
rc_proplot.update(kw_proplot)
rc_matplotlib.update(kw_matplotlib)
@staticmethod
def _save_rst(path):
"""
Used internally to create table for online docs.
"""
string = rcsetup._gen_rst_table()
with open(path, 'w') as fh:
fh.write(string)
@staticmethod
def _save_proplotrc(path, comment=False):
"""
Used internally to create initial proplotrc file and file for online docs.
"""
self = object() # self is unused when 'user' is False
RcConfigurator.save(self, path, user=False, backup=False, comment=comment)
[docs] def save(self, path=None, user=True, comment=None, backup=True, description=False):
"""
Save the current settings to a ``.proplotrc`` file. This writes
the default values commented out plus the values that *differ*
from the defaults at the top of the file.
Parameters
----------
path : str, optional
The path name. The default file name is ``.proplotrc`` and the default
directory is the home directory. Use ``path=''`` to save to the current
directory.
user : bool, optional
If ``True`` (the default), the settings you changed since importing
proplot are shown uncommented at the very top of the file.
backup : bool, optional
If the file already exists and this is set to ``True``, it is moved
to a backup file with the suffix ``.bak``.
comment : bool, optional
Whether to comment out the default settings. Default is the
value of `user`.
description : bool, optional
Whether to include descriptions of each setting as comments.
Default is ``False``.
See also
--------
RcConfigurator.load_file
"""
if path is None:
path = '~'
path = os.path.abspath(os.path.expanduser(path))
if os.path.isdir(path):
path = os.path.join(path, '.proplotrc')
if os.path.isfile(path) and backup:
os.rename(path, path + '.bak')
warnings._warn_proplot(
f'Existing proplotrc file {path!r} was moved to {path + ".bak"!r}.'
)
# Generate user-specific table, ignoring non-style related
# settings that may be changed from defaults like 'backend'
rc_user = ()
if user:
# Changed settings
rcdict = {
key: value for key, value in self
if value != rcsetup._get_default_param(key)
}
# Special handling for certain settings
# TODO: For now not sure how to detect if prop cycle changed since
# we cannot load it from _cmap_database in rcsetup.
rcdict.pop('interactive', None) # changed by backend
rcdict.pop('axes.prop_cycle', None)
# Filter and get table
rcdict = _get_filtered_dict(rcdict, warn=False)
rc_user_table = rcsetup._gen_yaml_table(rcdict, comment=False)
rc_user = ('# Settings changed by user', rc_user_table, '') # + blank line
# Generate tables and write
comment = _not_none(comment, user)
rc_proplot_table = rcsetup._gen_yaml_table(
rcsetup._rc_proplot, comment=comment, description=description,
)
rc_matplotlib_table = rcsetup._gen_yaml_table(
rcsetup._rc_matplotlib_default, comment=comment
)
with open(path, 'w') as fh:
fh.write('\n'.join((
'#--------------------------------------------------------------------',
'# Use this file to change the default proplot and matplotlib settings',
'# The syntax is identical to matplotlibrc syntax. For details see:',
'# https://proplot.readthedocs.io/en/latest/configuration.html',
'# https://matplotlib.org/stable/tutorials/introductory/customizing.html', # noqa: E501
'#--------------------------------------------------------------------',
*rc_user, # includes blank line
'# ProPlot settings',
rc_proplot_table,
'\n# Matplotlib settings',
rc_matplotlib_table,
)))
[docs] def items(self):
"""
Return an iterator that loops over all setting names and values.
Same as `dict.items`.
See also
--------
RcConfigurator.keys
RcConfigurator.values
RcConfigurator.items
"""
for key in self:
yield key, self[key]
[docs] def keys(self):
"""
Return an iterator that loops over all setting names.
Same as `dict.keys`.
See also
--------
RcConfigurator.values
RcConfigurator.items
"""
for key in self:
yield key
[docs] def values(self):
"""
Return an iterator that loops over all setting values.
Same as `dict.values`.
See also
--------
RcConfigurator.keys
RcConfigurator.items
"""
for key in self:
yield self[key]
[docs]def config_inline_backend(fmt=None):
"""
Set up the `ipython inline backend \
<https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-matplotlib>`__
format and ensure that inline figures always look the same as saved
figures. This runs the following ipython magic commands:
.. code-block:: ipython
%config InlineBackend.figure_formats = rc['inlinefmt']
%config InlineBackend.rc = {} # never override rc settings
%config InlineBackend.close_figures = True \
# cells start with no active figures
%config InlineBackend.print_figure_kwargs = {'bbox_inches': None} \
# never override rc settings
When the inline backend is inactive or unavailable, this has no effect.
This function is called when you modify the :rcraw:`inlinefmt` property.
Parameters
----------
fmt : str or list of str, optional
The inline backend file format(s). Default is :rc:`inlinefmt`.
Valid formats include ``'jpg'``, ``'png'``, ``'svg'``, ``'pdf'``,
and ``'retina'``.
See also
--------
RcConfigurator
"""
# Note if inline backend is unavailable this will fail silently
ipython = get_ipython()
if ipython is None:
return
fmt = _not_none(fmt, rc_proplot['inlinefmt'])
if isinstance(fmt, str):
fmt = [fmt]
elif np.iterable(fmt):
fmt = list(fmt)
else:
raise ValueError(
f'Invalid inline backend format {fmt!r}. Must be string or list thereof.'
)
ipython.magic('config InlineBackend.figure_formats = ' + repr(fmt))
ipython.magic('config InlineBackend.rc = {}')
ipython.magic('config InlineBackend.close_figures = True')
ipython.magic("config InlineBackend.print_figure_kwargs = {'bbox_inches': None}")
def _get_cycle_colors(cycle):
"""
Update the color cycle.
"""
try:
colors = pcolors._cmap_database[cycle].colors
except (KeyError, AttributeError):
cycles = sorted(
name for name, cmap in pcolors._cmap_database.items()
if isinstance(cmap, pcolors.ListedColormap)
)
raise ValueError(
f'Invalid cycle name {cycle!r}. Options are: '
+ ', '.join(map(repr, cycles)) + '.'
)
return colors
def _get_default_dict():
"""
Get the default rc parameters dictionary with deprecated parameters filtered.
"""
# NOTE: Use RcParams update to filter and translate deprecated settings
# before actually applying them to rcParams down pipeline. This way we can
# suppress warnings for deprecated default params but still issue warnings
# when user-supplied stylesheets have deprecated params.
# WARNING: Some deprecated rc params remain in dictionary as None so we
# filter them out. Beware if hidden attribute changes.
rcdict = _get_filtered_dict(mpl.rcParamsDefault, warn=False)
with _suppress_matplotlib_deprecation_warning():
rcdict = dict(RcParams(rcdict))
for attr in ('_deprecated_remain_as_none', '_deprecated_set'):
if hasattr(mpl, attr): # _deprecated_set is in matplotlib before v3
for deprecated in getattr(mpl, attr):
rcdict.pop(deprecated, None)
return rcdict
def _get_filtered_dict(rcdict, warn=True):
"""
Filter out blacklisted style parameters.
"""
# NOTE: This implements bugfix: https://github.com/matplotlib/matplotlib/pull/17252
# This fix is *critical* for proplot because we always run style.use()
# when the configurator is made. Without fix backend is reset every time
# you import proplot in jupyter notebooks. So apply retroactively.
rcdict_filtered = {}
for key in rcdict:
if key in mstyle.STYLE_BLACKLIST:
if warn:
warnings._warn_proplot(
f'Dictionary includes a parameter, {key!r}, that is not related '
'to style. Ignoring.'
)
else:
rcdict_filtered[key] = rcdict[key]
return rcdict_filtered
def _get_style_dicts(style, infer=False, filter=True):
"""
Return a dictionary of settings belonging to the requested style(s). If `infer`
is ``True``, two dictionaries are returned, where the second contains custom
ProPlot settings "inferred" from the matplotlib settings. If `filter` is ``True``,
invalid style parameters like `backend` are filtered out.
"""
# NOTE: This is adapted from matplotlib source for the following changes:
# 1. Add 'original' option. Like rcParamsOrig except we also *reload*
# from user matplotlibrc file.
# 2. When the style is changed we reset to the *default* state ignoring
# matplotlibrc. Matplotlib applies styles on top of current state
# (including matplotlibrc changes and runtime rcParams changes) but
# IMO the word 'style' implies a *rigid* static format.
# 3. Add a separate function that returns lists of style dictionaries so
# that we can modify the active style in a context block. ProPlot context
# is more conservative than matplotlib's rc_context because it gets
# called a lot (e.g. every time you make an axes and every format() call).
# Instead of copying the entire rcParams dict we just track the keys
# that were changed.
style_aliases = {
'538': 'fivethirtyeight',
'mpl20': 'default',
'mpl15': 'classic',
'original': mpl.matplotlib_fname(),
}
# Always apply the default style *first* so styles are rigid
kw_matplotlib = _get_default_dict()
if style == 'default' or style is mpl.rcParamsDefault:
if infer:
kw_proplot = _infer_added_params(kw_matplotlib)
return kw_matplotlib, kw_proplot
else:
return kw_matplotlib
# Apply limited deviations from the matplotlib style that we want to propagate to
# other styles. Want users selecting stylesheets to have few surprises, so
# currently just enforce the new aesthetically pleasing fonts.
kw_matplotlib['font.family'] = 'sans-serif'
for fmly in ('serif', 'sans-serif', 'monospace', 'cursive', 'fantasy'):
kw_matplotlib['font.' + fmly] = rcsetup._rc_matplotlib_default['font.' + fmly]
# Apply user input style(s) one by one
if isinstance(style, str) or isinstance(style, dict):
styles = [style]
else:
styles = style
for style in styles:
if isinstance(style, dict):
kw = style
elif isinstance(style, str):
style = style_aliases.get(style, style)
if style in mstyle.library:
kw = mstyle.library[style]
else:
try:
kw = mpl.rc_params_from_file(style, use_default_template=False)
except IOError:
raise IOError(
f'Style {style!r} not found in the style library and input is '
'not a valid URL or path. Available styles are: '
+ ', '.join(map(repr, mstyle.available)) + '.'
)
else:
raise ValueError(f'Invalid style {style!r}. Must be string or dictionary.')
if filter:
kw = _get_filtered_dict(kw, warn=True)
kw_matplotlib.update(kw)
# Infer proplot params from stylesheet params
if infer:
kw_proplot = _infer_added_params(kw_matplotlib)
return kw_matplotlib, kw_proplot
else:
return kw_matplotlib
def _infer_added_params(kw_params):
"""
Infer values for proplot's "added" parameters from stylesheets.
"""
kw_proplot = {}
mpl_to_proplot = {
'font.size': ('tick.labelsize',),
'axes.titlesize': (
'abc.size', 'suptitle.size', 'title.size',
'leftlabel.size', 'rightlabel.size',
'toplabel.size', 'bottomlabel.size',
),
'text.color': (
'abc.color', 'suptitle.color', 'tick.labelcolor', 'title.color',
'leftlabel.color', 'rightlabel.color',
'toplabel.color', 'bottomlabel.color',
),
}
for key, params in mpl_to_proplot.items():
if key in kw_params:
value = kw_params[key]
for param in params:
kw_proplot[param] = value
return kw_proplot
[docs]def use_style(style):
"""
Apply the `matplotlib style(s) \
<https://matplotlib.org/tutorials/introductory/customizing.html>`__
with `matplotlib.style.use`. This function is
called when you modify the :rcraw:`style` property.
Parameters
----------
style : str, dict, or list thereof
The matplotlib style name(s) or stylesheet filename(s), or dictionary(s)
of settings. Use ``'default'`` to apply matplotlib default settings and
``'original'`` to include settings from your user ``matplotlibrc`` file.
See also
--------
RcConfigurator
matplotlib.style.use
"""
# NOTE: This function is not really necessary but makes proplot's
# stylesheet-supporting features obvious. Plus changing the style does
# so much *more* than changing rc params or quick settings, so it is
# nice to have dedicated function instead of just another rc_param name.
kw_matplotlib, kw_proplot = _get_style_dicts(style, infer=True)
rc_matplotlib.update(kw_matplotlib)
rc_proplot.update(kw_proplot)
[docs]@docstring.add_snippets
def register_cmaps(user=True, default=False):
"""
Register colormaps packaged with ProPlot or saved to the
``~/.proplot/cmaps`` folder. This is called on import.
Colormaps are registered according to their filenames -- for example,
``name.xyz`` will be registered as ``'name'``.
%(register.ext_table)s
To visualize the registered colormaps, use `~proplot.demos.show_cmaps`.
Parameters
----------
%(register_cmaps.params)s
See also
--------
register_cycles
register_colors
register_fonts
proplot.demos.show_cmaps
"""
for i, dirname, filename in _iter_data_paths('cmaps', user=user, default=default):
path = os.path.join(dirname, filename)
cmap = pcolors.LinearSegmentedColormap.from_file(path, warn_on_failure=True)
if not cmap:
continue
if i == 0 and cmap.name.lower() in (
'phase', 'graycycle', 'romao', 'broco', 'corko', 'viko',
):
cmap.set_cyclic(True)
pcolors._cmap_database[cmap.name] = cmap
[docs]@docstring.add_snippets
def register_cycles(user=True, default=False):
"""
Register color cycles packaged with ProPlot or saved to the
``~/.proplot/cycles`` folder. This is called on import. Color cycles
are registered according to their filenames -- for example, ``name.hex``
will be registered as ``'name'``.
%(register.ext_table)s
To visualize the registered color cycles, use `~proplot.demos.show_cycles`.
Parameters
----------
%(register_cycles.params)s
See also
--------
register_cmaps
register_colors
register_fonts
proplot.demos.show_cycles
"""
for _, dirname, filename in _iter_data_paths('cycles', user=user, default=default):
path = os.path.join(dirname, filename)
cmap = pcolors.ListedColormap.from_file(path, warn_on_failure=True)
if not cmap:
continue
pcolors._cmap_database[cmap.name] = cmap
[docs]@docstring.add_snippets
def register_colors(user=True, default=False, space='hcl', margin=0.10):
"""
Register the `open-color <https://yeun.github.io/open-color/>`_ colors,
XKCD `color survey <https://xkcd.com/color/rgb/>`_ colors, and colors
saved to the ``~/.proplot/colors`` folder. This is called on import.
The color survey colors are filtered to a subset that is "perceptually
distinct" in the HCL colorspace. The user color names are loaded from
``.txt`` files saved in ``~/.proplot/colors``. Each file should contain
one line per color in the format ``name : hex``. Whitespace is ignored.
To visualize the registered colors, use `~proplot.demos.show_colors`.
Parameters
----------
%(register_colors.params)s
space : {'hcl', 'hsl', 'hpl'}, optional
The colorspace used to detect "perceptually distinct" colors.
margin : float, optional
The margin by which a color's normalized hue, saturation, and
luminance channel values must differ from the normalized channel
values of the other colors to be deemed "perceptually distinct."
See also
--------
register_cmaps
register_cycles
register_fonts
proplot.demos.show_colors
"""
# Reset native colors dictionary
mcolors.colorConverter.colors.clear() # clean out!
mcolors.colorConverter.cache.clear() # clean out!
# Add in base colors and CSS4 colors so user has no surprises
for name, dict_ in (('base', COLORS_BASE), ('css', mcolors.CSS4_COLORS)):
mcolors.colorConverter.colors.update(dict_)
# Load colors from file and get their HCL values
# NOTE: Colors that come *later* overwrite colors that come earlier.
hex = re.compile(rf'\A{pcolors.REGEX_HEX}\Z') # match each string
for i, dirname, filename in _iter_data_paths('colors', user=user, default=default):
path = os.path.join(dirname, filename)
cat, ext = os.path.splitext(filename)
if ext != '.txt':
raise ValueError(
f'Unknown color data file extension ({path!r}). '
'All files in this folder should have extension .txt.'
)
# Read data
loaded = {}
with open(path, 'r') as fh:
for cnt, line in enumerate(fh):
# Load colors from file
stripped = line.strip()
if not stripped or stripped[0] == '#':
continue
pair = tuple(
item.strip().lower() for item in line.split(':')
)
if len(pair) != 2 or not hex.match(pair[1]):
warnings._warn_proplot(
f'Illegal line #{cnt + 1} in file {path!r}:\n'
f'{line!r}\n'
f'Lines must be formatted as "name: hexcolor".'
)
continue
# Never overwrite "base" colors with xkcd colors.
# Only overwrite with user colors.
name, color = pair
if i == 0 and name in COLORS_BASE:
continue
loaded[name] = color
# Add every user color and every opencolor color and ensure XKCD
# colors are "perceptually distinct".
if i == 1:
mcolors.colorConverter.colors.update(loaded)
elif cat == 'opencolor':
mcolors.colorConverter.colors.update(loaded)
COLORS_OPEN.update(loaded)
elif cat == 'xkcd':
# Always add these colors, but make sure not to add other
# colors too close to them.
hcls = []
filtered = []
for name in COLORS_ADD:
color = loaded.pop(name, None)
if color is None:
continue
if 'grey' in name:
name = name.replace('grey', 'gray')
hcls.append(to_xyz(color, space=space))
filtered.append((name, color))
mcolors.colorConverter.colors[name] = color
COLORS_XKCD[name] = color
# Get locations of "perceptually distinct" colors
# WARNING: Unique axis argument requires numpy version >=1.13
for name, color in loaded.items():
for string, replace in COLORS_TRANSLATE:
if string in name:
name = name.replace(string, replace)
if any(string in name for string in COLORS_REMOVE):
continue # remove "unpofessional" names
hcls.append(to_xyz(color, space=space))
filtered.append((name, color)) # category name pair
hcls = np.asarray(hcls)
if not hcls.size:
continue
hcls = hcls / np.array([360, 100, 100])
hcls = np.round(hcls / margin).astype(np.int64)
_, idxs = np.unique(hcls, return_index=True, axis=0)
# Register "distinct" colors
for idx in idxs:
name, color = filtered[idx]
mcolors.colorConverter.colors[name] = color
COLORS_XKCD[name] = color
else:
raise ValueError(f'Unknown proplot color database {path!r}.')
def _patch_validators():
"""
Fix the fontsize validators to allow for new font scalings.
"""
# First define valdiators
# NOTE: In the future will subclass RcParams directly and control the
# validators ourselves.
def _validate_fontsize(s):
fontsizes = list(mfonts.font_scalings)
if isinstance(s, str):
s = s.lower()
if s in fontsizes:
return s
try:
return float(s)
except ValueError:
raise ValueError(
f'{s!r} is not a valid font size. Valid sizes are: '
+ ', '.join(map(repr, fontsizes))
)
def _validate_fontsize_None(s):
if s is None or s == 'None':
return None
else:
return _validate_fontsize(s)
_validate_fontsizelist = None
if hasattr(msetup, '_listify_validator'):
_validate_fontsizelist = msetup._listify_validator(_validate_fontsize)
# Apply new functions
validate = RcParams.validate
for key in list(validate): # modify in-place
validator = validate[key]
if validator is msetup.validate_fontsize:
validate[key] = _validate_fontsize
elif validator is getattr(msetup, 'validate_fontsize_None', None):
validate[key] = _validate_fontsize_None
elif validator is getattr(msetup, 'validate_fontsizelist', None):
if _validate_fontsizelist is not None:
validate[key] = _validate_fontsizelist
[docs]def register_fonts():
"""
Add fonts packaged with ProPlot or saved to the ``~/.proplot/fonts``
folder, if they are not already added. Detects ``.ttf`` and ``.otf`` files
-- see `this link \
<https://gree2.github.io/python/2015/04/27/python-change-matplotlib-font-on-mac>`__
for a guide on converting various other font file types to ``.ttf`` and
``.otf`` for use with matplotlib.
To visualize the registered fonts, use `~proplot.demos.show_fonts`.
See also
--------
register_cmaps
register_cycles
register_colors
proplot.demos.show_fonts
"""
# Find proplot fonts
# WARNING: If you include a font file with an unrecognized style,
# matplotlib may use that font instead of the 'normal' one! Valid styles:
# 'ultralight', 'light', 'normal', 'regular', 'book', 'medium', 'roman',
# 'semibold', 'demibold', 'demi', 'bold', 'heavy', 'extra bold', 'black'
# https://matplotlib.org/api/font_manager_api.html
# For macOS the only fonts with 'Thin' in one of the .ttf file names
# are Helvetica Neue and .SF NS Display Condensed. Never try to use these!
paths_proplot = _get_data_paths('fonts', reverse=True)
fnames_proplot = set(mfonts.findSystemFonts(paths_proplot))
# Detect user-input ttc fonts and issue warning
fnames_proplot_ttc = {
file for file in fnames_proplot if os.path.splitext(file)[1] == '.ttc'
}
if fnames_proplot_ttc:
warnings._warn_proplot(
'Ignoring the following .ttc fonts because they cannot be '
'saved into PDF or EPS files (see matplotlib issue #3135): '
+ ', '.join(map(repr, sorted(fnames_proplot_ttc)))
+ '. Please consider expanding them into separate .ttf files.'
)
# Rebuild font cache only if necessary! Can be >50% of total import time!
fnames_all = {font.fname for font in mfonts.fontManager.ttflist}
fnames_proplot -= fnames_proplot_ttc
if not fnames_all >= fnames_proplot:
warnings._warn_proplot('Rebuilding font cache.')
if hasattr(mfonts.fontManager, 'addfont'):
# New API lets us add font files manually and deprecates TTFPATH. However,
# to cache fonts added this way, we must call json_dump explicitly.
# NOTE: Previously, cache filename was specified as _fmcache variable, but
# recently became inaccessible. Must reproduce mpl code instead! Annoying.
# NOTE: Older mpl versions used fontList.json as the cache, but these
# versions also did not have 'addfont', so makes no difference.
for fname in fnames_proplot:
mfonts.fontManager.addfont(fname)
cache = os.path.join(
mpl.get_cachedir(),
f'fontlist-v{mfonts.FontManager.__version__}.json'
)
mfonts.json_dump(mfonts.fontManager, cache)
else:
# Old API requires us to modify TTFPATH
# NOTE: Previously we tried to modify TTFPATH before importing
# font manager with hope that it would load proplot fonts on
# initialization. But 99% of the time font manager just imports
# the FontManager from cache, so this doesn't work.
paths = ':'.join(paths_proplot)
if 'TTFPATH' not in os.environ:
os.environ['TTFPATH'] = paths
elif paths not in os.environ['TTFPATH']:
os.environ['TTFPATH'] += ':' + paths
mfonts._rebuild()
# Remove ttc files and 'Thin' fonts *after* rebuild
# NOTE: 'Thin' filter is ugly kludge but without this matplotlib picks up on
# Roboto thin ttf files installed on the RTD server when compiling docs.
mfonts.fontManager.ttflist = [
font for font in mfonts.fontManager.ttflist
if os.path.splitext(font.fname)[1] != '.ttc'
or 'Thin' in os.path.basename(font.fname)
]
# Initialize .proplotrc file
_user_rc_file = os.path.join(os.path.expanduser('~'), '.proplotrc')
if not os.path.exists(_user_rc_file):
RcConfigurator._save_proplotrc(_user_rc_file, comment=True)
# Initialize customization folders
_rc_folder = os.path.join(os.path.expanduser('~'), '.proplot')
if not os.path.isdir(_rc_folder):
os.mkdir(_rc_folder)
for _rc_sub in ('cmaps', 'cycles', 'colors', 'fonts'):
_rc_sub = os.path.join(_rc_folder, _rc_sub)
if not os.path.isdir(_rc_sub):
os.mkdir(_rc_sub)
# Add custom font scalings to font_manager and monkey patch rcParams validator
# NOTE: This is because we prefer large sizes
if hasattr(mfonts, 'font_scalings'):
mfonts.font_scalings['med-small'] = 0.9
mfonts.font_scalings['med-large'] = 1.1
_patch_validators()
# Convert colormaps that *should* be LinearSegmented from Listed
for _name in ('viridis', 'plasma', 'inferno', 'magma', 'cividis', 'twilight'):
_cmap = pcolors._cmap_database.get(_name, None)
if _cmap and isinstance(_cmap, pcolors.ListedColormap):
del pcolors._cmap_database[_name]
pcolors._cmap_database[_name] = pcolors.LinearSegmentedColormap.from_list(
_name, _cmap.colors, cyclic=(_name == 'twilight')
)
# Register objects and configure settings
with timers._benchmark('cmaps'):
register_cmaps(default=True)
with timers._benchmark('cycles'):
register_cycles(default=True)
with timers._benchmark('colors'):
register_colors(default=True)
with timers._benchmark('fonts'):
register_fonts()
with timers._benchmark('rc'):
_ = RcConfigurator()
#: Instance of `RcConfigurator`. This is used to change global settings.
#: See the :ref:`configuration guide <ug_config>` for details.
rc = _
# Modify N of existing colormaps because ProPlot settings may have changed
# image.lut. We have to register colormaps and cycles first so that the 'cycle'
# property accepts named cycles registered by ProPlot. No performance hit here.
lut = rc['image.lut']
for cmap in pcolors._cmap_database.values():
if isinstance(cmap, mcolors.LinearSegmentedColormap):
cmap.N = lut
# Deprecated
inline_backend_fmt = warnings._rename_objs(
'0.6', inline_backend_fmt=config_inline_backend
)