Source code for proplot.config

#!/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 in
# 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
# 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 was affecting behavior.
import re
import os
import numpy as np
import matplotlib as mpl
import matplotlib.font_manager as mfonts
import matplotlib.colors as mcolors
import as mstyle
import matplotlib.cbook as cbook
import matplotlib.rcsetup as msetup
import cycler
from collections import namedtuple
from . import colors as pcolors
from .utils import units, to_xyz
from .internals import ic  # noqa: F401
from .internals import rcsetup, docstring, timers, warnings, _not_none
    from IPython import get_ipython
except ImportError:
    def get_ipython():

__all__ = [
    'rc', 'RcConfigurator',
    'register_cmaps', 'register_cycles', 'register_colors', 'register_fonts',
    'config_inline_backend', 'use_style',
    'inline_backend_fmt', 'rc_configurator',  # deprecated

# Disable mathtext "missing glyph" warnings
import matplotlib.mathtext  # noqa
import logging
logger = logging.getLogger('matplotlib.mathtext')
logger.setLevel(logging.ERROR)  # suppress warnings!

# Dictionaries used to track custom proplot settings
rc_proplot = rcsetup._rc_proplot_default.copy()
rc_matplotlib = mpl.rcParams  # PEP8 4 lyfe
RcParams = mpl.RcParams  # the special class
_RcContext = namedtuple('RcContext', ('mode', 'kwargs', 'rc_new', 'rc_old'))

# Misc constants
# TODO: Use explicit validators for specific settings like matplotlib.
REGEX_STRING = re.compile('\\A(\'.*\'|".*")\\Z')
REGEX_POINTS = re.compile(

    *(  # 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 ',
ALWAYS_REMOVE = (  # filter these out, let's try to be professional here...
    'shit', 'poop', 'poo', 'pee', 'piss', 'puke', 'vomit', 'snot',
    'booger', 'bile', 'diarrhea',
TRANSLATE_COLORS = (  # prevent registering similar-sounding names
    ('/', ' '),
    ("'s", ''),
    ('forrest', 'forest'),  # typo?
    ('reddish', 'red'),  # remove 'ish'
    ('purplish', 'purple'),
    ('bluish', 'blue'),
    ('ish ', ' '),
    ('grey', 'gray'),
    ('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'),

OPEN_COLORS = {}  # populated during register_colors
XKCD_COLORS = {}  # populated during register_colors
    **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),

_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
                yield i, dirname, filename

[docs]class RcConfigurator(object): """ Magical abstract 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. To modify these settings, use the `rc` object. 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_context_mode(self): """ Return lowest (most permissive) context mode. """ return min((context.mode for context in self._context), default=0) 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_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 # See:, props matching # the below strings use the units 'points'. # TODO: Incorporate into more sophisticated validation system if any(REGEX_POINTS.match(_) for _ in keys): try: self._scale_font(value) # *validate* but do not translate except KeyError: value = units(value, 'pt') # 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) # 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_local_paths(): """ Return locations of local proplotrc files in this directory and in parent directories. """ idir = os.getcwd() paths = [] while idir: # not empty string ipath = os.path.join(idir, '.proplotrc') if os.path.exists(ipath): paths.append(ipath) ndir = os.path.dirname(idir) if ndir == idir: # root break idir = ndir return paths[::-1] # sort from decreasing to increasing importantce @staticmethod def _get_user_path(): """ Return location of user proplotrc file. """ return os.path.join(os.path.expanduser('~'), '.proplotrc') @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 isinstance(size, str): 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: size = rc_matplotlib['font.size'] * scale return size
[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`. """ 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 plot >>> with plot.rc.context(linewidth=2, ticklen=5): >>> fig, ax = plot.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 plot >>> fig, ax = plot.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`. """ 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`. """ 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. """ # 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: val = None # older proplot versions supported this elif val in ('True', 'False', 'None'): val = eval(val) # rare case where eval is o.k. 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. """ 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, 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``. """ 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:', '#', '#', '#--------------------------------------------------------------------', *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`. """ 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`. """ for key in self: yield key
[docs] def values(self): """ Return an iterator that loops over all setting values. Same as `dict.values`. """ for key in self: yield self[key]
[docs]def config_inline_backend(fmt=None): """ Set up the `ipython inline backend \ <>`__ 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'``. """ # 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 cbook._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: # 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: return kw_matplotlib # Apply "pseudo" default properties. Pretend some proplot settings are part of # the matplotlib specification so they propagate to other styles. kw_matplotlib[''] = 'sans-serif' kw_matplotlib['font.sans-serif'] = rcsetup._rc_matplotlib_default['font.sans-serif'] # Apply user input style(s) one by one # NOTE: Always use proplot fonts if style does not explicitly set them. 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) \ <>`__ with ``. 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. """ # 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, ```` will be registered as ``'name'``. %(register.ext_table)s To visualize the registered colormaps, use `~proplot.demos.show_cmaps`. Parameters ---------- %(register_cmaps.params)s """ 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 in ( 'phase', 'graycycle', 'romao', 'broco', 'corko', 'viko', ): cmap.set_cyclic(True) pcolors._cmap_database[] = 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 """ 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
[docs]@docstring.add_snippets def register_colors(user=True, default=False, space='hcl', margin=0.10): """ Register the `open-color <>`_ colors, XKCD `color survey <>`_ 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." """ # 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', BASE_COLORS), ('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.HEX_PATTERN}\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 BASE_COLORS: 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) OPEN_COLORS.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 ALWAYS_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 XKCD_COLORS[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 TRANSLATE_COLORS: if string in name: name = name.replace(string, replace) if any(string in name for string in ALWAYS_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 XKCD_COLORS[name] = color else: raise ValueError(f'Unknown proplot color database {path!r}.')
[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 \ <>`__ 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`. """ # 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' # # 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 for fname in fnames_proplot: mfonts.fontManager.addfont(fname) mfonts.json_dump(mfonts.fontManager, mfonts._fmcache) 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) ]
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 # 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, rc_configurator = warnings._rename_objs( '0.6', inline_backend_fmt=config_inline_backend, RcConfigurator=config_inline_backend, )