#!/usr/bin/env python3
"""
Utilities for configuring matplotlib and ProPlot global settings.
See :ref:`Configuring proplot` for details.
"""
# NOTE: Make sure to add to docs/configuration.rst when updating or adding
# new settings! Much of this script was adapted from seaborn; see:
# https://github.com/mwaskom/seaborn/blob/master/seaborn/rcmod.py
import re
import os
import yaml
import numpy as np
import cycler
import matplotlib.colors as mcolors
import matplotlib.cm as mcm
from matplotlib import style, rcParams
try:
import IPython
from IPython import get_ipython
except ModuleNotFoundError:
def get_ipython():
return None
from .utils import _warn_proplot, _counter, _benchmark, units
# Disable mathtext "missing glyph" warnings
import matplotlib.mathtext # noqa
import logging
logger = logging.getLogger('matplotlib.mathtext')
logger.setLevel(logging.ERROR) # suppress warnings!
__all__ = [
'rc', 'rc_configurator', 'ipython_autosave',
'ipython_autoreload', 'ipython_matplotlib',
]
# Initialize
defaultParamsShort = {
'abc': False,
'align': False,
'alpha': 1,
'autoreload': 2,
'autosave': 30,
'borders': False,
'cmap': 'fire',
'coast': False,
'color': 'k',
'cycle': 'colorblind',
'facecolor': 'w',
'fontname': 'sans-serif',
'inlinefmt': 'retina',
'geogrid': True,
'grid': True,
'gridminor': False,
'gridratio': 0.5,
'innerborders': False,
'lakes': False,
'land': False,
'large': 10,
'linewidth': 0.6,
'lut': 256,
'margin': 0.0,
'matplotlib': 'auto',
'nbsetup': True,
'ocean': False,
'reso': 'lo',
'rgbcycle': False,
'rivers': False,
'share': 3,
'small': 9,
'span': True,
'tickdir': 'out',
'ticklen': 4.0,
'ticklenratio': 0.5,
'tickminor': True,
'tickpad': 2.0,
'tickratio': 0.8,
'tight': True,
}
defaultParamsLong = {
'abc.border': True,
'abc.color': 'k',
'abc.linewidth': 1.5,
'abc.loc': 'l', # left side above the axes
'abc.size': None, # = large
'abc.style': 'a',
'abc.weight': 'bold',
'axes.facealpha': None, # if empty, depends on 'savefig.transparent'
'axes.formatter.timerotation': 90,
'axes.formatter.zerotrim': True,
'axes.geogrid': True,
'axes.gridminor': True,
'borders.color': 'k',
'borders.linewidth': 0.6,
'bottomlabel.color': 'k',
'bottomlabel.size': None, # = large
'bottomlabel.weight': 'bold',
'coast.color': 'k',
'coast.linewidth': 0.6,
'colorbar.extend': '1.3em',
'colorbar.framealpha': 0.8,
'colorbar.frameon': True,
'colorbar.grid': False,
'colorbar.insetextend': '1em',
'colorbar.insetlength': '8em',
'colorbar.insetpad': '0.5em',
'colorbar.insetwidth': '1.2em',
'colorbar.length': 1,
'colorbar.loc': 'right',
'colorbar.width': '1.5em',
'geoaxes.edgecolor': None, # = color
'geoaxes.facealpha': None, # = alpha
'geoaxes.facecolor': None, # = facecolor
'geoaxes.linewidth': None, # = linewidth
'geogrid.alpha': 0.5,
'geogrid.color': 'k',
'geogrid.labels': False,
'geogrid.labelsize': None, # = small
'geogrid.latmax': 90,
'geogrid.latstep': 20,
'geogrid.linestyle': ':',
'geogrid.linewidth': 1.0,
'geogrid.lonstep': 30,
'gridminor.alpha': None, # = grid.alpha
'gridminor.color': None, # = grid.color
'gridminor.linestyle': None, # = grid.linewidth
'gridminor.linewidth': None, # = grid.linewidth x gridratio
'image.edgefix': True,
'image.levels': 11,
'innerborders.color': 'k',
'innerborders.linewidth': 0.6,
'lakes.color': 'w',
'land.color': 'k',
'leftlabel.color': 'k',
'leftlabel.size': None, # = large
'leftlabel.weight': 'bold',
'ocean.color': 'w',
'rightlabel.color': 'k',
'rightlabel.size': None, # = large
'rightlabel.weight': 'bold',
'rivers.color': 'k',
'rivers.linewidth': 0.6,
'subplots.axpad': '1em',
'subplots.axwidth': '18em',
'subplots.pad': '0.5em',
'subplots.panelpad': '0.5em',
'subplots.panelwidth': '4em',
'suptitle.color': 'k',
'suptitle.size': None, # = large
'suptitle.weight': 'bold',
'tick.labelcolor': None, # = color
'tick.labelsize': None, # = small
'tick.labelweight': 'normal',
'title.border': True,
'title.color': 'k',
'title.linewidth': 1.5,
'title.loc': 'c', # centered above the axes
'title.pad': 3.0, # copy
'title.size': None, # = large
'title.weight': 'normal',
'toplabel.color': 'k',
'toplabel.size': None, # = large
'toplabel.weight': 'bold',
}
defaultParams = {
'axes.grid': True,
'axes.labelpad': 3.0,
'axes.titlepad': 3.0,
'axes.titleweight': 'normal',
'axes.xmargin': 0.0,
'axes.ymargin': 0.0,
'figure.autolayout': False,
'figure.facecolor': '#f2f2f2',
'figure.max_open_warning': 0,
'figure.titleweight': 'bold',
'font.serif': (
'New Century Schoolbook',
'Century Schoolbook L',
'Utopia',
'ITC Bookman',
'Bookman',
'Nimbus Roman No9 L',
'Times New Roman',
'Times',
'Palatino',
'Charter',
'Computer Modern Roman',
'DejaVu Serif',
'Bitstream Vera Serif',
'serif',
),
'font.sans-serif': (
'Helvetica',
'Arial',
'Lucida Grande',
'Verdana',
'Geneva',
'Lucid',
'Avant Garde',
'TeX Gyre Heros',
'DejaVu Sans',
'Bitstream Vera Sans',
'Computer Modern Sans Serif',
'sans-serif',
),
'font.monospace': (
'Andale Mono',
'Nimbus Mono L',
'Courier New',
'Courier',
'Fixed',
'Terminal',
'Computer Modern Typewriter',
'DejaVu Sans Mono',
'Bitstream Vera Sans Mono',
'monospace',
),
'grid.alpha': 0.1,
'grid.color': 'k',
'grid.linestyle': '-',
'grid.linewidth': 0.6,
'hatch.color': 'k',
'hatch.linewidth': 0.6,
'legend.borderaxespad': 0,
'legend.borderpad': 0.5,
'legend.columnspacing': 1.0,
'legend.fancybox': False,
'legend.framealpha': 0.8,
'legend.frameon': True,
'legend.handlelength': 1.5,
'legend.handletextpad': 0.5,
'legend.labelspacing': 0.5,
'lines.linewidth': 1.3,
'lines.markersize': 3.0,
'mathtext.fontset': 'custom',
'mathtext.default': 'regular',
'savefig.bbox': 'standard',
'savefig.directory': '',
'savefig.dpi': 300,
'savefig.facecolor': 'white',
'savefig.format': 'pdf',
'savefig.pad_inches': 0.0,
'savefig.transparent': True,
'text.usetex': False,
'xtick.minor.visible': True,
'ytick.minor.visible': True,
}
rcParamsShort = {}
rcParamsLong = {}
# Initialize user file
_rc_file = os.path.join(os.path.expanduser('~'), '.proplotrc')
if not os.path.isfile(_rc_file):
def _tabulate(rcdict):
string = ''
maxlen = max(map(len, rcdict))
for key, value in rcdict.items():
value = '' if value is None else repr(value)
space = ' ' * (maxlen - len(key) + 1) * int(bool(value))
string += f'# {key}:{space}{value}\n'
return string.strip()
with open(_rc_file, 'x') as f:
f.write(f"""
#------------------------------------------------------
# Use this file to customize settings
# For descriptions of each key name see:
# https://proplot.readthedocs.io/en/latest/rctools.html
#------------------------------------------------------
# ProPlot short name settings
{_tabulate(defaultParamsShort)}
#
# ProPlot long name settings
{_tabulate(defaultParamsLong)}
#
# Matplotlib settings
{_tabulate(defaultParams)}
""".strip())
# "Global" settings and the lower-level settings they change
# NOTE: This whole section, declaring dictionaries and sets, takes 1ms
RC_CHILDREN = {
'cmap': (
'image.cmap',
),
'lut': (
'image.lut',
),
'alpha': ( # this is a custom setting
'axes.facealpha',
),
'facecolor': (
'axes.facecolor', 'geoaxes.facecolor'
),
'fontname': (
'font.family',
),
'color': ( # change the 'color' of an axes
'axes.edgecolor', 'geoaxes.edgecolor', 'axes.labelcolor',
'tick.labelcolor', 'hatch.color', 'xtick.color', 'ytick.color'
),
'small': ( # the 'small' fonts
'font.size', 'tick.labelsize', 'xtick.labelsize', 'ytick.labelsize',
'axes.labelsize', 'legend.fontsize', 'geogrid.labelsize'
),
'large': ( # the 'large' fonts
'abc.size', 'figure.titlesize',
'axes.titlesize', 'suptitle.size', 'title.size',
'leftlabel.size', 'toplabel.size',
'rightlabel.size', 'bottomlabel.size'
),
'linewidth': (
'axes.linewidth', 'geoaxes.linewidth', 'hatch.linewidth',
'xtick.major.width', 'ytick.major.width'
),
'margin': (
'axes.xmargin', 'axes.ymargin'
),
'grid': (
'axes.grid',
),
'gridminor': (
'axes.gridminor',
),
'geogrid': (
'axes.geogrid',
),
'ticklen': (
'xtick.major.size', 'ytick.major.size'
),
'tickdir': (
'xtick.direction', 'ytick.direction'
),
'labelpad': (
'axes.labelpad',
),
'titlepad': (
'axes.titlepad',
),
'tickpad': (
'xtick.major.pad', 'xtick.minor.pad',
'ytick.major.pad', 'ytick.minor.pad'
),
}
# Names of the new settings
RC_PARAMNAMES = {*rcParams.keys()}
RC_SHORTNAMES = {
'abc',
'align',
'alpha',
'autoreload',
'autosave',
'borders',
'cmap',
'coast',
'color',
'cycle',
'facecolor',
'fontname',
'geogrid',
'grid',
'gridminor',
'gridratio',
'inlinefmt',
'innerborders',
'lakes',
'land',
'large',
'linewidth',
'lut',
'margin',
'matplotlib',
'ocean',
'reso',
'rgbcycle',
'rivers',
'share',
'small',
'span',
'tickdir',
'ticklen',
'ticklenratio',
'tickpad',
'tickratio',
'tight',
}
RC_LONGNAMES = {
'abc.border',
'abc.color',
'abc.linewidth',
'abc.loc',
'abc.size',
'abc.style',
'abc.weight',
'axes.alpha',
'axes.formatter.timerotation',
'axes.formatter.zerotrim',
'axes.geogrid',
'axes.gridminor',
'borders.color',
'borders.linewidth',
'bottomlabel.color',
'bottomlabel.size',
'bottomlabel.weight',
'coast.color',
'coast.linewidth',
'colorbar.axespad',
'colorbar.extend',
'colorbar.framealpha',
'colorbar.frameon',
'colorbar.grid',
'colorbar.insetextend',
'colorbar.insetlength',
'colorbar.insetwidth',
'colorbar.length',
'colorbar.loc',
'colorbar.width',
'geoaxes.edgecolor',
'geoaxes.facecolor',
'geoaxes.linewidth',
'geogrid.alpha',
'geogrid.color',
'geogrid.labels',
'geogrid.labelsize',
'geogrid.latmax',
'geogrid.latstep',
'geogrid.linestyle',
'geogrid.linewidth',
'geogrid.lonstep',
'gridminor.alpha',
'gridminor.color',
'gridminor.linestyle',
'gridminor.linewidth',
'image.edgefix',
'image.levels',
'innerborders.color',
'innerborders.linewidth',
'lakes.color',
'land.color',
'leftlabel.color',
'leftlabel.size',
'leftlabel.weight',
'ocean.color',
'rightlabel.color',
'rightlabel.size',
'rightlabel.weight',
'rivers.color',
'rivers.linewidth',
'subplots.axpad',
'subplots.axwidth',
'subplots.pad',
'subplots.panelpad',
'subplots.panelwidth',
'suptitle.color',
'suptitle.size',
'suptitle.weight',
'tick.labelcolor',
'tick.labelsize',
'tick.labelweight',
'title.border',
'title.color',
'title.linewidth',
'title.loc',
'title.pad',
'title.size',
'title.weight',
'toplabel.color',
'toplabel.size',
'toplabel.weight',
}
# Used by Axes.format, allows user to pass rc settings as keyword args,
# way less verbose. For example, landcolor='b' vs. rc_kw={'land.color':'b'}.
RC_NODOTSNAMES = { # useful for passing these as kwargs
name.replace('.', ''): name for names in
(RC_LONGNAMES, RC_PARAMNAMES, RC_SHORTNAMES)
for name in names
}
# Categories for returning dict of subcategory properties
RC_CATEGORIES = {
*(re.sub(r'\.[^.]*$', '', name) for names in
(RC_LONGNAMES, RC_PARAMNAMES) for name in names),
*(re.sub(r'\..*$', '', name) for names in
(RC_LONGNAMES, RC_PARAMNAMES) for name in names)
}
def _to_points(key, value):
"""Convert certain rc keys to the units "points"."""
# See: https://matplotlib.org/users/customizing.html, all props matching
# the below strings use the units 'points', except custom categories!
if (isinstance(value, str)
and key.split('.')[0] not in ('colorbar', 'subplots')
and re.match('^.*(width|space|size|pad|len|small|large)$', key)):
value = units(value, 'pt')
return value
def _get_config_paths():
"""Return a list of configuration file paths in reverse order of
precedence."""
# Local configuration
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
paths = paths[::-1] # sort from decreasing to increasing importantce
# Home configuration
ipath = os.path.join(os.path.expanduser('~'), '.proplotrc')
if os.path.exists(ipath) and ipath not in paths:
paths.insert(0, ipath)
return paths
def _get_synced_params(key, value):
"""Return dictionaries for updating the `rcParamsShort`, `rcParamsLong`,
and `rcParams` properties associated with this key."""
kw = {} # builtin properties that global setting applies to
kw_long = {} # custom properties that global setting applies to
kw_short = {} # short name properties
if '.' not in key and key not in rcParamsShort:
key = RC_NODOTSNAMES.get(key, key)
# Skip full name keys
if '.' in key:
pass
# Cycler
elif key in ('cycle', 'rgbcycle'):
if key == 'rgbcycle':
cycle, rgbcycle = rcParamsShort['cycle'], value
else:
cycle, rgbcycle = value, rcParamsShort['rgbcycle']
try:
colors = mcm.cmap_d[cycle].colors
except (KeyError, AttributeError):
cycles = sorted(
name for name,
cmap in mcm.cmap_d.items() if isinstance(
cmap,
mcolors.ListedColormap))
raise ValueError(
f'Invalid cycle name {cycle!r}. Options are: '
', '.join(map(repr, cycles)) + '.'
)
if rgbcycle and cycle.lower() == 'colorblind':
regcolors = colors + [(0.1, 0.1, 0.1)]
elif mcolors.to_rgb('r') != (1.0, 0.0, 0.0): # reset
regcolors = [
(0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0),
(0.75, 0.75, 0.0), (0.75, 0.75, 0.0), (0.0, 0.75, 0.75),
(0.0, 0.0, 0.0)]
else:
regcolors = [] # no reset necessary
for code, color in zip('brgmyck', regcolors):
rgb = mcolors.to_rgb(color)
mcolors.colorConverter.colors[code] = rgb
mcolors.colorConverter.cache[code] = rgb
kw['patch.facecolor'] = colors[0]
kw['axes.prop_cycle'] = cycler.cycler('color', colors)
# Zero linewidth almost always means zero tick length
elif key == 'linewidth' and _to_points(key, value) == 0:
_, ikw_long, ikw = _get_synced_params('ticklen', 0)
kw.update(ikw)
kw_long.update(ikw_long)
# Tick length/major-minor tick length ratio
elif key in ('ticklen', 'ticklenratio'):
if key == 'ticklen':
ticklen = _to_points(key, value)
ratio = rcParamsShort['ticklenratio']
else:
ticklen = rcParamsShort['ticklen']
ratio = value
kw['xtick.minor.size'] = ticklen * ratio
kw['ytick.minor.size'] = ticklen * ratio
# Spine width/major-minor tick width ratio
elif key in ('linewidth', 'tickratio'):
if key == 'linewidth':
tickwidth = _to_points(key, value)
ratio = rcParamsShort['tickratio']
else:
tickwidth = rcParamsShort['linewidth']
ratio = value
kw['xtick.minor.width'] = tickwidth * ratio
kw['ytick.minor.width'] = tickwidth * ratio
# Gridline width
elif key in ('grid.linewidth', 'gridratio'):
if key == 'grid.linewidth':
gridwidth = _to_points(key, value)
ratio = rcParamsShort['gridratio']
else:
gridwidth = rcParams['grid.linewidth']
ratio = value
kw_long['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'):
ovalue = rcParams['axes.grid']
owhich = rcParams['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 we already tested
# if gridminor=False, enable major, and vice versa
value = 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:
value = 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
kw['axes.grid'] = value
kw['axes.grid.which'] = which
# Now update linked settings
value = _to_points(key, value)
if key in rcParamsShort:
kw_short[key] = value
elif key in rcParamsLong:
kw_long[key] = value
elif key in rcParams:
kw[key] = value
else:
raise KeyError(f'Invalid key {key!r}.')
for name in RC_CHILDREN.get(key, ()):
if name in rcParamsLong:
kw_long[name] = value
else:
kw[name] = value
return kw_short, kw_long, kw
def _sanitize_key(key):
"""Convert the key to a palatable value."""
if not isinstance(key, str):
raise KeyError(f'Invalid key {key!r}. Must be string.')
if '.' not in key and key not in rcParamsShort:
key = RC_NODOTSNAMES.get(key, key)
return key.lower()
[docs]class rc_configurator(object):
"""
Magical abstract class for managing matplotlib
`rcParams <https://matplotlib.org/users/customizing.html>`__
and additional ProPlot :ref:`rcParamsLong` and :ref:`rcParamsShort`
settings. When initialized, this loads defaults settings plus any user
overrides in the ``~/.proplotrc`` file. See the `~proplot.rctools`
documentation for details.
"""
def __contains__(self, key):
return key in rcParamsShort or key in rcParamsLong or key in rcParams
def __iter__(self):
for key in sorted((*rcParamsShort, *rcParamsLong, *rcParams)):
yield key
def __repr__(self):
rcdict = type('rc', (dict,), {})(rcParamsShort)
string = type(rcParams).__repr__(rcdict)
indent = ' ' * 4 # indent is rc({
return string.strip(
'})') + f'\n{indent}... (rcParams) ...\n{indent}}})'
def __str__(self): # encapsulate params in temporary class
rcdict = type('rc', (dict,), {})(rcParamsShort)
string = type(rcParams).__str__(rcdict)
return string + '\n... (rcParams) ...'
@_counter # about 0.05s
def __init__(self, local=True):
"""
Parameters
----------
local : bool, optional
Whether to load overrides from local and user ``.proplotrc``
file(s). Default is ``True``.
"""
# Attributes and style
object.__setattr__(self, '_context', [])
with _benchmark(' use'):
style.use('default')
# Update from defaults
rcParams.update(defaultParams)
rcParamsLong.clear()
rcParamsLong.update(defaultParamsLong)
rcParamsShort.clear()
rcParamsShort.update(defaultParamsShort)
for rcdict in (rcParamsShort, rcParamsLong):
for key, value in rcdict.items():
_, rc_long, rc = _get_synced_params(key, value)
rcParamsLong.update(rc_long)
rcParams.update(rc)
# Update from files
if not local:
return
for i, file in enumerate(_get_config_paths()):
if not os.path.exists(file):
continue
with open(file) as f:
try:
data = yaml.safe_load(f)
except yaml.YAMLError as err:
print('{file!r} has invalid YAML syntax.')
raise err
for key, value in (data or {}).items():
try:
rc_short, rc_long, rc = _get_synced_params(key, value)
except KeyError:
raise RuntimeError(f'{file!r} has invalid key {key!r}.')
else:
rcParamsShort.update(rc_short)
rcParamsLong.update(rc_long)
rcParams.update(rc)
def __enter__(self):
"""Apply settings from the most recent context block."""
if not self._context:
raise RuntimeError(
f'rc object must be initialized with rc.context().'
)
*_, kwargs, cache, restore = self._context[-1]
def _update(rcdict, newdict):
for key, value in newdict.items():
restore[key] = rcdict[key]
rcdict[key] = cache[key] = value
for key, value in kwargs.items():
rc_short, rc_long, rc = _get_synced_params(key, value)
_update(rcParamsShort, rc_short)
_update(rcParamsLong, rc_long)
_update(rcParams, rc)
def __exit__(self, *args):
"""Restore settings from the most recent context block."""
if not self._context:
raise RuntimeError(
f'rc object must be initialized with rc.context().'
)
*_, restore = self._context[-1]
for key, value in restore.items():
rc_short, rc_long, rc = _get_synced_params(key, value)
rcParamsShort.update(rc_short)
rcParamsLong.update(rc_long)
rcParams.update(rc)
del self._context[-1]
def __delitem__(self, *args):
"""Raise an error. This enforces pseudo-immutability."""
raise RuntimeError('rc settings cannot be deleted.')
def __delattr__(self, *args):
"""Raise an error. This enforces pseudo-immutability."""
raise RuntimeError('rc settings cannot be deleted.')
[docs] def __getattr__(self, attr):
"""Pass the attribute to `~rc_configurator.__getitem__` and return
the result."""
if attr[:1] == '_':
return super().__getattr__(attr)
else:
return self[attr]
[docs] def __getitem__(self, key):
"""Return an `rcParams \
<https://matplotlib.org/users/customizing.html>`__,
:ref:`rcParamsLong`, or :ref:`rcParamsShort` setting."""
key = _sanitize_key(key)
for kw in (rcParamsShort, rcParamsLong, rcParams):
try:
return kw[key]
except KeyError:
continue
raise KeyError(f'Invalid property name {key!r}.')
[docs] def __setattr__(self, attr, value):
"""Pass the attribute and value to `~rc_configurator.__setitem__`."""
self[attr] = value
[docs] def __setitem__(self, key, value):
"""Modify an `rcParams \
<https://matplotlibcorg/users/customizing.html>`__,
:ref:`rcParamsLong`, and :ref:`rcParamsShort` setting(s)."""
if key == 'matplotlib':
return ipython_matplotlib(value)
elif key == 'autosave':
return ipython_autosave(value)
elif key == 'autoreload':
return ipython_autoreload(value)
rc_short, rc_long, rc = _get_synced_params(key, value)
rcParamsShort.update(rc_short)
rcParamsLong.update(rc_long)
rcParams.update(rc)
def _get_item(self, key, mode=None):
"""As with `~rc_configurator.__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 = min((context[0] for context in self._context), default=0)
caches = (context[2] for context in self._context)
if mode == 0:
rcdicts = (*caches, rcParamsShort, rcParamsLong, rcParams)
elif mode == 1:
rcdicts = (*caches, rcParamsShort, rcParamsLong) # custom only!
elif mode == 2:
rcdicts = (*caches,)
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 property name {key!r}.')
else:
return None
[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 `~rc_configurator.context`.
"""
if cat not in RC_CATEGORIES:
raise ValueError(
f'Invalid rc category {cat!r}. Valid categories are '
', '.join(map(repr, RC_CATEGORIES)) + '.'
)
kw = {}
mode = 0 if not context else None
for rcdict in (rcParamsLong, rcParams):
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, **kwargs):
"""
Temporarily modify the rc settings in a "with as" block.
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. Testing showed that these gratuitous
`rcParams <https://matplotlib.org/users/customizing.html>`__
lookups and artist updates increased runtime by seconds, even for
relatively simple plots. It also resulted in overwriting previous
rc changes with the default values upon subsequent calls to
`~proplot.axes.Axes.format`.
Parameters
----------
*args
Dictionaries of `rc` names and values.
**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 `~rc_configurator.get`,
`~rc_configurator.fill`, and `~rc_configurator.category` within a
"with as" block when called with ``context=True``. The options are
as follows.
0. All settings (`rcParams \
<https://matplotlib.org/users/customizing.html>`__,
:ref:`rcParamsLong`, and :ref:`rcParamsShort`) are returned,
whether or not `~rc_configurator.context` has changed them.
1. Unchanged `rcParams \
<https://matplotlib.org/users/customizing.html>`__
return ``None``. :ref:`rcParamsLong` and :ref:`rcParamsShort`
are returned whether or not `~rc_configurator.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`.
Example
-------
The below applies settings to axes in a specific figure using
`~rc_configurator.context`.
>>> import proplot as plot
>>> with plot.rc.context(linewidth=2, ticklen=5):
... f, ax = plot.subplots()
... ax.plot(data)
By contrast, the below applies settings to a specific axes using
`~proplot.axes.Axes.format`.
>>> import proplot as plot
>>> f, ax = plot.subplots()
>>> ax.format(linewidth=2, ticklen=5)
"""
if mode not in range(3):
raise ValueError(f'Invalid mode {mode!r}.')
for arg in args:
if not isinstance(arg, dict):
raise ValueError('Non-dictionary argument {arg!r}.')
kwargs.update(arg)
self._context.append((mode, kwargs, {}, {}))
return self
[docs] def dict(self):
"""
Return a raw dictionary of all settings.
"""
output = {}
for key in sorted((*rcParamsShort, *rcParamsLong, *rcParams)):
output[key] = self[key]
return output
[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 `~rc_configurator.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 `~rc_configurator.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 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.items`.
"""
for key in self:
yield key
[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[prefix + key] = value
[docs] def reset(self, **kwargs):
"""
Reset the configurator to its initial state.
Parameters
----------
**kwargs
Passed to `rc_configurator`.
"""
self.__init__(**kwargs)
[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 ipython_matplotlib(backend=None, fmt=None):
"""
Set up the `matplotlib backend \
<https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-matplotlib>`__
for ipython sessions and apply the following ``%config InlineBackend``
magic commands.
.. code-block:: ipython
%config InlineBackend.figure_formats = fmt
%config InlineBackend.rc = {} # never override my rc settings!
%config InlineBackend.close_figures = True # memory issues
%config InlineBackend.print_figure_kwargs = {'bbox_inches': None} \
# we use our own tight layout algorithm
This must be called *before drawing any figures*! For some ipython
sessions (e.g. terminals) the backend can only be changed by adding
``matplotlib: backend`` to your ``.proplotrc`` file.
See :ref:`Configuring proplot` for details.
Parameters
----------
backend : str, optional
The backend name. The default is ``'auto'``, which applies
``%matplotlib inline`` for notebooks and ``%matplotlib qt`` for
all other sessions.
Note that when using the qt backend on macOS, you may want to prevent
"tabbed" figure windows by navigating to Settings...Dock and changing
"Prefer tabs when opening documents" to "Manually" (see \
`Issue #13164 <https://github.com/matplotlib/matplotlib/issues/13164>`__).
fmt : str or list of str, optional
The inline backend file format(s). Valid formats include ``'jpg'``,
``'png'``, ``'svg'``, ``'pdf'``, and ``'retina'``. This is ignored
for non-inline backends.
""" # noqa
# Bail out
ipython = get_ipython()
backend = backend or rcParamsShort['matplotlib']
if ipython is None or backend is None:
return
# Default behavior dependent on type of ipython session
# See: https://stackoverflow.com/a/22424821/4970632
ibackend = backend
if backend == 'auto':
if 'IPKernelApp' in getattr(get_ipython(), 'config', ''):
ibackend = 'inline'
else:
ibackend = 'qt'
try:
ipython.magic('matplotlib ' + ibackend)
if 'rc' in globals(): # should always be True, but just in case
rc.reset()
except KeyError:
if backend != 'auto':
_warn_proplot(f'{"%matplotlib " + backend!r} failed.')
# Configure inline backend no matter what type of session this is
# Should be silently ignored for terminal ipython sessions
fmt = fmt or rcParamsShort['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(f'config InlineBackend.figure_formats = {fmt!r}')
ipython.magic('config InlineBackend.rc = {}') # no notebook overrides
ipython.magic('config InlineBackend.close_figures = True') # memory issues
ipython.magic( # use ProPlot tight layout instead
'config InlineBackend.print_figure_kwargs = {"bbox_inches":None}'
)
[docs]def ipython_autoreload(autoreload=None):
"""
Set up the `ipython autoreload utility \
<https://ipython.readthedocs.io/en/stable/config/extensions/autoreload.html>`__
by running the following ipython magic.
.. code-block:: ipython
%autoreload autoreload
This is called on import by default. Add ``autoreload:`` to your
``.proplotrc`` to disable. See :ref:`Configuring proplot` for details.
Parameters
----------
autoreload : float, optional
The autoreload level. Default is :rc:`autoreload`.
""" # noqa
ipython = get_ipython()
autoreload = autoreload or rcParamsShort['autoreload']
if ipython is None or autoreload is None:
return
if 'autoreload' not in ipython.magics_manager.magics['line']:
with IPython.utils.io.capture_output(): # capture annoying message
ipython.magic('load_ext autoreload')
ipython.magic('autoreload ' + str(autoreload))
[docs]def ipython_autosave(autosave=None):
"""
Set up the `ipython autosave utility \
<https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-matplotlib>`__
by running the following ipython magic.
.. code-block:: ipython
%autosave autosave
This is called on import by default. Add ``autosave:`` to your
``.proplotrc`` to disable. See :ref:`Configuring proplot` for details.
Parameters
----------
autosave : float, optional
The autosave interval in seconds. Default is :rc:`autosave`.
""" # noqa
ipython = get_ipython()
autosave = autosave or rcParamsShort['autosave']
if ipython is None or autosave is None:
return
with IPython.utils.io.capture_output(): # capture annoying message
try:
ipython.magic('autosave ' + str(autosave))
except IPython.core.error.UsageError:
pass
#: Instance of `rc_configurator`. This is used to change global settings.
#: See :ref:`Configuring proplot` for details.
rc = rc_configurator()
# Manually call setup functions after rc has been instantiated
# We cannot call these inside rc.__init__ because ipython_matplotlib may
# need to reset the configurator to overwrite backend-imposed settings!
ipython_matplotlib()
ipython_autoreload()
ipython_autosave()