From b9667cf3db2438c7e844299122f3ddb275b74747 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Thu, 19 Dec 2019 13:56:27 -0700 Subject: [PATCH 01/33] Initial commit From e464a99ecfad1263e64ab9c9d4abd19ebbd93e0a Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 16:24:23 -0700 Subject: [PATCH 02/33] Move default props from .proplotrc --> rctools.py --- proplot/.proplotrc | 161 ----------------- proplot/rctools.py | 422 +++++++++++++++++++++++++++++++-------------- 2 files changed, 292 insertions(+), 291 deletions(-) delete mode 100644 proplot/.proplotrc diff --git a/proplot/.proplotrc b/proplot/.proplotrc deleted file mode 100644 index 254b703fa..000000000 --- a/proplot/.proplotrc +++ /dev/null @@ -1,161 +0,0 @@ -#----------------------- -# rcParamsShort defaults -#----------------------- -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" -geogrid: True -grid: True -gridminor: False -gridratio: 0.5 -inlinefmt: retina -innerborders: False -lakes: False -land: False -large: 10 -linewidth: 0.6 -lut: 256 -margin: 0.0 -matplotlib: auto -ocean: False -reso: lo -rgbcycle: False -rivers: False -share: 3 -small: 9 -span: True -tickdir: out -ticklen: 4.0 -ticklenratio: 0.5 -tickpad: 2.0 -tickratio: 0.8 -tight: True - -#---------------------- -# rcParamsLong defaults -#---------------------- -abc.border: True -abc.color: k -abc.linewidth: 1.5 -abc.loc: l -abc.size: # filled by 'large' -abc.style: a -abc.weight: bold -axes.alpha: # if empty, depends on 'savefig.transparent' setting -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: # filled by 'large' -bottomlabel.weight: bold -coast.color: k -coast.linewidth: 0.6 -colorbar.axespad: 0.5em -colorbar.extend: 1.3em -colorbar.framealpha: 0.8 -colorbar.frameon: True -colorbar.grid: False -colorbar.insetextend: 1em -colorbar.insetlength: 8em -colorbar.insetwidth: 1.2em -colorbar.length: 1 -colorbar.loc: right -colorbar.width: 1.5em -geoaxes.edgecolor: # filled by "color" -geoaxes.facecolor: # filled by "facecolor" -geoaxes.linewidth: # filled by "linewidth" -geogrid.alpha: 0.5 -geogrid.color: k -geogrid.labels: False -geogrid.labelsize: # filled by "small" -geogrid.latmax: 90 -geogrid.latstep: 20 -geogrid.linestyle: ':' -geogrid.linewidth: 1.0 -geogrid.lonstep: 30 -gridminor.alpha: # filled by "grid.alpha" -gridminor.color: # filled by "grid.color" -gridminor.linestyle: # filled by "grid.linewidth" -gridminor.linewidth: # filled by "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: # filled by 'large' -leftlabel.weight: bold -ocean.color: w -rightlabel.color: k -rightlabel.size: # filled by 'large' -rightlabel.weight: bold -rivers.color: k -rivers.linewidth: 0.6 -subplots.axpad: 1em -subplots.axwidth: 18em # 2 inches -subplots.innerspace: 1.5em -subplots.pad: 0.5em -subplots.panelpad: 0.5em -subplots.panelspace: 1em -subplots.panelwidth: 4em # 0.45 inches -subplots.titlespace: 2em -subplots.xlabspace: 4em -subplots.ylabspace: 5.5em -suptitle.color: k -suptitle.size: # filled by 'large' -suptitle.weight: bold -tick.labelcolor: # filled by 'color' -tick.labelsize: # filled by '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: # filled by 'large' -title.weight: normal -toplabel.color: k -toplabel.size: # filled by 'large' -toplabel.weight: bold - -#--------------------------------------------------- -# rcParams defaults -# See: https://matplotlib.org/users/customizing.html -#--------------------------------------------------- -axes.labelpad: 3.0 -axes.titlepad: 3.0 -figure.autolayout: False -figure.facecolor: '#f2f2f2' -grid.alpha: 0.1 -grid.color: 'k' -grid.linestyle: '-' -grid.linewidth: 0.6 -hatch.color: k -hatch.linewidth: 0.6 -legend.borderaxespad: 0 -legend.columnspacing: 1.0 -legend.fancybox: False -legend.handletextpad: 0.5 -lines.linewidth: 1.3 -lines.markersize: 3.0 -savefig.bbox: standard -savefig.directory: -savefig.dpi: 300 -savefig.format: pdf -savefig.pad_inches: 0.0 -savefig.transparent: True -xtick.minor.visible: True -ytick.minor.visible: True diff --git a/proplot/rctools.py b/proplot/rctools.py index 679c19280..0934021e0 100644 --- a/proplot/rctools.py +++ b/proplot/rctools.py @@ -6,32 +6,277 @@ # 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 -from matplotlib import rcParams as rcParams -from .utils import _warn_proplot, _counter, _benchmark, units import re import os import yaml -import cycler import numpy as np -import matplotlib.colors as mcolors -import matplotlib.cm as mcm -with _benchmark('pyplot'): - import matplotlib.pyplot as plt +# import cycler +# import matplotlib.colors as mcolors +# import matplotlib.cm as mcm +from matplotlib import style, rcParams try: import IPython - get_ipython = IPython.get_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': 9, + 'linewidth': 0.6, + 'lut': 256, + 'margin': 0.0, + 'matplotlib': 'auto', + 'nbsetup': True, + 'ocean': False, + 'reso': 'lo', + 'rgbcycle': False, + 'rivers': False, + 'share': 3, + 'small': 8, + '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.dpi': 90, + '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 = { @@ -41,27 +286,24 @@ def get_ipython(): 'lut': ( 'image.lut', ), - 'alpha': ( - 'axes.alpha', - ), # this is a custom setting + 'alpha': ( # this is a custom setting + 'axes.facealpha', + ), 'facecolor': ( 'axes.facecolor', 'geoaxes.facecolor' ), 'fontname': ( 'font.family', ), - # change the 'color' of an axes - 'color': ( + 'color': ( # change the 'color' of an axes 'axes.edgecolor', 'geoaxes.edgecolor', 'axes.labelcolor', 'tick.labelcolor', 'hatch.color', 'xtick.color', 'ytick.color' ), - # the 'small' fonts - 'small': ( + 'small': ( # the 'small' fonts 'font.size', 'tick.labelsize', 'xtick.labelsize', 'ytick.labelsize', 'axes.labelsize', 'legend.fontsize', 'geogrid.labelsize' ), - # the 'large' fonts - 'large': ( + 'large': ( # the 'large' fonts 'abc.size', 'figure.titlesize', 'axes.titlesize', 'suptitle.size', 'title.size', 'leftlabel.size', 'toplabel.size', @@ -249,13 +491,9 @@ def get_ipython(): def _convert_units(key, value): - """Converts certain keys to the units "points". If "key" is passed, tests - that key against possible keys that accept physical units.""" + """Convert certain rc keys to the units "points".""" # See: https://matplotlib.org/users/customizing.html, all props matching - # the strings use the units 'points', and special categories are inches! - # WARNING: Must keep colorbar and subplots units alive, so when user - # requests em units, values change with respect to font size. The points - # thing is a conveniene feature so not as important for them. + # 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)): @@ -263,38 +501,10 @@ def _convert_units(key, value): return value -def _set_cycler(name): - """Sets the default color cycler.""" - # Draw from dictionary - try: - colors = mcm.cmap_d[name].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 {name!r}. Options are: {", ".join(cycles)}') - # Apply color name definitions - if rcParamsShort['rgbcycle'] and name.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 - # Pass to cycle constructor - rcParams['patch.facecolor'] = colors[0] - rcParams['axes.prop_cycle'] = cycler.cycler('color', colors) - - def _get_config_paths(): - """Return configuration file paths in reverse order of precedence.""" - # Get paths + """Return a list of configuration file paths in reverse order of + precedence.""" + # Local configuration idir = os.getcwd() paths = [] while idir: # not empty string @@ -310,11 +520,6 @@ def _get_config_paths(): ipath = os.path.join(os.path.expanduser('~'), '.proplotrc') if os.path.exists(ipath) and ipath not in paths: paths.insert(0, ipath) - # Global configuration - ipath = os.path.join(os.path.dirname(__file__), '.proplotrc') - if ipath in paths: - paths.remove(ipath) - paths.insert(0, ipath) return paths @@ -388,78 +593,35 @@ def __contains__(self, key): RC_PARAMNAMES or key in RC_NODOTSNAMES) # biggest lists last @_counter # about 0.05s - def __init__(self): - """Magical abstract class for managing matplotlib `rcParams \ -`__ - settings, ProPlot :ref:`rcParamsLong` settings, and - :ref:`rcParamsShort` "global" settings. This starts with the - default settings plus user ``.proplotrc`` overrides. - See :ref:`Configuring proplot` for details.""" - # Set the default style. Note that after first figure made, backend - # is 'sticky', never changes! See: - # https://stackoverflow.com/a/48322150/4970632 - plt.style.use('default') - - # Prefer more widely used fonts - # NOTE: Without 'mathtext.fontset' = 'custom', 'fallback_to_cm' is - # always carried out! The default 'mathtext.fontset' = 'dejavusans' - # creates a DejaVuFonts class which is evidently a misnomer, *always* - # falls back to CM and tries to use *active* font rather than DejaVu. - # NOTE: This will be put in rcParamsDefaults dictionary when #50 - # is merged! For now do not add to .proplotrc. - import matplotlib.mathtext as _ # noqa - import logging - logger = logging.getLogger('matplotlib.mathtext') - logger.setLevel(logging.ERROR) # suppress warnings! - rcParams['mathtext.fontset'] = 'custom' - rcParams['mathtext.default'] = 'regular' - rcParams['text.usetex'] = False - rcParams['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', - ) - rcParams['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' - ) - rcParams['font.monospace'] = ( - 'Andale Mono', - 'Nimbus Mono L', - 'Courier New', - 'Courier', - 'Fixed', - 'Terminal', - 'Computer Modern Typewriter', - 'DejaVu Sans Mono', - 'Bitstream Vera Sans Mono', - 'monospace' - ) - - # Load the defaults from file + 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()): - # Load if not os.path.exists(file): continue with open(file) as f: From e7608078d9823a1c76ed4d0e23381583976914fb Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 16:36:57 -0700 Subject: [PATCH 03/33] Improve rc_configurator getitem/setitem, add helper func --- proplot/rctools.py | 546 +++++++++++++++++++++------------------------ 1 file changed, 249 insertions(+), 297 deletions(-) diff --git a/proplot/rctools.py b/proplot/rctools.py index 0934021e0..14d504a0f 100644 --- a/proplot/rctools.py +++ b/proplot/rctools.py @@ -10,9 +10,9 @@ import os import yaml import numpy as np -# import cycler -# import matplotlib.colors as mcolors -# import matplotlib.cm as mcm +import cycler +import matplotlib.colors as mcolors +import matplotlib.cm as mcm from matplotlib import style, rcParams try: import IPython @@ -490,7 +490,7 @@ def _tabulate(rcdict): } -def _convert_units(key, value): +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! @@ -523,71 +523,173 @@ def _get_config_paths(): return paths -def _get_synced_params(key=None, value=None): - """Returns dictionaries for updating "child" properties in - `rcParams` and `rcParamsLong` with global property.""" +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_custom = {} # custom properties that global setting applies to - if key is not None and value is not None: - items = [(key, value)] - else: - items = rcParamsShort.items() - for key, value in items: - # Tick length/major-minor tick length ratio - if key in ('ticklen', 'ticklenratio'): - if key == 'ticklen': - ticklen = _convert_units(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 = _convert_units(key, value) - ratio = rcParamsShort['tickratio'] - else: - tickwidth = rcParamsShort['linewidth'] - ratio = value - kw['xtick.minor.width'] = tickwidth * ratio - kw['ytick.minor.width'] = tickwidth * ratio - # Grid line - elif key in ('grid.linewidth', 'gridratio'): - if key == 'grid.linewidth': - gridwidth = _convert_units(key, value) - ratio = rcParamsShort['gridratio'] + 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 + + # Special ipython settings + # TODO: Put this inside __setitem__? + elif key == 'matplotlib': + ipython_matplotlib(value) + elif key == 'autosave': + ipython_autosave(value) + elif key == 'autoreload': + ipython_autoreload(value) + + # 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: - gridwidth = rcParams['grid.linewidth'] - ratio = value - kw_custom['gridminor.linewidth'] = gridwidth * ratio - # Now update linked settings - val = None - for name in RC_CHILDREN.get(key, ()): - val = _convert_units(key, value) - if name in rcParamsLong: - kw_custom[name] = val + 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: - kw[name] = val - if key == 'linewidth' and val == 0: - ikw, ikw_custom = _get_synced_params('ticklen', 0) - kw.update(ikw) - kw_custom.update(ikw_custom) - return kw, kw_custom + 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 -class rc_configurator(object): - _public_api = ( - 'get', 'fill', 'category', 'reset', 'context', 'update' - ) # getattr and setattr will not look for these items +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() - def __str__(self): - return type(rcParams).__str__(rcParamsShort) # just show globals - - def __repr__(self): - return type(rcParams).__repr__(rcParamsShort) +class rc_configurator(object): + """ + Magical abstract class for managing matplotlib + `rcParams `__ + 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 RC_SHORTNAMES or key in RC_LONGNAMES or key in RC_PARAMNAMES or key in RC_NODOTSNAMES) # biggest lists last @@ -630,262 +732,112 @@ def __init__(self, local=True): except yaml.YAMLError as err: print('{file!r} has invalid YAML syntax.') raise err - # Special duplicate keys - if data is None: - continue - # Deprecated nbsetup - # No warning for now, just keep it undocumented - format = data.pop('format', None) - if format is not None: - data['inlinefmt'] = format - nbsetup = data.pop('nbsetup', None) - if nbsetup is not None and not nbsetup: - data['matplotlib'] = None - data['autoreload'] = None - data['autosave'] = None - # Add keys to dictionaries - gkeys, ckeys = {*()}, {*()} - for key, value in data.items(): - if key in RC_SHORTNAMES: - rcParamsShort[key] = value - if i == 0: - gkeys.add(key) - elif key in RC_LONGNAMES: - value = _convert_units(key, value) - rcParamsLong[key] = value - if i == 0: - ckeys.add(key) - elif key in RC_PARAMNAMES: - value = _convert_units(key, value) - rcParams[key] = value - else: + for key, value in (data or {}).items(): + try: + self[key] = value + except KeyError: raise RuntimeError(f'{file!r} has invalid key {key!r}.') - # Make sure we did not miss anything - if i == 0: - if gkeys != RC_SHORTNAMES: - raise RuntimeError( - f'{file!r} has incomplete or invalid global keys ' - f'{RC_SHORTNAMES - gkeys}.') - if ckeys != RC_LONGNAMES: - raise RuntimeError( - f'{file!r} has incomplete or invalid custom keys ' - f'{RC_LONGNAMES - ckeys}.') - - # Apply *global settings* to children settings - rcParams['axes.titlepad'] = rcParamsLong['title.pad'] - _set_cycler(rcParamsShort['cycle']) - rc, rc_new = _get_synced_params() - for key, value in rc.items(): - rcParams[key] = value - for key, value in rc_new.items(): - rcParamsLong[key] = value - - # Caching stuff - self._init = True - self._getitem_mode = 0 - self._context = {} - self._cache = {} - self._cache_orig = {} - self._cache_restore = {} + + def __enter__(self): + """Apply settings from the most recent context block.""" + if not self._context: + raise RuntimeError( + f'rc context 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 context must be initialized with rc.context().') + *_, restore = self._context[-1] + for key, value in restore.items(): + self[key] = value + 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.') + + 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] def __getitem__(self, key): - """Get `rcParams `__, - :ref:`rcParamsLong`, and :ref:`rcParamsShort` settings. If we are in - a `~rc_configurator.context` block, may return ``None`` if the setting - is not cached (i.e. if it was not changed by the user).""" - # Can get a whole bunch of different things - # Get full dictionary e.g. for rc[None] - if not key: - return {**rcParams, **rcParamsLong} - - # Standardize - # NOTE: If key is invalid, raise error down the line. - if '.' not in key and key not in rcParamsShort: - key = RC_NODOTSNAMES.get(key, key) + """Return an `rcParams \ +`__, + :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}.') - # Allow for special time-saving modes where we *ignore rcParams* - # or even *ignore rcParamsLong*. - mode = self._getitem_mode + def __setattr__(self, attr, value): + """Pass the attribute and value to `~rc_configurator.__setitem__`.""" + self[attr] = value + + def __setitem__(self, key, value): + """Modify an `rcParams \ +`__, + :ref:`rcParamsLong`, and :ref:`rcParamsShort` setting(s).""" + 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: - kws = (self._cache, rcParamsShort, rcParamsLong, rcParams) + rcdicts = (*caches, rcParamsShort, rcParamsLong, rcParams) elif mode == 1: - kws = (self._cache, rcParamsShort, rcParamsLong) # custom only! + rcdicts = (*caches, rcParamsShort, rcParamsLong) # custom only! elif mode == 2: - kws = (self._cache,) # changed only! + rcdicts = (*caches,) else: raise KeyError(f'Invalid caching mode {mode!r}.') - - # Get individual property. Will successively index a few different - # dicts. Try to return the value - for kw in kws: + for rcdict in rcdicts: + if not rcdict: + continue try: - return kw[key] + return rcdict[key] except KeyError: continue - # If we were in one of the exclusive modes, return None if mode == 0: - raise KeyError(f'Invalid prop name {key!r}.') + raise KeyError(f'Invalid property name {key!r}.') else: return None - def __setitem__(self, key, value): - """Set `rcParams `__, - :ref:`rcParamsLong`, and :ref:`rcParamsShort` settings.""" - # Check whether we are in context block - # NOTE: Do not add key to cache until we are sure it is a valid key - cache = self._cache - context = bool(self._context) # test if context dict is non-empty - if context: - restore = self._cache_restore - - # Standardize - # NOTE: If key is invalid, raise error down the line. - if '.' not in key and key not in rcParamsShort: - key = RC_NODOTSNAMES.get(key, key) - - # Special keys - if key == 'title.pad': - key = 'axes.titlepad' - if key == 'matplotlib': - ipython_matplotlib(value) - if key == 'autosave': - ipython_autosave(value) - if key == 'autoreload': - ipython_autoreload(value) - if key == 'rgbcycle': # if must re-apply cycler afterward - cache[key] = value - rcParamsShort[key] = value - key, value = 'cycle', rcParamsShort['cycle'] - - # Set the default cycler - if key == 'cycle': - cache[key] = value - if context: - restore[key] = rcParamsShort[key] - restore['axes.prop_cycle'] = rcParams['axes.prop_cycle'] - restore['patch.facecolor'] = rcParams['patch.facecolor'] - _set_cycler(value) - - # Gridline toggling, complicated because of the clunky way this is - # implemented in matplotlib. There should be a gridminor setting! - elif key in ('grid', 'gridminor'): - cache[key] = value - ovalue = rcParams['axes.grid'] - owhich = rcParams['axes.grid.which'] - if context: - restore[key] = rcParamsShort[key] - restore['axes.grid'] = ovalue - restore['axes.grid.which'] = owhich - # 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 - value = True - # if gridminor=False, enable major, and vice versa - 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 - cache.update({'axes.grid': value, 'axes.grid.which': which}) - rcParams.update({'axes.grid': value, 'axes.grid.which': which}) - - # Ordinary settings - elif key in rcParamsShort: - # Update global setting - cache[key] = value - if context: - restore[key] = rcParamsShort[key] - rcParamsShort[key] = value - # Update children of setting - rc, rc_new = _get_synced_params(key, value) - cache.update(rc) - cache.update(rc_new) - if context: - restore.update({key: rcParams[key] for key in rc}) - restore.update({key: rcParamsLong[key] for key in rc_new}) - rcParams.update(rc) - rcParamsLong.update(rc_new) - # Update normal settings - elif key in RC_LONGNAMES: - value = _convert_units(key, value) - cache[key] = value - if context: - restore[key] = rcParamsLong[key] - rcParamsLong[key] = value - elif key in RC_PARAMNAMES: - value = _convert_units(key, value) - cache[key] = value - if context: - restore[key] = rcParams[key] - rcParams[key] = value # rcParams dict has key validation - else: - raise KeyError(f'Invalid key {key!r}.') - self._init = False # we are no longer in initial state - # Attributes same as items - def __getattr__(self, attr): - """Invoke `~rc_configurator.__getitem__` so that ``rc.key`` - is identical to ``rc[key]``.""" - if attr[:1] == '_': - return object.__getattr__(self, attr) - else: - return self[attr] - def __setattr__(self, attr, value): - """Invoke `~rc_configurator.__setitem__` so that ``rc.key = value`` - is identical to ``rc[key] = value``.""" - if attr[:1] == '_': - object.__setattr__(self, attr, value) - else: - self[attr] = value - # Immutability - def __delitem__(self, *args): - """Pseudo-immutability.""" - raise RuntimeError('rc settings cannot be deleted.') - - def __delattr__(self, *args): - """Pseudo-immutability.""" - raise RuntimeError('rc settings cannot be deleted.') - # Context tools - def __enter__(self): - """Apply settings from configurator cache.""" - self._cache_orig = rc._cache.copy() - self._cache_restore = {} # shouldn't be necessary but just in case - self._cache = {} - for key, value in self._context.items(): - self[key] = value # applies linked and individual settings - def __exit__(self, *args): - """Restore configurator cache to initial state.""" - self._context = {} - self._getitem_mode = 0 - for key, value in self._cache_restore.items(): - self[key] = value - self._cache = self._cache_orig - self._cache_restore = {} - self._cache_orig = {} def context(self, *args, mode=0, **kwargs): """ From d6b60201645cc6568d30d887e118350dc2960afa Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 16:40:01 -0700 Subject: [PATCH 04/33] Update get(), fill(), category(); add dict() and iterator methods --- proplot/rctools.py | 168 ++++++++++++++++++++++++--------------------- 1 file changed, 88 insertions(+), 80 deletions(-) diff --git a/proplot/rctools.py b/proplot/rctools.py index 14d504a0f..f746ed1d4 100644 --- a/proplot/rctools.py +++ b/proplot/rctools.py @@ -833,10 +833,35 @@ def _get_item(self, key, mode=None): else: return None + def category(self, cat, *, context=False): + """ + Return a dictionary of settings beginning with the substring + ``cat + '.'``. - - - + Parameters + ---------- + cat : str, optional + The `rc` settings category. + 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.search(f'^{cat}[.][^.]+$', key): + continue + value = self._get_item(key, mode) + if value is None: + continue + kw[key] = value + return kw def context(self, *args, mode=0, **kwargs): @@ -901,100 +926,70 @@ def context(self, *args, mode=0, **kwargs): self._getitem_mode = mode return self - # Other tools - def get(self, key, cache=False): + def dict(self): + """ + Return a raw dictionary of all settings. + """ + output = {} + for key in sorted((*rcParamsShort, *rcParamsLong, *rcParams)): + output[key] = self[key] + return output + + def get(self, key, *, context=False): """ - Returns a setting. + Return a single setting. Parameters ---------- key : str The setting name. - cache : bool, optional - If ``False``, the `~rc_configurator.__getitem__` mode is - temporarily set to ``0`` (see `~rc_configurator.context`). + context : bool, optional + If ``True``, then ``None`` is returned if the setting is not found + in the context mode dictionaries. See `~rc_configurator.context`. """ - if not cache: - orig = self._getitem_mode - self._getitem_mode = 0 - item = self[key] - if not cache: - self._getitem_mode = orig - return item - - def fill(self, props, cache=True): + mode = 0 if not context else None + return self._get_item(key, mode) + + def fill(self, props, *, context=False): """ - Returns a dictionary filled with `rc` settings, used internally to - build dictionaries for updating `~matplotlib.artist.Artist` instances. + Return a dictionary filled with settings whose names match the + string values in the input dictionary. Parameters ---------- props : dict-like - Dictionary whose values are names of `rc` settings. The values + Dictionary whose values are names of settings. The values are replaced with the corresponding property only if `~rc_configurator.__getitem__` does not return ``None``. Otherwise, that key, value pair is omitted from the output dictionary. - cache : bool, optional - If ``False``, the `~rc_configurator.__getitem__` mode is - temporarily set to ``0`` (see `~rc_configurator.context`). - Otherwise, if an `rc` lookup returns ``None``, the setting is - omitted from the output dictionary. + 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`. """ - if not cache: - orig = self._getitem_mode - self._getitem_mode = 0 - props_out = {} + kw = {} + mode = 0 if not context else None for key, value in props.items(): - item = self[value] + item = self._get_item(value, mode) if item is not None: - props_out[key] = item - if not cache: - self._getitem_mode = orig - return props_out + kw[key] = item + return kw - def category(self, cat, cache=True): + def items(self): """ - Returns a dictionary of settings belonging to the indicated category, - i.e. settings beginning with the substring ``cat + '.'``. - - Parameters - ---------- - cat : str, optional - The `rc` settings category. - cache : bool, optional - If ``False``, the `~rc_configurator.__getitem__` mode is - temporarily set to ``0`` (see `~rc_configurator.context`). + Return an iterator that loops over all setting names and values. + Same as `dict.items`. """ - # Check - if cat not in RC_CATEGORIES: - raise ValueError( - f'RC category {cat!r} does not exist. Valid categories are ' - f', '.join(map(repr, RC_CATEGORIES)) + '.') - if not cache: - mode = 0 - else: - mode = self._getitem_mode + for key in self: + yield key, self[key] - # Allow for special time-saving modes where we *ignore rcParams* - # or even *ignore rcParamsLong*. - if mode == 0: - kws = (self._cache, rcParamsShort, rcParamsLong, rcParams) - elif mode == 1: - kws = (self._cache, rcParamsShort, rcParamsLong) - elif mode == 2: - kws = (self._cache, rcParamsShort) - else: - raise KeyError(f'Invalid caching mode {mode}.') - - # Return params dictionary - params = {} - for kw in kws: - for category, value in kw.items(): - if re.search(rf'^{cat}\.', category): - subcategory = re.sub(rf'^{cat}\.', '', category) - if subcategory and '.' not in subcategory: - params[subcategory] = value - return params + def keys(self): + """ + Return an iterator that loops over all setting names. + Same as `dict.items`. + """ + for key in self: + yield key def update(self, *args, **kwargs): """ @@ -1037,11 +1032,24 @@ def update(self, *args, **kwargs): for key, value in kw.items(): self[prefix + key] = value - def reset(self): - """Restores settings to the initial state -- ProPlot defaults, plus - any user overrides in the ``~/.proplotrc`` file.""" - if not self._init: # save resources if rc is unchanged! - return self.__init__() + def reset(self, **kwargs): + """ + Reset the configurator to its initial state. + + Parameters + ---------- + **kwargs + Passed to `rc_configurator`. + """ + self.__init__(**kwargs) + + def values(self): + """ + Return an iterator that loops over all setting values. + Same as `dict.values`. + """ + for key in self: + yield self[key] def ipython_matplotlib(backend=None, fmt=None): From a82dd689bbd4ba5a533e8477a7b4315f0a70b4b4 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 16:41:03 -0700 Subject: [PATCH 05/33] Fix rc_configurator.context() bug --- proplot/rctools.py | 65 +++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/proplot/rctools.py b/proplot/rctools.py index f746ed1d4..a1c1e8226 100644 --- a/proplot/rctools.py +++ b/proplot/rctools.py @@ -866,32 +866,33 @@ def category(self, cat, *, context=False): def context(self, *args, mode=0, **kwargs): """ - Temporarily modifies settings in a ``with...as`` block, - used by ProPlot internally but may also be useful for power users. + Temporarily modify the rc settings in a "with as" block. - This function was invented to prevent successive calls to + 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 `__ lookups and artist updates increased runtime by seconds, even for - relatively simple plots. + 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 setting names and values. + Dictionaries of `rc` names and values. **kwargs - Setting names and values passed as keyword arguments. + `rc` names and values passed as keyword arguments. If the + name has dots, simply omit them. Other parameters ---------------- mode : {0,1,2}, optional - The `~rc_configurator.__getitem__` mode. - Dictates the behavior of the `rc` object within a ``with...as`` - block when settings are requested with e.g. :rcraw:`setting`. If - you are using `~rc_configurator.context` manually, the `mode` is - automatically set to ``0`` -- other input is ignored. Internally, - ProPlot uses all of the three available modes. + 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 \ `__, @@ -901,29 +902,37 @@ def context(self, *args, mode=0, **kwargs): `__ return ``None``. :ref:`rcParamsLong` and :ref:`rcParamsShort` are returned whether or not `~rc_configurator.context` has - changed them. This is used in the ``__init__`` call to - `~proplot.axes.Axes.format`. When a setting lookup returns + 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 _getitem_mode {mode}.') + raise ValueError(f'Invalid mode {mode!r}.') for arg in args: if not isinstance(arg, dict): - raise ValueError('Non-dictionary argument.') + raise ValueError('Non-dictionary argument {arg!r}.') kwargs.update(arg) - self._context = kwargs # could be empty - self._getitem_mode = mode + self._context.append((mode, kwargs, {}, {})) return self def dict(self): @@ -993,20 +1002,22 @@ def keys(self): def update(self, *args, **kwargs): """ - Bulk updates settings, usage is similar to python `dict` objects. + Update multiple settings at once. Parameters ---------- *args : str, dict, or (str, dict) - Positional arguments can be a dictionary of `rc` settings and/or - a "category" string name. If a category name is passed, all - settings in the dictionary (if it was passed) and all keyword arg - names (if they were passed) are prepended with the string - ``cat + '.'``. For example, - ``rc.update('axes', labelsize=20, titlesize=20)`` - changes the ``axes.labelsize`` and ``axes.titlesize`` properties. + The first argument can optionally be a "category" string name, + in which case all other setting names passed to this function are + prepended with the string ``cat + '.'``. For example, + ``rc.update('axes', labelsize=20, titlesize=20)`` changes the + :rcraw:`axes.labelsize` and :rcraw:`axes.titlesize` properties. + + The first or second argument can also be a dictionary of `rc` + names and values. **kwargs - `rc` settings passed as keyword args. + `rc` names and values passed as keyword arguments. If the + name has dots, simply omit them. """ # Parse args kw = {} From dea6d94dd79569585f8e34d2258920d9b43b95e8 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 16:41:56 -0700 Subject: [PATCH 06/33] Improve iter, contains, and string repr methods --- proplot/rctools.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/proplot/rctools.py b/proplot/rctools.py index a1c1e8226..dce8f5a35 100644 --- a/proplot/rctools.py +++ b/proplot/rctools.py @@ -691,8 +691,23 @@ class rc_configurator(object): documentation for details. """ def __contains__(self, key): - return (key in RC_SHORTNAMES or key in RC_LONGNAMES or key in - RC_PARAMNAMES or key in RC_NODOTSNAMES) # biggest lists last + 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): @@ -863,7 +878,6 @@ def category(self, cat, *, context=False): kw[key] = value return kw - def context(self, *args, mode=0, **kwargs): """ Temporarily modify the rc settings in a "with as" block. From 44a4bc9446062ad06dc676415f7fe8cb57e62b51 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 17:04:17 -0700 Subject: [PATCH 07/33] Improve _loc_translate + _get_title_props, related cleanup --- proplot/axes.py | 183 ++++++++++++++++++++++++++---------------------- 1 file changed, 101 insertions(+), 82 deletions(-) diff --git a/proplot/axes.py b/proplot/axes.py index 0ce9e02ea..34b900e50 100644 --- a/proplot/axes.py +++ b/proplot/axes.py @@ -4,7 +4,7 @@ """ import numpy as np import functools -from numbers import Integral +from numbers import Integral, Number import matplotlib.projections as mproj import matplotlib.axes as maxes import matplotlib.dates as mdates @@ -43,8 +43,13 @@ # Translator for inset colorbars and legends ABC_STRING = 'abcdefghijklmnopqrstuvwxyz' +SIDE_TRANSLATE = { + 'l': 'left', + 'r': 'right', + 'b': 'bottom', + 't': 'top', +} LOC_TRANSLATE = { - None: None, 'inset': 'best', 'i': 'best', 0: 'best', @@ -71,12 +76,6 @@ 'uc': 'upper center', 'lc': 'lower center', } -SIDE_TRANSLATE = { - 'l': 'left', - 'r': 'right', - 'b': 'bottom', - 't': 'top', -} def _abc(i): @@ -242,80 +241,94 @@ def _get_extent_axes(self, x): return [pax, *axs] def _get_title_props(self, abc=False, loc=None): - """Returns standardized location name, position keyword arguments, and - setting keyword arguments for the relevant title or a-b-c label at + """Return the standardized location name, position keyword arguments, + and setting keyword arguments for the relevant title or a-b-c label at location `loc`.""" - # Props - # NOTE: Sometimes we load all properties from rc object, sometimes - # just changed ones. This is important if e.g. user calls in two - # lines ax.format(titleweight='bold') then ax.format(title='text'), - # don't want to override custom setting with rc default setting. - def props(cache): - return rc.fill({ - 'fontsize': f'{prefix}.size', - 'weight': f'{prefix}.weight', - 'color': f'{prefix}.color', - 'border': f'{prefix}.border', - 'linewidth': f'{prefix}.linewidth', - 'fontfamily': 'font.family', - }, cache=cache) - # Location string and position coordinates - cache = True + context = True prefix = 'abc' if abc else 'title' - loc = _notNone(loc, rc[f'{prefix}.loc']) - iloc = getattr(self, '_' + ('abc' if abc else 'title') + '_loc') # old + loc = _notNone(loc, rc.get(f'{prefix}.loc', context=True)) + loc_prev = getattr( + self, '_' + ('abc' if abc else 'title') + + '_loc') # old if loc is None: - loc = iloc - elif iloc is not None and loc != iloc: - cache = False - - # Above axes - loc = LOC_TRANSLATE.get(loc, loc) - if loc in ('top', 'bottom'): - raise ValueError(f'Invalid title location {loc!r}.') - elif loc in ('left', 'right', 'center'): - kw = props(cache) - kw.pop('border', None) # no border for titles outside axes - kw.pop('linewidth', None) + loc = loc_prev + elif loc_prev is not None and loc != loc_prev: + context = False + try: + loc = self._loc_translate(loc) + except KeyError: + raise ValueError(f'Invalid title or abc loc {loc!r}.') + else: + if loc in ('top', 'bottom', 'best') or not isinstance(loc, str): + raise ValueError(f'Invalid title or abc loc {loc!r}.') + + # Existing object + if loc in ('left', 'right', 'center'): if loc == 'center': obj = self.title else: obj = getattr(self, '_' + loc + '_title') - # Inside axes elif loc in self._titles_dict: - kw = props(cache) obj = self._titles_dict[loc] + # New object else: - kw = props(False) + context = False width, height = self.get_size_inches() if loc in ('upper center', 'lower center'): x, ha = 0.5, 'center' elif loc in ('upper left', 'lower left'): - xpad = rc.get('axes.titlepad') / (72 * width) + xpad = rc['axes.titlepad'] / (72 * width) x, ha = 1.5 * xpad, 'left' elif loc in ('upper right', 'lower right'): - xpad = rc.get('axes.titlepad') / (72 * width) + xpad = rc['axes.titlepad'] / (72 * width) x, ha = 1 - 1.5 * xpad, 'right' else: - raise ValueError(f'Invalid title or abc "loc" {loc}.') + raise RuntimeError # should be impossible if loc in ('upper left', 'upper right', 'upper center'): - ypad = rc.get('axes.titlepad') / (72 * height) + ypad = rc['axes.titlepad'] / (72 * height) y, va = 1 - 1.5 * ypad, 'top' elif loc in ('lower left', 'lower right', 'lower center'): - ypad = rc.get('axes.titlepad') / (72 * height) + ypad = rc['axes.titlepad'] / (72 * height) y, va = 1.5 * ypad, 'bottom' + else: + raise RuntimeError # should be impossible obj = self.text(x, y, '', ha=ha, va=va, transform=self.transAxes) obj.set_transform(self.transAxes) + + # Return location, object, and settings + # NOTE: Sometimes we load all properties from rc object, sometimes + # just changed ones. This is important if e.g. user calls in two + # lines ax.format(titleweight='bold') then ax.format(title='text') + kw = rc.fill({ + 'fontsize': f'{prefix}.size', + 'weight': f'{prefix}.weight', + 'color': f'{prefix}.color', + 'border': f'{prefix}.border', + 'linewidth': f'{prefix}.linewidth', + 'fontfamily': 'font.family', + }, context=context) + if loc in ('left', 'right', 'center'): + kw.pop('border', None) + kw.pop('linewidth', None) return loc, obj, kw @staticmethod - def _loc_translate(loc, **kwargs): - """Translates location string `loc` into a standardized form.""" - if loc is True: - loc = 'r' # for on-the-fly colorbars and legends + def _loc_translate(loc, default=None): + """Return the location string `loc` translated into a standardized + form.""" + if loc in (None, True): + loc = default elif isinstance(loc, (str, Integral)): - loc = LOC_TRANSLATE.get(loc, loc) + try: + loc = LOC_TRANSLATE[loc] + except KeyError: + raise KeyError(f'Invalid location {loc!r}.') + elif np.iterable(loc) and len(loc) == 2 and all( + isinstance(l, Number) for l in loc): + loc = np.array(loc) + else: + raise KeyError(f'Invalid location {loc!r}.') return loc def _make_inset_locator(self, bounds, trans): @@ -877,8 +890,8 @@ def colorbar(self, *args, loc=None, pad=None, alpha=None, linewidth=None, edgecolor=None, facecolor=None, **kwargs): """ - Adds colorbar as an *inset* or along the outside edge of the axes. - See `~proplot.wrappers.colorbar_wrapper` for details. + Add an *inset* colorbar or *outer* colorbar along the outside edge of + the axes. See `~proplot.wrappers.colorbar_wrapper` for details. Parameters ---------- @@ -903,7 +916,7 @@ def colorbar(self, *args, loc=None, pad=None, pad : float or str, optional The space between the axes edge and the colorbar. For inset colorbars only. Units are interpreted by `~proplot.utils.units`. - Default is :rc:`colorbar.axespad`. + Default is :rc:`colorbar.insetpad`. length : float or str, optional The colorbar length. For outer colorbars, units are relative to the axes width or height. Default is :rc:`colorbar.length`. For inset @@ -911,17 +924,19 @@ def colorbar(self, *args, loc=None, pad=None, is :rc:`colorbar.insetlength`. width : float or str, optional The colorbar width. Units are interpreted by - `~proplot.utils.units`. Default is :rc:`colorbar.width` or + `~proplot.utils.units`. For outer colorbars, default is + :rc:`colorbar.width`. For inset colorbars, default is :rc:`colorbar.insetwidth`. space : float or str, optional - The space between the colorbar and the main axes. For outer - colorbars only. Units are interpreted by `~proplot.utils.units`. + For outer colorbars only. The space between the colorbar and the + main axes. Units are interpreted by `~proplot.utils.units`. When :rcraw:`tight` is ``True``, this is adjusted automatically. - Otherwise, defaut is :rc:`subplots.panelspace`. + When :rcraw:`tight` is ``False``, the default is + :rc:`subplots.panelspace`. frame, frameon : bool, optional - Whether to draw a frame around inset colorbars, just like - `~matplotlib.axes.Axes.legend`. - Default is :rc:`colorbar.frameon`. + For inset colorbars, indicates whether to draw a "frame", just + like `~matplotlib.axes.Axes.legend`. Default is + :rc:`colorbar.frameon`. alpha, linewidth, edgecolor, facecolor : optional Transparency, edge width, edge color, and face color for the frame around the inset colorbar. Default is @@ -933,12 +948,11 @@ def colorbar(self, *args, loc=None, pad=None, """ # TODO: add option to pad inset away from axes edge! kwargs.update({'edgecolor': edgecolor, 'linewidth': linewidth}) - loc = _notNone(loc, rc['colorbar.loc']) - loc = self._loc_translate(loc) - if loc == 'best': # a white lie - loc = 'lower right' + loc = self._loc_translate(loc, rc['colorbar.loc']) if not isinstance(loc, str): # e.g. 2-tuple or ndarray raise ValueError(f'Invalid colorbar location {loc!r}.') + if loc == 'best': # white lie + loc = 'lower right' # Generate panel if loc in ('left', 'right', 'top', 'bottom'): @@ -1014,12 +1028,16 @@ def colorbar(self, *args, loc=None, pad=None, cbwidth, cblength = width, length width, height = self.get_size_inches() extend = units(_notNone( - kwargs.get('extendsize', None), rc['colorbar.insetextend'])) + kwargs.get('extendsize', None), + rc['colorbar.insetextend'] + )) cbwidth = units(_notNone( - cbwidth, rc['colorbar.insetwidth'])) / height + cbwidth, rc['colorbar.insetwidth'] + )) / height cblength = units(_notNone( - cblength, rc['colorbar.insetlength'])) / width - pad = units(_notNone(pad, rc['colorbar.axespad'])) + cblength, rc['colorbar.insetlength'] + )) / width + pad = units(_notNone(pad, rc['colorbar.insetpad'])) xpad, ypad = pad / width, pad / height # Get location in axes-relative coordinates @@ -1056,18 +1074,19 @@ def colorbar(self, *args, loc=None, pad=None, frame, frameon, rc['colorbar.frameon'], names=('frame', 'frameon')) if frameon: - # Make patch object xmin, ymin, width, height = fbounds patch = mpatches.Rectangle( (xmin, ymin), width, height, snap=True, zorder=4, transform=self.transAxes) - # Update patch props alpha = _notNone(alpha, rc['colorbar.framealpha']) linewidth = _notNone(linewidth, rc['axes.linewidth']) edgecolor = _notNone(edgecolor, rc['axes.edgecolor']) facecolor = _notNone(facecolor, rc['axes.facecolor']) - patch.update({'alpha': alpha, 'linewidth': linewidth, - 'edgecolor': edgecolor, 'facecolor': facecolor}) + patch.update({ + 'alpha': alpha, + 'linewidth': linewidth, + 'edgecolor': edgecolor, + 'facecolor': facecolor}) self.add_artist(patch) # Make axes @@ -1098,7 +1117,7 @@ def colorbar(self, *args, loc=None, pad=None, def legend(self, *args, loc=None, width=None, space=None, **kwargs): """ - Adds an *inset* legend or *outer* legend along the edge of the axes. + Add an *inset* legend or *outer* legend along the edge of the axes. See `~proplot.wrappers.legend_wrapper` for details. Parameters @@ -1128,21 +1147,21 @@ def legend(self, *args, loc=None, width=None, space=None, **kwargs): ================== ======================================= width : float or str, optional - The space allocated for outer legends. This does nothing - if :rcraw:`tight` is ``True``. Units are interpreted by + For outer legends only. The space allocated for the legend box. + Ignored if :rcraw:`tight` is ``True``. Units are interpreted by `~proplot.utils.units`. space : float or str, optional - The space between the axes and the legend for outer legends. - Units are interpreted by `~proplot.utils.units`. + For outer legends only. The space between the axes and the legend + box. Units are interpreted by `~proplot.utils.units`. When :rcraw:`tight` is ``True``, this is adjusted automatically. - Otherwise, defaut is :rc:`subplots.panelspace`. + When :rcraw:`tight` is ``False``, this is adjusted automatically. Other parameters ---------------- *args, **kwargs Passed to `~proplot.wrappers.legend_wrapper`. """ - loc = self._loc_translate(loc, width=width, space=space) + loc = self._loc_translate(loc, rc['legend.loc']) if isinstance(loc, np.ndarray): loc = loc.tolist() From 424bc67c3d22f60c544be7ef62e07218307030c8 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 17:31:53 -0700 Subject: [PATCH 08/33] Remove Axes.context, update rc lookups, related cleanup --- proplot/axes.py | 356 ++++++++++++++++++++++++++++-------------------- 1 file changed, 209 insertions(+), 147 deletions(-) diff --git a/proplot/axes.py b/proplot/axes.py index 34b900e50..c9c421b3b 100644 --- a/proplot/axes.py +++ b/proplot/axes.py @@ -102,6 +102,20 @@ def _wrapper(self, *args, **kwargs): return decorator +def _parse_format(mode=2, rc_kw=None, **kwargs): + """Separate `~proplot.rctools.rc` setting name value pairs from + `~Axes.format` keyword arguments.""" + kw = {} + rc_kw = rc_kw or {} + for key, value in kwargs.items(): + key_fixed = RC_NODOTSNAMES.get(key, None) + if key_fixed is None: + kw[key] = value + else: + rc_kw[key_fixed] = value + return rc_kw, mode, kw + + class Axes(maxes.Axes): """Lowest-level axes subclass. Handles titles and axis sharing. Adds several new methods and overrides existing ones.""" @@ -144,11 +158,10 @@ def __init__(self, *args, number=None, self._number = number # for abc numbering self._abc_loc = None self._abc_text = None - self._titles_dict = {} # title text objects and their locations + self._titles_dict = {} # dictionary of titles and locs self._title_loc = None # location of main title - self._title_pad = rc.get('axes.titlepad') + self._title_pad = rc['axes.titlepad'] # format() can overwrite self._title_above_panel = True # TODO: add rc prop? - # Children and related properties self._bpanels = [] self._tpanels = [] self._lpanels = [] @@ -657,9 +670,9 @@ def format( llabels=None, rlabels=None, tlabels=None, blabels=None, **kwargs): """ - Called by `XYAxes.format`, `ProjAxes.format`, and - `PolarAxes.format`. Formats the axes title(s), the a-b-c label, row - and column labels, and the figure title. + Modify the axes title(s), the a-b-c label, row and column labels, and + the figure title. Called by `CartesianAxes.format`, + `ProjectionAxes.format`, and `PolarAxes.format`. Parameters ---------- @@ -741,11 +754,11 @@ def format( """ # Figure patch (for some reason needs to be re-asserted even if # declared before figure is drawn) - kw = rc.fill({'facecolor': 'figure.facecolor'}) + kw = rc.fill({'facecolor': 'figure.facecolor'}, context=True) self.figure.patch.update(kw) if top is not None: self._title_above_panel = top - pad = rc['axes.titlepad'] + pad = rc.get('axes.titlepad', context=True) if pad is not None: self._set_title_offset_trans(pad) self._title_pad = pad @@ -758,9 +771,10 @@ def format( # NOTE: Below workaround prevents changed *figure-wide* settings # from getting overwritten when user makes a new axes. fig = self.figure - suptitle = _notNone(figtitle, suptitle, None, - names=('figtitle', 'suptitle')) - if len(fig._axes_main) > 1 and rc._getitem_mode == 1: + suptitle = _notNone( + figtitle, suptitle, None, names=('figtitle', 'suptitle') + ) + if len(fig._axes_main) > 1 and rc._context and rc._context[-1][0] == 1: kw = {} else: kw = rc.fill({ @@ -768,18 +782,26 @@ def format( 'weight': 'suptitle.weight', 'color': 'suptitle.color', 'fontfamily': 'font.family' - }) + }, context=True) if suptitle or kw: fig._update_figtitle(suptitle, **kw) # Labels - llabels = _notNone(rowlabels, leftlabels, llabels, - None, names=('rowlabels', 'leftlabels', 'llabels')) - tlabels = _notNone(collabels, toplabels, tlabels, - None, names=('collabels', 'toplabels', 'tlabels')) - rlabels = _notNone(rightlabels, rlabels, None, - names=('rightlabels', 'rlabels')) - blabels = _notNone(bottomlabels, blabels, None, - names=('bottomlabels', 'blabels')) + llabels = _notNone( + rowlabels, leftlabels, llabels, None, + names=('rowlabels', 'leftlabels', 'llabels') + ) + tlabels = _notNone( + collabels, toplabels, tlabels, None, + names=('collabels', 'toplabels', 'tlabels') + ) + rlabels = _notNone( + rightlabels, rlabels, None, + names=('rightlabels', 'rlabels') + ) + blabels = _notNone( + bottomlabels, blabels, None, + names=('bottomlabels', 'blabels') + ) for side, labels in zip( ('left', 'right', 'top', 'bottom'), (llabels, rlabels, tlabels, blabels)): @@ -788,7 +810,7 @@ def format( 'weight': side + 'label.weight', 'color': side + 'label.color', 'fontfamily': 'font.family' - }) + }, context=True) if labels or kw: fig._update_labels(self, side, labels, **kw) @@ -796,7 +818,7 @@ def format( titles_dict = self._titles_dict if not self._panel_side: # Location and text - abcstyle = rc['abc.style'] # changed or running format first time? + abcstyle = rc.get('abc.style', context=True) # 1st run, or changed if 'abcformat' in kwargs: # super sophisticated deprecation system abcstyle = kwargs.pop('abcformat') _warn_proplot( @@ -828,7 +850,7 @@ def format( # Toggle visibility # NOTE: If abc is a matplotlib 'title' attribute, making it # invisible messes stuff up. Just set text to empty. - abc = rc['abc'] + abc = rc.get('abc', context=True) if abc is not None: obj.set_text(self._abc_text if bool(abc) else '') @@ -1028,8 +1050,7 @@ def colorbar(self, *args, loc=None, pad=None, cbwidth, cblength = width, length width, height = self.get_size_inches() extend = units(_notNone( - kwargs.get('extendsize', None), - rc['colorbar.insetextend'] + kwargs.get('extendsize', None), rc['colorbar.insetextend'] )) cbwidth = units(_notNone( cbwidth, rc['colorbar.insetwidth'] @@ -1148,13 +1169,14 @@ def legend(self, *args, loc=None, width=None, space=None, **kwargs): width : float or str, optional For outer legends only. The space allocated for the legend box. - Ignored if :rcraw:`tight` is ``True``. Units are interpreted by - `~proplot.utils.units`. + This does nothing if :rcraw:`tight` is ``True``. Units are + interpreted by `~proplot.utils.units`. space : float or str, optional For outer legends only. The space between the axes and the legend - box. Units are interpreted by `~proplot.utils.units`. + box. Units are interpreted by `~proplot.utils.units`. When :rcraw:`tight` is ``True``, this is adjusted automatically. - When :rcraw:`tight` is ``False``, this is adjusted automatically. + When :rcraw:`tight` is ``False``, the default is + :rc:`subplots.panelspace`. Other parameters ---------------- @@ -1706,14 +1728,12 @@ def _parse_dualxy_args(x, kwargs): return kwargs -def _rcloc_to_stringloc(x, string): # figures out string location - """Gets *location string* from the *boolean* "left", "right", "top", and - "bottom" rc settings, e.g. :rc:`axes.spines.left` or :rc:`ytick.left`. - Might be ``None`` if settings are unchanged.""" - # For x axes +def _parse_rcloc(x, string): # figures out string location + """Convert the *boolean* "left", "right", "top", and "bottom" rc settings + to a location string. Returns ``None`` if settings are unchanged.""" if x == 'x': - top = rc[f'{string}.top'] - bottom = rc[f'{string}.bottom'] + top = rc.get(f'{string}.top', context=True) + bottom = rc.get(f'{string}.bottom', context=True) if top is None and bottom is None: return None elif top and bottom: @@ -1724,10 +1744,9 @@ def _rcloc_to_stringloc(x, string): # figures out string location return 'bottom' else: return 'neither' - # For y axes else: - left = rc[f'{string}.left'] - right = rc[f'{string}.right'] + left = rc.get(f'{string}.left', context=True) + right = rc.get(f'{string}.right', context=True) if left is None and right is None: return None elif left and right: @@ -1953,9 +1972,9 @@ def format( patch_kw=None, **kwargs): """ - Calls `Axes.format` and `Axes.context`, formats the - *x* and *y* axis labels, tick locations, tick labels, - axis scales, spine settings, and more. + Modify the *x* and *y* axis labels, tick locations, tick labels, + axis scales, spine settings, and more. Unknown keyword arguments + are passed to `Axes.format` and `Axes.context`. Parameters ---------- @@ -2073,8 +2092,14 @@ def format( Keyword arguments used to update the background patch object. You can use this, for example, to set background hatching with ``patch_kw={'hatch':'xxx'}``. + rc_kw : dict, optional + Dictionary containing `~proplot.rctools.rc` settings applied to + this axes using `~proplot.rctools.rc_configurator.context`. **kwargs - Passed to `Axes.format` and `Axes.context`. + Passed to `Axes.format` or passed to + `~proplot.rctools.rc_configurator.context` and used to update + axes `~proplot.rctools.rc` settings. For example, + ``axestitlesize=15`` modifies the :rcraw:`axes.titlesize` setting. Note ---- @@ -2089,15 +2114,15 @@ def format( -------- :py:obj:`Axes.format`, :py:obj:`Axes.context` """ - context, kwargs = self.context(**kwargs) - with context: + rc_kw, rc_mode, kwargs = _parse_format(**kwargs) + with rc.context(rc_kw, mode=rc_mode): # Background basics self.patch.set_clip_on(False) self.patch.set_zorder(-1) kw_face = rc.fill({ 'facecolor': 'axes.facecolor', - 'alpha': 'axes.alpha' - }) + 'alpha': 'axes.facealpha' + }, context=True) patch_kw = patch_kw or {} kw_face.update(patch_kw) self.patch.update(kw_face) @@ -2115,12 +2140,20 @@ def format( yminorlocator_kw = yminorlocator_kw or {} # Flexible keyword args, declare defaults - xmargin = _notNone(xmargin, rc['axes.xmargin']) - ymargin = _notNone(ymargin, rc['axes.ymargin']) - xtickdir = _notNone(xtickdir, rc['xtick.direction']) - ytickdir = _notNone(ytickdir, rc['ytick.direction']) - xtickminor = _notNone(xtickminor, rc['xtick.minor.visible']) - ytickminor = _notNone(ytickminor, rc['ytick.minor.visible']) + xmargin = _notNone(xmargin, rc.get('axes.xmargin', context=True)) + ymargin = _notNone(ymargin, rc.get('axes.ymargin', context=True)) + xtickdir = _notNone( + xtickdir, rc.get('xtick.direction', context=True) + ) + ytickdir = _notNone( + ytickdir, rc.get('ytick.direction', context=True) + ) + xtickminor = _notNone( + xtickminor, rc.get('xtick.minor.visible', context=True) + ) + ytickminor = _notNone( + ytickminor, rc.get('ytick.minor.visible', context=True) + ) xformatter = _notNone( xticklabels, xformatter, None, names=('xticklabels', 'xformatter') @@ -2147,13 +2180,14 @@ def format( ) # Grid defaults are more complicated - axis = rc.get('axes.grid.axis') # always need this property - grid, which = rc['axes.grid'], rc['axes.grid.which'] + grid = rc.get('axes.grid', context=True) + which = rc.get('axes.grid.which', context=True) if which is not None or grid is not None: # if *one* was changed + axis = rc['axes.grid.axis'] # always need this property if grid is None: - grid = rc.get('axes.grid') + grid = rc['axes.grid'] elif which is None: - which = rc.get('axes.grid.which') + which = rc['axes.grid.which'] xgrid = _notNone( xgrid, grid and axis in ('x', 'both') and which in ('major', 'both') @@ -2181,16 +2215,16 @@ def format( yloc, yspineloc, None, names=('yloc', 'yspineloc') ) xtickloc = _notNone( - xtickloc, xspineloc, _rcloc_to_stringloc('x', 'xtick') + xtickloc, xspineloc, _parse_rcloc('x', 'xtick') ) ytickloc = _notNone( - ytickloc, yspineloc, _rcloc_to_stringloc('y', 'ytick') + ytickloc, yspineloc, _parse_rcloc('y', 'ytick') ) xspineloc = _notNone( - xspineloc, _rcloc_to_stringloc('x', 'axes.spines') + xspineloc, _parse_rcloc('x', 'axes.spines') ) yspineloc = _notNone( - yspineloc, _rcloc_to_stringloc('y', 'axes.spines') + yspineloc, _parse_rcloc('y', 'axes.spines') ) if xtickloc != 'both': xticklabelloc = _notNone(xticklabelloc, xtickloc) @@ -2267,7 +2301,7 @@ def format( kw = rc.fill({ 'linewidth': 'axes.linewidth', 'color': 'axes.edgecolor', - }) + }, context=True) if color is not None: kw['color'] = color sides = ('bottom', 'top') if x == 'x' else ('left', 'right') @@ -2312,37 +2346,39 @@ def format( spines = [side for side, spine in zip( sides, spines) if spine.get_visible()] - # Tick and grid settings for major and minor ticks separately - # Override is just a "new default", but user can override this - def grid_dict(grid): + # Helper func + def _grid_dict(grid): return { 'grid_color': grid + '.color', 'grid_alpha': grid + '.alpha', 'grid_linewidth': grid + '.linewidth', 'grid_linestyle': grid + '.linestyle', } + + # Tick and grid settings for major and minor ticks separately + # Override is just a "new default", but user can override this for which, igrid in zip(('major', 'minor'), (grid, gridminor)): # Tick properties - kw_ticks = rc.category(x + 'tick.' + which) + kw_ticks = rc.category(x + 'tick.' + which, context=True) if kw_ticks is None: kw_ticks = {} else: kw_ticks.pop('visible', None) # invalid setting if ticklen is not None: - if which == 'major': - kw_ticks['size'] = units(ticklen, 'pt') - else: - kw_ticks['size'] = units( - ticklen, 'pt') * rc.get('ticklenratio') + kw_ticks['size'] = units(ticklen, 'pt') + if which == 'minor': + kw_ticks['size'] *= rc['ticklenratio'] # Grid style and toggling if igrid is not None: # toggle with special global props axis.grid(igrid, which=which) if which == 'major': - kw_grid = rc.fill(grid_dict('grid')) + kw_grid = rc.fill(_grid_dict('grid'), context=True) else: kw_major = kw_grid - kw_grid = rc.fill(grid_dict('gridminor')) + kw_grid = rc.fill( + _grid_dict('gridminor'), context=True + ) kw_grid.update({ key: value for key, value in kw_major.items() if key not in kw_grid @@ -2407,7 +2443,7 @@ def grid_dict(grid): 'labelcolor': 'tick.labelcolor', # new props 'labelsize': 'tick.labelsize', 'color': x + 'tick.color', - }) + }, context=True) if color: kw['color'] = color kw['labelcolor'] = color @@ -2416,10 +2452,10 @@ def grid_dict(grid): kw['pad'] = 1 # ticklabels should be much closer if ticklabeldir == 'in': # put tick labels inside the plot tickdir = 'in' - pad = (rc.get(x + 'tick.major.size') - + rc.get(x + 'tick.major.pad') - + rc.get(x + 'tick.labelsize')) - kw['pad'] = -pad + kw['pad'] = -1 * sum( + rc[f'{x}tick.{key}'] + for key in ('major.size', 'major.pad', 'labelsize') + ) if tickdir is not None: kw['direction'] = tickdir axis.set_tick_params(which='both', **kw) @@ -2429,7 +2465,7 @@ def grid_dict(grid): kw = rc.fill({ 'fontfamily': 'font.family', 'weight': 'tick.labelweight' - }) + }, context=True) if rotation is not None: kw = {'rotation': rotation} if x == 'x': @@ -2450,7 +2486,7 @@ def grid_dict(grid): 'weight': 'axes.labelweight', 'fontsize': 'axes.labelsize', 'fontfamily': 'font.family', - }) + }, context=True) if label is not None: kw['text'] = label if color: @@ -2691,10 +2727,10 @@ def format(self, *args, thetaformatter_kw=None, rformatter_kw=None, **kwargs): """ - Calls `Axes.format` and `Axes.context`, formats radial gridline - locations, gridline labels, limits, and more. All ``theta`` arguments - are specified in *degrees*, not radians. The below parameters are - specific to `PolarAxes`. + Modify radial gridline locations, gridline labels, limits, and more. + Unknown keyword arguments are passed to `Axes.format` and + `Axes.context`. All ``theta`` arguments are specified in *degrees*, not + radians. The below parameters are specific to `PolarAxes`. Parameters ---------- @@ -2739,15 +2775,21 @@ def format(self, *args, thetaformatter_kw, rformatter_kw : dict-like, optional The azimuthal and radial label formatter settings. Passed to `~proplot.axistools.Formatter`. + rc_kw : dict, optional + Dictionary containing `~proplot.rctools.rc` settings applied to + this axes using `~proplot.rctools.rc_configurator.context`. **kwargs - Passed to `Axes.format` and `Axes.context` + Passed to `Axes.format` or passed to + `~proplot.rctools.rc_configurator.context` and used to update the + axes `~proplot.rctools.rc` settings. For example, + ``axestitlesize=15`` modifies the :rcraw:`axes.titlesize` setting. See also -------- :py:obj:`Axes.format`, :py:obj:`Axes.context` """ - context, kwargs = self.context(**kwargs) - with context: + rc_kw, rc_mode, kwargs = _parse_format(**kwargs) + with rc.context(rc_kw, mode=rc_mode): # Not mutable default args thetalocator_kw = thetalocator_kw or {} thetaformatter_kw = thetaformatter_kw or {} @@ -2821,7 +2863,7 @@ def format(self, *args, kw = rc.fill({ 'linewidth': 'axes.linewidth', 'color': 'axes.edgecolor', - }) + }, context=True) sides = ('inner', 'polar') if r == 'r' else ('start', 'end') spines = [self.spines[s] for s in sides] for spine, side in zip(spines, sides): @@ -2837,13 +2879,13 @@ def format(self, *args, 'grid_alpha': 'grid.alpha', 'grid_linewidth': 'grid.linewidth', 'grid_linestyle': 'grid.linestyle', - }) + }, context=True) axis.set_tick_params(which='both', **kw) # Label settings that can't be controlled with set_tick_params kw = rc.fill({ 'fontfamily': 'font.family', 'weight': 'tick.labelweight' - }) + }, context=True) for t in axis.get_ticklabels(): t.update(kw) @@ -2948,9 +2990,9 @@ def format(self, *, patch_kw=None, **kwargs, ): """ - Calls `Axes.format` and `Axes.context`, formats the meridian - and parallel labels, longitude and latitude map limits, geographic - features, and more. + Modify the meridian and parallel labels, longitude and latitude map + limits, geographic features, and more. Unknown keyword arguments are + passed to `Axes.format` and `Axes.context`. Parameters ---------- @@ -3001,26 +3043,34 @@ def format(self, *, Keyword arguments used to update the background patch object. You can use this, for example, to set background hatching with ``patch_kw={'hatch':'xxx'}``. + rc_kw : dict, optional + Dictionary containing `~proplot.rctools.rc` settings applied to + this axes using `~proplot.rctools.rc_configurator.context`. **kwargs - Passed to `Axes.format` and `Axes.context`. + Passed to `Axes.format` or passed to + `~proplot.rctools.rc_configurator.context` and used to update + axes `~proplot.rctools.rc` settings. For example, + ``axestitlesize=15`` modifies the :rcraw:`axes.titlesize` setting. See also -------- :py:obj:`Axes.format`, :py:obj:`Axes.context` """ - # Parse alternative keyword args - # TODO: Why isn't default latmax 80 respected sometimes? - context, kwargs = self.context(**kwargs) - with context: + rc_kw, rc_mode, kwargs = _parse_format(**kwargs) + with rc.context(rc_kw, mode=rc_mode): + # Parse alternative keyword args + # TODO: Why isn't default latmax 80 respected sometimes? lonlines = _notNone( - lonlines, lonlocator, rc['geogrid.lonstep'], - names=('lonlines', 'lonlocator')) + lonlines, lonlocator, rc.get('geogrid.lonstep', context=True), + names=('lonlines', 'lonlocator') + ) latlines = _notNone( - latlines, latlocator, rc['geogrid.latstep'], - names=('latlines', 'latlocator')) - latmax = _notNone(latmax, rc['geogrid.latmax']) - labels = _notNone(labels, rc['geogrid.labels']) - grid = _notNone(grid, rc['geogrid']) + latlines, latlocator, rc.get('geogrid.latstep', context=True), + names=('latlines', 'latlocator') + ) + latmax = _notNone(latmax, rc.get('geogrid.latmax', context=True)) + labels = _notNone(labels, rc.get('geogrid.labels', context=True)) + grid = _notNone(grid, rc.get('geogrid', context=True)) if labels: lonlabels = _notNone(lonlabels, 1) latlabels = _notNone(latlabels, 1) @@ -3051,9 +3101,9 @@ def format(self, *, # Fill defaults if latlines is None: latlines = _notNone( - self._latlines_values, rc.get('geogrid.latstep')) - ilatmax = _notNone(latmax, self._latmax, - rc.get('geogrid.latmax')) + self._latlines_values, rc['geogrid.latstep'] + ) + ilatmax = _notNone(latmax, self._latmax, rc['geogrid.latmax']) # Get tick locations if not np.iterable(latlines): if (ilatmax % latlines) == (-ilatmax % latlines): @@ -3328,7 +3378,7 @@ def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, 'color': 'geogrid.color', 'linewidth': 'geogrid.linewidth', 'linestyle': 'geogrid.linestyle', - }) # cached changes + }, context=True) gl.collection_kwargs.update(kw) # Grid locations eps = 1e-10 @@ -3381,9 +3431,9 @@ def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, # NOTE: The e.g. cfeature.COASTLINE features are just for convenience, # hi res versions. Use cfeature.COASTLINE.name to see how it can be # looked up with NaturalEarthFeature. - reso = rc.get('reso') + reso = rc['reso'] if reso not in ('lo', 'med', 'hi'): - raise ValueError(f'Invalid resolution {reso}.') + raise ValueError(f'Invalid resolution {reso!r}.') reso = { 'lo': '110m', 'med': '50m', @@ -3400,14 +3450,14 @@ def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, } for name, args in features.items(): # Get feature - if not rc.get(name): # toggled + if not rc[name]: # toggled continue if getattr(self, '_' + name, None): # already drawn continue feat = cfeature.NaturalEarthFeature(*args, reso) # For 'lines', need to specify edgecolor and facecolor # See: https://github.com/SciTools/cartopy/issues/803 - kw = rc.category(name, cache=False) + kw = rc.category(name) # do not omit uncached props if name in ('coast', 'rivers', 'borders', 'innerborders'): kw['edgecolor'] = kw.pop('color') kw['facecolor'] = 'none' @@ -3420,19 +3470,20 @@ def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, # Update patch kw_face = rc.fill({ - 'facecolor': 'geoaxes.facecolor' - }) - kw_face.update(patch_kw) - self.background_patch.update(kw_face) + 'facecolor': 'geoaxes.facecolor', + 'alpha': 'geoaxes.facealpha', + }, context=True) kw_edge = rc.fill({ 'edgecolor': 'geoaxes.edgecolor', - 'linewidth': 'geoaxes.linewidth' - }) + 'linewidth': 'geoaxes.linewidth', + }, context=True) + kw_face.update(patch_kw or {}) + self.background_patch.update(kw_face) self.outline_patch.update(kw_edge) def _hide_labels(self): - """No-op for now. In future will hide meridian and parallel labels - for rectangular projections.""" + """No-op for now. In future this will hide meridian and parallel + labels for rectangular projections.""" pass def get_tightbbox(self, renderer, *args, **kwargs): @@ -3455,7 +3506,7 @@ def get_tightbbox(self, renderer, *args, **kwargs): self._gridliners = [] return super().get_tightbbox(renderer, *args, **kwargs) - # Document projection property + # Projection property @property def projection(self): """The `~cartopy.crs.Projection` instance associated with this axes.""" @@ -3463,16 +3514,17 @@ def projection(self): @projection.setter def projection(self, map_projection): + import cartopy.crs as ccrs + if not isinstance(map_projection, ccrs.CRS): + raise ValueError(f'Projection must be a cartopy.crs.CRS instance.') self._map_projection = map_projection # Wrapped methods - # TODO: Remove this duplication of Axes! Can do this when we implement - # all wrappers as decorators. + # TODO: Remove this duplication! if GeoAxes is not object: text = _text_wrapper( GeoAxes.text ) - # Wrapped by standardize method plot = _default_transform(_plot_wrapper(_standardize_1d( _add_errorbars(_cycle_changer(GeoAxes.plot)) ))) @@ -3517,18 +3569,24 @@ def projection(self, map_projection): tricontourf = _default_transform(_cmap_changer( GeoAxes.tricontourf )) - - # Special GeoAxes commands - get_extent = _default_crs(GeoAxes.get_extent) - set_extent = _default_crs(GeoAxes.set_extent) - set_xticks = _default_crs(GeoAxes.set_xticks) - set_yticks = _default_crs(GeoAxes.set_yticks) + get_extent = _default_crs( + GeoAxes.get_extent + ) + set_extent = _default_crs( + GeoAxes.set_extent + ) + set_xticks = _default_crs( + GeoAxes.set_xticks + ) + set_yticks = _default_crs( + GeoAxes.set_yticks + ) class BasemapAxes(ProjAxes): """Axes subclass for plotting `~mpl_toolkits.basemap` projections. The `~mpl_toolkits.basemap.Basemap` projection instance is added as - the `map_projection` attribute, but this is all abstracted away. You can + the `map_projection` attribute, but this is all abstracted away -- you can use `~matplotlib.axes.Axes` methods like `~matplotlib.axes.Axes.plot` and `~matplotlib.axes.Axes.contour` with your raw longitude-latitude data.""" #: The registered projection name. @@ -3578,7 +3636,7 @@ def __init__(self, *args, map_projection=None, **kwargs): def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, lonlines, latlines, latmax, lonarray, latarray): - """Applies formatting to basemap axes.""" + """Apply changes to the basemap axes.""" # Checks if (lonlim is not None or latlim is not None or boundinglat is not None): @@ -3599,15 +3657,15 @@ def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, # edges/fill color disappear # * For now will enforce that map plots *always* have background # whereas axes plots can have transparent background + kw_face = rc.fill({ + 'facecolor': 'geoaxes.facecolor', + 'alpha': 'geoaxes.facealpha', + }, context=True) kw_edge = rc.fill({ 'linewidth': 'geoaxes.linewidth', - 'edgecolor': 'geoaxes.edgecolor' - }) - kw_face = rc.fill({ - 'facecolor': 'geoaxes.facecolor' - }) - patch_kw = patch_kw or {} - kw_face.update(patch_kw) + 'edgecolor': 'geoaxes.edgecolor', + }, context=True) + kw_face.update(patch_kw or {}) self.axesPatch = self.patch # bugfix or something if self.projection.projection in self._proj_non_rectangular: self.patch.set_alpha(0) # make patch invisible @@ -3616,12 +3674,13 @@ def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, p = self.projection.drawmapboundary(ax=self) else: p = self.projection._mapboundarydrawn - p.update({**kw_face, **kw_edge}) + p.update(kw_face) + p.update(kw_edge) p.set_rasterized(False) p.set_clip_on(False) # so edges denoting boundary aren't cut off self._map_boundary = p else: - self.patch.update({**kw_face, 'edgecolor': 'none'}) + self.patch.update(edgecolor='none', **kw_face) for spine in self.spines.values(): spine.update(kw_edge) @@ -3634,11 +3693,11 @@ def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, 'color': 'geogrid.color', 'linewidth': 'geogrid.linewidth', 'linestyle': 'geogrid.linestyle', - }, cache=False) + }) # always apply tkw = rc.fill({ 'color': 'geogrid.color', 'fontsize': 'geogrid.labelsize', - }, cache=False) + }) # Change from left/right/bottom/top to left/right/top/bottom if lonarray is not None: lonarray[2:] = lonarray[2:][::-1] @@ -3697,11 +3756,11 @@ def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, 'innerborders': 'drawstates', } for name, method in features.items(): - if not rc.get(name): # toggled + if not rc[name]: # toggled continue if getattr(self, f'_{name}', None): # already drawn continue - kw = rc.category(name, cache=False) + kw = rc.category(name) feat = getattr(self.projection, method)(ax=self) if isinstance(feat, (list, tuple)): # list of artists? for obj in feat: @@ -3710,7 +3769,7 @@ def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, feat.update(kw) setattr(self, '_' + name, feat) - # Document projection property + # Projection property @property def projection(self): """The `~mpl_toolkits.basemap.Basemap` instance associated with @@ -3719,6 +3778,9 @@ def projection(self): @projection.setter def projection(self, map_projection): + import mpl_toolkits.basemap as mbasemap + if not isinstance(map_projection, mbasemap.Basemap): + raise ValueError(f'Projection must be a basemap.Basemap instance.') self._map_projection = map_projection # Wrapped methods From 8e7655e6b05f795b257d0f38ed7468c51b73c051 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 17:32:57 -0700 Subject: [PATCH 09/33] Add x/ylinewidth, x/ygridcolor XYAxes.format kwargs --- proplot/axes.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/proplot/axes.py b/proplot/axes.py index c9c421b3b..21d62928a 100644 --- a/proplot/axes.py +++ b/proplot/axes.py @@ -1963,6 +1963,8 @@ def format( xbounds=None, ybounds=None, xmargin=None, ymargin=None, xcolor=None, ycolor=None, + xlinewidth=None, ylinewidth=None, + xgridcolor=None, ygridcolor=None, xticklen=None, yticklen=None, xlabel_kw=None, ylabel_kw=None, xscale_kw=None, yscale_kw=None, @@ -2075,6 +2077,14 @@ def format( Color for the *x* and *y* axis spines, ticks, tick labels, and axis labels. Default is :rc:`color`. Use e.g. ``ax.format(color='red')`` to set for both axes. + xlinewidth, ylinewidth : color-spec, optional + Line width for the *x* and *y* axis spines and major ticks. + Default is :rc:`linewidth`. Use e.g. ``ax.format(linewidth=2)`` + to set for both axes. + xgridcolor, ygridcolor : color-spec, optional + Color for the *x* and *y* axis major and minor gridlines. + Default is :rc:`grid.color`. Use e.g. ``ax.format(gridcolor='r')`` + to set for both axes. xticklen, yticklen : float or str, optional Tick lengths for the *x* and *y* axis. Units are interpreted by `~proplot.utils.units`, with "points" as the numeric unit. Default @@ -2240,7 +2250,9 @@ def format( # Begin loop for ( x, axis, - label, color, ticklen, + label, color, + linewidth, gridcolor, + ticklen, margin, bounds, tickloc, spineloc, ticklabelloc, labelloc, @@ -2255,7 +2267,9 @@ def format( formatter_kw ) in zip( ('x', 'y'), (self.xaxis, self.yaxis), - (xlabel, ylabel), (xcolor, ycolor), (xticklen, yticklen), + (xlabel, ylabel), (xcolor, ycolor), + (xlinewidth, ylinewidth), (xgridcolor, ygridcolor), + (xticklen, yticklen), (xmargin, ymargin), (xbounds, ybounds), (xtickloc, ytickloc), (xspineloc, yspineloc), (xticklabelloc, yticklabelloc), (xlabelloc, ylabelloc), @@ -2304,6 +2318,8 @@ def format( }, context=True) if color is not None: kw['color'] = color + if linewidth is not None: + kw['linewidth'] = linewidth sides = ('bottom', 'top') if x == 'x' else ('left', 'right') spines = [self.spines[s] for s in sides] for spine, side in zip(spines, sides): @@ -2384,6 +2400,8 @@ def _grid_dict(grid): if key not in kw_grid }) # Changed rc settings + if gridcolor is not None: + kw['grid_color'] = gridcolor axis.set_tick_params(which=which, **kw_grid, **kw_ticks) # Tick and ticklabel properties that apply to major and minor From 25f7bd88f41f55e9b215932e2196e4dbe55aa821 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 17:34:59 -0700 Subject: [PATCH 10/33] Fix rc lookups in other files --- proplot/axistools.py | 4 ++-- proplot/wrappers.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/proplot/axistools.py b/proplot/axistools.py index 840e89216..c1cf2e67c 100644 --- a/proplot/axistools.py +++ b/proplot/axistools.py @@ -412,7 +412,7 @@ def __init__(self, *args, """ tickrange = tickrange or (-np.inf, np.inf) super().__init__(*args, **kwargs) - zerotrim = _notNone(zerotrim, rc.get('axes.formatter.zerotrim')) + zerotrim = _notNone(zerotrim, rc['axes.formatter.zerotrim']) self._zerotrim = zerotrim self._tickrange = tickrange self._prefix = prefix or '' @@ -621,7 +621,7 @@ def set_default_locators_and_formatters(self, axis, only_if_default=False): name = axis.axis_name if axis.axis_name in 'xy' else 'x' axis.set_minor_locator( self._default_minor_locator or Locator( - 'minor' if rc.get(name + 'tick.minor.visible') else 'null' + 'minor' if rc[name + 'tick.minor.visible'] else 'null' ) ) axis.isDefault_minloc = True diff --git a/proplot/wrappers.py b/proplot/wrappers.py index 09d4c4201..9a7c454a8 100644 --- a/proplot/wrappers.py +++ b/proplot/wrappers.py @@ -1361,7 +1361,7 @@ def text_wrapper( if size is not None: kwargs['fontsize'] = units(size, 'pt') # text.color is ignored sometimes unless we apply this - kwargs.setdefault('color', rc.get('text.color')) + kwargs.setdefault('color', rc['text.color']) obj = func(self, x, y, text, transform=transform, **kwargs) # Optionally draw border around text @@ -2779,11 +2779,11 @@ def colorbar_wrapper( if orientation == 'horizontal': scale = 3 # em squares alotted for labels length = width * abs(self.get_position().width) - fontsize = kw_ticklabels.get('size', rc.get('xtick.labelsize')) + fontsize = kw_ticklabels.get('size', rc['xtick.labelsize']) else: scale = 1 length = height * abs(self.get_position().height) - fontsize = kw_ticklabels.get('size', rc.get('ytick.labelsize')) + fontsize = kw_ticklabels.get('size', rc['ytick.labelsize']) maxn = _notNone(maxn, int(length / (scale * fontsize / 72))) maxn_minor = _notNone(maxn_minor, int( length / (0.5 * fontsize / 72))) From 77e406a784a8d378d9edc4144285863b4a7c1135 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 17:42:58 -0700 Subject: [PATCH 11/33] Remove Axes.context, add documented Axes.number prop --- proplot/axes.py | 100 +++++++++++------------------------------------- 1 file changed, 23 insertions(+), 77 deletions(-) diff --git a/proplot/axes.py b/proplot/axes.py index 21d62928a..55c14abcf 100644 --- a/proplot/axes.py +++ b/proplot/axes.py @@ -155,7 +155,7 @@ def __init__(self, *args, number=None, # Ensure isDefault_minloc enabled at start, needed for dual axes self.xaxis.isDefault_minloc = self.yaxis.isDefault_minloc = True # Properties - self._number = number # for abc numbering + self.number = number self._abc_loc = None self._abc_text = None self._titles_dict = {} # dictionary of titles and locs @@ -326,6 +326,18 @@ def _get_title_props(self, abc=False, loc=None): kw.pop('linewidth', None) return loc, obj, kw + def _iter_panels(self, sides='lrbt'): + """Return a list of axes and child panel axes.""" + axs = [self] if self.get_visible() else [] + if not ({*sides} <= {*'lrbt'}): + raise ValueError(f'Invalid sides {sides!r}.') + for s in sides: + for ax in getattr(self, '_' + s + 'panels'): + if not ax or not ax.get_visible(): + continue + axs.append(ax) + return axs + @staticmethod def _loc_translate(loc, default=None): """Return the location string `loc` translated into a standardized @@ -604,64 +616,6 @@ def _update_title(self, obj, **kwargs): y = _notNone(kwargs.pop('y', y), pos[1]) return self.text(x, y, text, **kwextra) - def context(self, *, mode=2, rc_kw=None, **kwargs): - """ - For internal use. Sets up temporary `~proplot.rctools.rc` settings by - returning the result of `~proplot.rctools.rc_configurator.context`. - - Parameters - ---------- - rc_kw : dict, optional - A dictionary containing "rc" configuration settings that will - be applied to this axes. Temporarily updates the - `~proplot.rctools.rc` object. - **kwargs - Any of three options: - - * A keyword arg for `Axes.format`, `XYAxes.format`, - or `ProjAxes.format`. - * A global "rc" keyword arg, like ``linewidth`` or ``color``. - * A standard "rc" keyword arg **with the dots omitted**, - like ``landcolor`` instead of ``land.color``. - - The latter two options update the `~proplot.rctools.rc` - object, just like `rc_kw`. - - Other parameters - ---------------- - mode : int, optional - The "getitem mode". This is used under-the-hood -- you shouldn't - have to use it directly. Determines whether queries to the - `~proplot.rctools.rc` object will ignore - `rcParams `__. - This can help prevent a massive number of unnecessary lookups - when the settings haven't been changed by the user. - See `~proplot.rctools.rc_configurator` for details. - - Returns - ------- - `~proplot.rctools.rc_configurator` - The `proplot.rctools.rc` object primed for use in a "with" - statement. - dict - Dictionary of keyword arguments that are not `~proplot.rctools.rc` - properties, to be passed to the ``format`` methods. - """ - # Figure out which kwargs are valid rc settings - # TODO: Support for 'small', 'large', etc. font - kw = {} # for format - rc_kw = rc_kw or {} - for key, value in kwargs.items(): - key_fixed = RC_NODOTSNAMES.get(key, None) - if key_fixed is None: - kw[key] = value - else: - rc_kw[key_fixed] = value - rc._getitem_mode = 0 # might still be non-zero if had error - # Return "context object", which is just the configurator itself - # primed for use in a "with" statement - return rc.context(rc_kw, mode=mode), kw - def format( self, *, title=None, top=None, figtitle=None, suptitle=None, rowlabels=None, collabels=None, @@ -953,8 +907,7 @@ def colorbar(self, *args, loc=None, pad=None, For outer colorbars only. The space between the colorbar and the main axes. Units are interpreted by `~proplot.utils.units`. When :rcraw:`tight` is ``True``, this is adjusted automatically. - When :rcraw:`tight` is ``False``, the default is - :rc:`subplots.panelspace`. + Otherwise, the default is :rc:`subplots.panelspace`. frame, frameon : bool, optional For inset colorbars, indicates whether to draw a "frame", just like `~matplotlib.axes.Axes.legend`. Default is @@ -1175,8 +1128,7 @@ def legend(self, *args, loc=None, width=None, space=None, **kwargs): For outer legends only. The space between the axes and the legend box. Units are interpreted by `~proplot.utils.units`. When :rcraw:`tight` is ``True``, this is adjusted automatically. - When :rcraw:`tight` is ``False``, the default is - :rc:`subplots.panelspace`. + Otherwise, the default is :rc:`subplots.panelspace`. Other parameters ---------------- @@ -1398,7 +1350,7 @@ def panel_axes(self, side, **kwargs): space : float or str or list thereof, optional Empty space between the main subplot and the panel. When :rcraw:`tight` is ``True``, this is adjusted automatically. - Otherwise, defaut is :rc:`subplots.panelspace`. + Otherwise, the default is :rc:`subplots.panelspace`. share : bool, optional Whether to enable axis sharing between the *x* and *y* axes of the main subplot and the panel long axes for each panel in the stack. @@ -1535,22 +1487,16 @@ def violins(self, *args, **kwargs): @property def number(self): - """The axes number, controls a-b-c label order and order of - appearence in the `~proplot.subplots.subplot_grid` returned by + """The axes number. This controls the order of a-b-c labels and the + order of appearence in the `~proplot.subplots.subplot_grid` returned by `~proplot.subplots.subplots`.""" return self._number - def _iter_panels(self, sides='lrbt'): - """Iterates over axes and child panel axes.""" - axs = [self] if self.get_visible() else [] - if not ({*sides} <= {*'lrbt'}): - raise ValueError(f'Invalid sides {sides!r}.') - for s in sides: - for ax in getattr(self, '_' + s + 'panels'): - if not ax or not ax.get_visible(): - continue - axs.append(ax) - return axs + @number.setter + def number(self, num): + if not isinstance(num, Integral) or num < 1: + raise ValueError(f'Invalid number {num!r}. Must be integer >=1.') + self._number = num # Wrapped by special functions # Also support redirecting to Basemap methods From 61a45d408ce5ef2b98447c6a02b70dd3741ccd9d Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 17:46:49 -0700 Subject: [PATCH 12/33] Update changelog and index --- CHANGELOG.rst | 25 ++++++++++++------------- docs/index.rst | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ac3b3180f..fbd91e6cc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -32,22 +32,12 @@ ProPlot v0.5.0 (2020-##-##) *or* `~proplot.subplots.subplots` (:pr:`50`). This is a major improvement! - `~proplot.subplots.GridSpec` now accepts physical units, rather than having `~proplot.subplots.subplots` handle the units (:pr:`50`). -- Add `xlinewidth`, `ylinewidth`, `xgridcolor`, `ygridcolor` keyword - args to `~proplot.axes.XYAxes.format` (:pr:`50`). - Allow "hanging" twin *x* and *y* axes as members of the `~proplot.subplots.EdgeStack` container. Arbitrarily many siblings are now permitted. - Use `~proplot.subplots.GeometrySolver` for calculating various automatic layout stuff instead of having 1000 hidden `~proplot.subplots.Figure` methods (:pr:`50`). - Use `~proplot.subplots.EdgeStack` class for handling stacks of colorbars, legends, and text (:pr:`50`). -- `~proplot.rctools.rc` `~proplot.rctools.rc_configurator.__getitem__` always - returns the setting; "caching" can only be used *explicitly* by passing ``cache=True`` to - `~proplot.rctools.rc_configurator.get`, `~proplot.rctools.rc_configurator.fill`, and - `~proplot.rctools.rc_configurator.category` (:pr:`50`). - -.. rubric:: Bug fixes - -- Fix `~proplot.rctools.rc_configurator.context` fatal bug (:issue:`80`). .. rubric:: Internals @@ -56,9 +46,6 @@ ProPlot v0.5.0 (2020-##-##) - Panels, colorbars, and legends are now members of `~proplot.subplots.EdgeStack` stacks rather than getting inserted directly into the main `~proplot.subplots.GridSpec` (:pr:`50`). -- Define `~proplot.rctools.rc` default values with inline dictionaries rather than - with a default ``.proplotrc`` file, change the auto-generated user ``.proplotrc`` - (:pr:`50`). ProPlot v0.4.0 (2020-##-##) =========================== @@ -72,7 +59,13 @@ ProPlot v0.4.0 (2020-##-##) - Add Fira Math as DejaVu Sans-alternative (:pr:`95`). Has complete set of math characters. - Add TeX Gyre Heros as Helvetica-alternative (:pr:`95`). This is the new open-source default font. +- Add `xlinewidth`, `ylinewidth`, `xgridcolor`, `ygridcolor` keyword + args to `~proplot.axes.XYAxes.format` (:pr:`95`). - Add `~proplot.subplots.Figure` ``fallback_to_cm`` kwarg. This is used by `~proplot.styletools.show_fonts` to show dummy glyphs to clearly illustrate when fonts are missing characters, but preserve graceful fallback for end user. +- `~proplot.rctools.rc` `~proplot.rctools.rc_configurator.__getitem__` always + returns the setting. To get context block-restricted settings, you must explicitly pass + ``context=True`` to `~proplot.rctools.rc_configurator.get`, `~proplot.rctools.rc_configurator.fill`, + or `~proplot.rctools.rc_configurator.category` (:pr:`91`). .. rubric:: Bug fixes @@ -86,6 +79,12 @@ ProPlot v0.4.0 (2020-##-##) - Imperative mood for docstring summaries (:pr:`92`). - Fix `~proplot.styletools.show_cycles` bug (:pr:`90`) and show cycles using colorbars rather than lines. +.. rubric:: Internals + +- Define `~proplot.rctools.rc` default values with inline dictionaries rather than + with a default ``.proplotrc`` file, change the auto-generated user ``.proplotrc`` + (:pr:`91`). + ProPlot v0.3.1 (2019-12-16) =========================== .. rubric:: Bug fixes diff --git a/docs/index.rst b/docs/index.rst index 8565867ad..e522517c6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,7 @@ ProPlot A comprehensive, easy-to-use `matplotlib `__ wrapper for making beautiful, publication-quality graphics. This project is published `on GitHub `__. -Please note that due to my day job as a graduate student, `certain feature additions `__ will be delayed to the summer of 2020. In the meantime, if you are interested in contributing to ProPlot, please see the :ref:`Contribution guide`. Any amount of help is welcome! +Please note that due to my day job as a graduate student, `certain feature additions `__ will be delayed to the summer of 2020. In the meantime, if you are interested in contributing to ProPlot, please see the :ref:`contribution guide `. Any amount of help is welcome! .. toctree:: :maxdepth: 1 From 93c8aa54a89884a7b51fa30f90b59309ceaf1dc3 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 17:48:55 -0700 Subject: [PATCH 13/33] Cleaner install instructions --- INSTALL.rst | 8 +------- README.rst | 9 +-------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index 7140de578..14aa865d5 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -6,19 +6,13 @@ ProPlot can be installed with `pip `__ or `conda `__ or `conda Date: Mon, 6 Jan 2020 17:55:11 -0700 Subject: [PATCH 14/33] Remove rc._init reference --- proplot/rctools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/proplot/rctools.py b/proplot/rctools.py index dce8f5a35..b56eac80a 100644 --- a/proplot/rctools.py +++ b/proplot/rctools.py @@ -1121,7 +1121,6 @@ def ipython_matplotlib(backend=None, fmt=None): # Default behavior dependent on type of ipython session # See: https://stackoverflow.com/a/22424821/4970632 - rc._init = False ibackend = backend if backend == 'auto': if 'IPKernelApp' in getattr(get_ipython(), 'config', ''): From 5b45522c73792a0ff46691d54b1e37decab04cd1 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 18:30:07 -0700 Subject: [PATCH 15/33] Fix bug/conflict between ipython_* funcs and rc.__init__ --- proplot/rctools.py | 61 +++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/proplot/rctools.py b/proplot/rctools.py index b56eac80a..16bbe8025 100644 --- a/proplot/rctools.py +++ b/proplot/rctools.py @@ -536,15 +536,6 @@ def _get_synced_params(key, value): if '.' in key: pass - # Special ipython settings - # TODO: Put this inside __setitem__? - elif key == 'matplotlib': - ipython_matplotlib(value) - elif key == 'autosave': - ipython_autosave(value) - elif key == 'autoreload': - ipython_autoreload(value) - # Cycler elif key in ('cycle', 'rgbcycle'): if key == 'rgbcycle': @@ -749,9 +740,13 @@ def __init__(self, local=True): raise err for key, value in (data or {}).items(): try: - self[key] = value + 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.""" @@ -777,7 +772,10 @@ def __exit__(self, *args): f'rc context must be initialized with rc.context().') *_, restore = self._context[-1] for key, value in restore.items(): - self[key] = value + 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): @@ -814,8 +812,14 @@ def __setattr__(self, attr, value): def __setitem__(self, key, value): """Modify an `rcParams \ -`__, +`__, :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) @@ -1016,21 +1020,19 @@ def keys(self): def update(self, *args, **kwargs): """ - Update multiple settings at once. + Update several settings at once with a dictionary and/or + keyword arguments. Parameters ---------- - *args : str, dict, or (str, dict) - The first argument can optionally be a "category" string name, - in which case all other setting names passed to this function are - prepended with the string ``cat + '.'``. For example, + *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. - - The first or second argument can also be a dictionary of `rc` - names and values. - **kwargs - `rc` names and values passed as keyword arguments. If the + **kwargs, optional + `rc` keys and values passed as keyword arguments. If the name has dots, simply omit them. """ # Parse args @@ -1038,10 +1040,10 @@ def update(self, *args, **kwargs): prefix = '' if len(args) > 2: raise ValueError( - 'Accepts 1-2 positional arguments. Use plot.rc.update(kw) ' - 'to update a bunch of names, or plot.rc.update(category, kw) ' - 'to update subcategories belonging to single category ' - 'e.g. axes. Keyword args are added to the kw dict.') + 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] @@ -1129,7 +1131,8 @@ def ipython_matplotlib(backend=None, fmt=None): ibackend = 'qt' try: ipython.magic('matplotlib ' + ibackend) - rc.reset() + 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.') @@ -1214,7 +1217,9 @@ def ipython_autosave(autosave=None): #: See :ref:`Configuring proplot` for details. rc = rc_configurator() -# Call setup functions +# 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() From 1bf78ce0f4dcf844b2806b47d9691912c613ded9 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 18:52:08 -0700 Subject: [PATCH 16/33] Remove _getitem_mode assignment --- proplot/subplots.py | 1 - 1 file changed, 1 deletion(-) diff --git a/proplot/subplots.py b/proplot/subplots.py index d0e029bf3..6c569ad79 100644 --- a/proplot/subplots.py +++ b/proplot/subplots.py @@ -1983,7 +1983,6 @@ def subplots( axs : `subplot_grid` A special list of axes instances. See `subplot_grid`. """ # noqa - rc._getitem_mode = 0 # Build array if order not in ('C', 'F'): # better error message raise ValueError( From 3c44865e99cd10fecea146c79cf02d46516242cf Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 19:03:12 -0700 Subject: [PATCH 17/33] Add _get_space function, simplify _panel_kwargs function --- proplot/subplots.py | 60 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/proplot/subplots.py b/proplot/subplots.py index 6c569ad79..85d23bfba 100644 --- a/proplot/subplots.py +++ b/proplot/subplots.py @@ -533,7 +533,65 @@ def _preprocess(self, *args, **kwargs): return _preprocess.__get__(canvas) # ...I don't get it either -def _panels_kwargs( +def _get_panelargs( + side, share=None, width=None, space=None, + filled=False, figure=False): + """Return default properties for new axes and figure panels.""" + s = side[0] + if s not in 'lrbt': + raise ValueError(f'Invalid panel spec {side!r}.') + space = space_user = units(space) + if share is None: + share = (not filled) + if width is None: + if filled: + width = rc['colorbar.width'] + else: + width = rc['subplots.panelwidth'] + width = units(width) + if space is None: + key = ('wspace' if s in 'lr' else 'hspace') + pad = (rc['axpad'] if figure else rc['panelpad']) + space = _get_space(key, share, pad=pad) + return share, width, space, space_user + + +def _get_space(key, share=0, pad=None): + """Return suitable default spacing given a shared axes setting.""" + if key == 'left': + space = units(_notNone(pad, rc['subplots.pad'])) + ( + rc['ytick.major.size'] + rc['ytick.labelsize'] + + rc['ytick.major.pad'] + rc['axes.labelsize']) / 72 + elif key == 'right': + space = units(_notNone(pad, rc['subplots.pad'])) + elif key == 'bottom': + space = units(_notNone(pad, rc['subplots.pad'])) + ( + rc['xtick.major.size'] + rc['xtick.labelsize'] + + rc['xtick.major.pad'] + rc['axes.labelsize']) / 72 + elif key == 'top': + space = units(_notNone(pad, rc['subplots.pad'])) + ( + rc['axes.titlepad'] + rc['axes.titlesize']) / 72 + elif key == 'wspace': + space = (units(_notNone(pad, rc['subplots.axpad'])) + + rc['ytick.major.size'] / 72) + if share < 3: + space += (rc['ytick.labelsize'] + rc['ytick.major.pad']) / 72 + if share < 1: + space += rc['axes.labelsize'] / 72 + elif key == 'hspace': + space = units(_notNone(pad, rc['subplots.axpad'])) + ( + rc['axes.titlepad'] + rc['axes.titlesize'] + + rc['xtick.major.size']) / 72 + if share < 3: + space += (rc['xtick.labelsize'] + rc['xtick.major.pad']) / 72 + if share < 0: + space += rc['axes.labelsize'] / 72 + else: + raise KeyError(f'Invalid space key {key!r}.') + return space + + +def _get_panelargs( side, share=None, width=None, space=None, filled=False, figure=False): """Converts global keywords like `space` and `width` to side-local From 3fb9a527af17d84f5fd59e0191cf5efbf6c94c08 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 19:04:32 -0700 Subject: [PATCH 18/33] Rename _panel_kwargs --> _get_panelargs --- proplot/subplots.py | 35 ++++++----------------------------- 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/proplot/subplots.py b/proplot/subplots.py index 85d23bfba..0b3a8978e 100644 --- a/proplot/subplots.py +++ b/proplot/subplots.py @@ -591,31 +591,6 @@ def _get_space(key, share=0, pad=None): return space -def _get_panelargs( - side, share=None, width=None, space=None, - filled=False, figure=False): - """Converts global keywords like `space` and `width` to side-local - keywords like `lspace` and `lwidth`, and applies default settings.""" - # Return values - # NOTE: Make default legend width same as default colorbar width, in - # case user draws legend and colorbar panel in same row or column! - s = side[0] - if s not in 'lrbt': - raise ValueError(f'Invalid panel spec {side!r}.') - space_orig = units(space) - if filled: - default = rc['colorbar.width'] - else: - default = rc['subplots.panelwidth'] - share = _notNone(share, (not filled)) - width = units(_notNone(width, default)) - space = _notNone(units(space), units(rc['subplots.' + ( - 'panel' if share and not figure - else 'xlab' if s == 'b' else 'ylab' if s == 'l' - else 'inner' if figure else 'panel') + 'space'])) - return share, width, space, space_orig - - def _subplots_geometry(**kwargs): """Save arguments passed to `subplots`, calculates gridspec settings and figure size necessary for requested geometry, and returns keyword args @@ -919,8 +894,9 @@ def _add_axes_panel(self, ax, side, filled=False, **kwargs): raise ValueError(f'Invalid side {side!r}.') ax = ax._panel_parent or ax # redirect to main axes side = SIDE_TRANSLATE[s] - share, width, space, space_orig = _panels_kwargs( - s, filled=filled, figure=False, **kwargs) + share, width, space, space_orig = _get_panelargs( + s, filled=filled, figure=False, **kwargs + ) # Get gridspec and subplotspec indices subplotspec = ax.get_subplotspec() @@ -975,8 +951,9 @@ def _add_figure_panel(self, side, if s not in 'lrbt': raise ValueError(f'Invalid side {side!r}.') side = SIDE_TRANSLATE[s] - _, width, space, space_orig = _panels_kwargs( - s, filled=True, figure=True, **kwargs) + _, width, space, space_orig = _get_panelargs( + s, filled=True, figure=True, **kwargs + ) if s in 'lr': for key, value in (('col', col), ('cols', cols)): if value is not None: From dba9f70dc126ba7c4c401272eac403a9cc1b4c43 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 19:11:51 -0700 Subject: [PATCH 19/33] Use _get_space; remove refs to subplots.ylabspace, etc. --- proplot/subplots.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/proplot/subplots.py b/proplot/subplots.py index 0b3a8978e..758e3305a 100644 --- a/proplot/subplots.py +++ b/proplot/subplots.py @@ -2185,25 +2185,15 @@ def subplots( 'wspace': wspace, 'hspace': hspace, } - # Default border spaces - left = _notNone(left, units(rc['subplots.ylabspace'])) - right = _notNone(right, units(rc['subplots.innerspace'])) - top = _notNone(top, units(rc['subplots.titlespace'])) - bottom = _notNone(bottom, units(rc['subplots.xlabspace'])) - # Default spaces between axes + # Apply default spaces + left = _notNone(left, _get_space('left')) + right = _notNone(right, _get_space('right')) + bottom = _notNone(bottom, _get_space('bottom')) + top = _notNone(top, _get_space('top')) wratios, hratios = [*wratios], [*hratios] # copies wspace, hspace = np.array(wspace), np.array(hspace) # also copies! - wspace[wspace == None] = ( # noqa - units(rc['subplots.innerspace']) if sharey == 3 else - units(rc['subplots.ylabspace']) - units(rc['subplots.titlespace']) - if sharey in (1, 2) else units(rc['subplots.ylabspace'])) - hspace[hspace == None] = ( # noqa - units(rc['subplots.titlespace']) + units(rc['subplots.innerspace']) - if sharex == 3 else units(rc['subplots.xlabspace']) - if sharex in (1, 2) else units(rc['subplots.titlespace']) - + units(rc['subplots.xlabspace']) - ) - wspace, hspace = wspace.tolist(), hspace.tolist() + wspace[wspace == None] = _get_space('wspace', sharex) # noqa + hspace[hspace == None] = _get_space('hspace', sharey) # noqa # Parse arguments, fix dimensions in light of desired aspect ratio figsize, gridspec_kw, subplots_kw = _subplots_geometry( From fdfe042b563df58a3f025542ea770b08005d8023 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 19:12:06 -0700 Subject: [PATCH 20/33] Update default font sizes --- proplot/rctools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proplot/rctools.py b/proplot/rctools.py index 16bbe8025..c26951a92 100644 --- a/proplot/rctools.py +++ b/proplot/rctools.py @@ -55,7 +55,7 @@ def get_ipython(): 'innerborders': False, 'lakes': False, 'land': False, - 'large': 9, + 'large': 10, 'linewidth': 0.6, 'lut': 256, 'margin': 0.0, @@ -66,7 +66,7 @@ def get_ipython(): 'rgbcycle': False, 'rivers': False, 'share': 3, - 'small': 8, + 'small': 9, 'span': True, 'tickdir': 'out', 'ticklen': 4.0, From 3218c2a8357d2afebfa2d4e7fddaae3d3bfd3a40 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 19:14:51 -0700 Subject: [PATCH 21/33] Change refs from panelspace --> panelpad --- proplot/axes.py | 6 +++--- proplot/subplots.py | 16 +++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/proplot/axes.py b/proplot/axes.py index 55c14abcf..29308fd11 100644 --- a/proplot/axes.py +++ b/proplot/axes.py @@ -907,7 +907,7 @@ def colorbar(self, *args, loc=None, pad=None, For outer colorbars only. The space between the colorbar and the main axes. Units are interpreted by `~proplot.utils.units`. When :rcraw:`tight` is ``True``, this is adjusted automatically. - Otherwise, the default is :rc:`subplots.panelspace`. + Otherwise, the default is :rc:`subplots.panelpad`. frame, frameon : bool, optional For inset colorbars, indicates whether to draw a "frame", just like `~matplotlib.axes.Axes.legend`. Default is @@ -1128,7 +1128,7 @@ def legend(self, *args, loc=None, width=None, space=None, **kwargs): For outer legends only. The space between the axes and the legend box. Units are interpreted by `~proplot.utils.units`. When :rcraw:`tight` is ``True``, this is adjusted automatically. - Otherwise, the default is :rc:`subplots.panelspace`. + Otherwise, the default is :rc:`subplots.panelpad`. Other parameters ---------------- @@ -1350,7 +1350,7 @@ def panel_axes(self, side, **kwargs): space : float or str or list thereof, optional Empty space between the main subplot and the panel. When :rcraw:`tight` is ``True``, this is adjusted automatically. - Otherwise, the default is :rc:`subplots.panelspace`. + Otherwise, the default is :rc:`subplots.panelpad`. share : bool, optional Whether to enable axis sharing between the *x* and *y* axes of the main subplot and the panel long axes for each panel in the stack. diff --git a/proplot/subplots.py b/proplot/subplots.py index 758e3305a..a2379985e 100644 --- a/proplot/subplots.py +++ b/proplot/subplots.py @@ -1606,7 +1606,7 @@ def colorbar(self, *args, The space between the main subplot grid and the colorbar, or the space between successively stacked colorbars. Units are interpreted by `~proplot.utils.units`. By default, this is determined by - the "tight layout" algorithm, or is :rc:`subplots.panelspace` + the "tight layout" algorithm, or is :rc:`subplots.panelpad` if "tight layout" is off. width : float or str, optional The colorbar width. Units are interpreted by @@ -1681,17 +1681,19 @@ def legend(self, *args, space between successively stacked colorbars. Units are interpreted by `~proplot.utils.units`. By default, this is adjusted automatically in the "tight layout" calculation, or is - :rc:`subplots.panelspace` if "tight layout" is turned off. + :rc:`subplots.panelpad` if "tight layout" is turned off. *args, **kwargs Passed to `~proplot.axes.Axes.legend`. """ if 'ax' in kwargs: - return kwargs.pop('ax').legend(*args, - space=space, width=width, **kwargs) + return kwargs.pop('ax').legend( + *args, space=space, width=width, **kwargs + ) else: - ax = self._add_figure_panel(loc, - space=space, width=width, span=span, - row=row, col=col, rows=rows, cols=cols) + ax = self._add_figure_panel( + loc, space=space, width=width, span=span, + row=row, col=col, rows=rows, cols=cols + ) return ax.legend(*args, loc='_fill', **kwargs) def save(self, filename, **kwargs): From 1ab92caff9b413381ed23424669913048cdcd074 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 19:15:19 -0700 Subject: [PATCH 22/33] Remove subplots.ylabspace, etc. from configuration docs --- docs/configuration.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 5a2bc2e15..b417523d5 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -170,11 +170,6 @@ Key(s) Description ``subplots.pad`` Padding around figure edge. Units are interpreted by `~proplot.utils.units`. ``subplots.axpad`` Padding between adjacent subplots. Units are interpreted by `~proplot.utils.units`. ``subplots.panelpad`` Padding between subplots and panels, and between stacked panels. Units are interpreted by `~proplot.utils.units`. -``subplots.titlespace`` Vertical space for titles. Units are interpreted by `~proplot.utils.units`. -``subplots.ylabspace`` Horizontal space between subplots allotted for *y*-labels. Units are interpreted by `~proplot.utils.units`. -``subplots.xlabspace`` Vertical space between subplots allotted for *x*-labels. Units are interpreted by `~proplot.utils.units`. -``subplots.innerspace`` Space between subplots allotted for tick marks. Units are interpreted by `~proplot.utils.units`. -``subplots.panelspace`` Purely empty space between main axes and side panels. Units are interpreted by `~proplot.utils.units`. ``suptitle.color`` Figure title color. ``suptitle.size`` Figure title font size. ``suptitle.weight`` Figure title font weight. From 16ae44f1d76b1c608daa5db1b67bd61bca74bf3b Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 19:23:50 -0700 Subject: [PATCH 23/33] Fix _loc_translate bug --- proplot/axes.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/proplot/axes.py b/proplot/axes.py index 29308fd11..267c699ec 100644 --- a/proplot/axes.py +++ b/proplot/axes.py @@ -345,10 +345,13 @@ def _loc_translate(loc, default=None): if loc in (None, True): loc = default elif isinstance(loc, (str, Integral)): - try: - loc = LOC_TRANSLATE[loc] - except KeyError: - raise KeyError(f'Invalid location {loc!r}.') + if loc in LOC_TRANSLATE.values(): # full name + pass + else: + try: + loc = LOC_TRANSLATE[loc] + except KeyError: + raise KeyError(f'Invalid location {loc!r}.') elif np.iterable(loc) and len(loc) == 2 and all( isinstance(l, Number) for l in loc): loc = np.array(loc) From 6629efba28288609f77ba05d3f1c3fbd4f26433d Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 19:43:23 -0700 Subject: [PATCH 24/33] Remove panel_kw from colorbar/legend_wrapper --- proplot/wrappers.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/proplot/wrappers.py b/proplot/wrappers.py index 9a7c454a8..94faad1c1 100644 --- a/proplot/wrappers.py +++ b/proplot/wrappers.py @@ -1388,7 +1388,6 @@ def cycle_changer( label=None, labels=None, values=None, legend=None, legend_kw=None, colorbar=None, colorbar_kw=None, - panel_kw=None, **kwargs): """ Wraps methods that use the property cycler (%(methods)s), @@ -1432,10 +1431,6 @@ def cycle_changer( colorbar_kw : dict-like, optional Ignored if `colorbar` is ``None``. Extra keyword args for our call to `~proplot.axes.Axes.colorbar`. - panel_kw : dict-like, optional - Dictionary of keyword arguments passed to - `~proplot.axes.Axes.panel`, if you are generating an - on-the-fly panel. Other parameters ---------------- @@ -1457,7 +1452,6 @@ def cycle_changer( cycle_kw = cycle_kw or {} legend_kw = legend_kw or {} colorbar_kw = colorbar_kw or {} - panel_kw = panel_kw or {} # Test input # NOTE: Requires standardize_1d wrapper before reaching this. Also note @@ -1619,12 +1613,12 @@ def cycle_changer( # Add colorbar and/or legend if colorbar: # Add handles - panel_kw.setdefault('mode', 'colorbar') - loc = self._loc_translate(colorbar, **panel_kw) + loc = self._loc_translate(colorbar) if not isinstance(loc, str): raise ValueError( f'Invalid on-the-fly location {loc!r}. ' - 'Must be a preset location. See Axes.colorbar') + 'Must be a preset location. See Axes.colorbar' + ) if loc not in self._auto_colorbar: self._auto_colorbar[loc] = ([], {}) self._auto_colorbar[loc][0].extend(objs) @@ -1636,12 +1630,12 @@ def cycle_changer( self._auto_colorbar[loc][1].update(colorbar_kw) if legend: # Add handles - panel_kw.setdefault('mode', 'legend') - loc = self._loc_translate(legend, **panel_kw) + loc = self._loc_translate(legend) if not isinstance(loc, str): raise ValueError( f'Invalid on-the-fly location {loc!r}. ' - 'Must be a preset location. See Axes.legend') + 'Must be a preset location. See Axes.legend' + ) if loc not in self._auto_legend: self._auto_legend[loc] = ([], {}) self._auto_legend[loc][0].extend(objs) @@ -1671,7 +1665,7 @@ def cmap_changer( N=None, levels=None, values=None, centers=None, vmin=None, vmax=None, locator=None, symmetric=False, locator_kw=None, edgefix=None, labels=False, labels_kw=None, fmt=None, precision=2, - colorbar=False, colorbar_kw=None, panel_kw=None, + colorbar=False, colorbar_kw=None, lw=None, linewidth=None, linewidths=None, ls=None, linestyle=None, linestyles=None, color=None, colors=None, edgecolor=None, edgecolors=None, @@ -1766,10 +1760,6 @@ def cmap_changer( colorbar_kw : dict-like, optional Ignored if `colorbar` is ``None``. Extra keyword args for our call to `~proplot.axes.Axes.colorbar`. - panel_kw : dict-like, optional - Dictionary of keyword arguments passed to - `~proplot.axes.Axes.panel`, if you are generating an - on-the-fly panel. Other parameters ---------------- @@ -1813,7 +1803,6 @@ def cmap_changer( locator_kw = locator_kw or {} labels_kw = labels_kw or {} colorbar_kw = colorbar_kw or {} - panel_kw = panel_kw or {} # Parse args # Disable edgefix=True for certain keyword combos e.g. if user wants @@ -2113,8 +2102,7 @@ def cmap_changer( # Add colorbar if colorbar: - panel_kw.setdefault('mode', 'colorbar') - loc = self._loc_translate(colorbar, **panel_kw) + loc = self._loc_translate(colorbar) if not isinstance(loc, str): raise ValueError( f'Invalid on-the-fly location {loc!r}. ' From ef336948b37b13ae146ee808c3fb23876950019f Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 19:43:32 -0700 Subject: [PATCH 25/33] Update changelog --- CHANGELOG.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fbd91e6cc..382ba694e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -61,7 +61,9 @@ ProPlot v0.4.0 (2020-##-##) - Add TeX Gyre Heros as Helvetica-alternative (:pr:`95`). This is the new open-source default font. - Add `xlinewidth`, `ylinewidth`, `xgridcolor`, `ygridcolor` keyword args to `~proplot.axes.XYAxes.format` (:pr:`95`). -- Add `~proplot.subplots.Figure` ``fallback_to_cm`` kwarg. This is used by `~proplot.styletools.show_fonts` to show dummy glyphs to clearly illustrate when fonts are missing characters, but preserve graceful fallback for end user. +- Add `~proplot.subplots.Figure` ``fallback_to_cm`` kwarg. This is used by + `~proplot.styletools.show_fonts` to show dummy glyphs to clearly illustrate when fonts are + missing characters, but preserve graceful fallback for end user. - `~proplot.rctools.rc` `~proplot.rctools.rc_configurator.__getitem__` always returns the setting. To get context block-restricted settings, you must explicitly pass ``context=True`` to `~proplot.rctools.rc_configurator.get`, `~proplot.rctools.rc_configurator.fill`, @@ -77,13 +79,16 @@ ProPlot v0.4.0 (2020-##-##) .. rubric:: Documentation - Imperative mood for docstring summaries (:pr:`92`). -- Fix `~proplot.styletools.show_cycles` bug (:pr:`90`) and show cycles using colorbars rather than lines. +- Fix `~proplot.styletools.show_cycles` bug (:pr:`90`) and show cycles using colorbars + rather than lines. .. rubric:: Internals - Define `~proplot.rctools.rc` default values with inline dictionaries rather than with a default ``.proplotrc`` file, change the auto-generated user ``.proplotrc`` (:pr:`91`). +- Remove useless `panel_kw` keyword arg from `~proplot.wrappers.legend_wrapper` and + `~proplot.wrappers.colorbar_wrapper` (:pr:`91`). ProPlot v0.3.1 (2019-12-16) =========================== From 20b9f5b8c5b9e1ddb986a818ca47482d46bbfdfd Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 19:47:43 -0700 Subject: [PATCH 26/33] No longer change figure.dpi --- proplot/rctools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/proplot/rctools.py b/proplot/rctools.py index c26951a92..003d311a6 100644 --- a/proplot/rctools.py +++ b/proplot/rctools.py @@ -169,7 +169,6 @@ def get_ipython(): 'axes.xmargin': 0.0, 'axes.ymargin': 0.0, 'figure.autolayout': False, - 'figure.dpi': 90, 'figure.facecolor': '#f2f2f2', 'figure.max_open_warning': 0, 'figure.titleweight': 'bold', From db78bc046493973684be2f3762ab4ec8b732827d Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 22:00:20 -0700 Subject: [PATCH 27/33] Fix rc.category bug, minor subplots bugs --- proplot/rctools.py | 14 ++++++++++---- proplot/subplots.py | 5 +++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/proplot/rctools.py b/proplot/rctools.py index 003d311a6..015b2058a 100644 --- a/proplot/rctools.py +++ b/proplot/rctools.py @@ -851,7 +851,7 @@ def _get_item(self, key, mode=None): else: return None - def category(self, cat, *, context=False): + def category(self, cat, *, trimcat=True, context=False): """ Return a dictionary of settings beginning with the substring ``cat + '.'``. @@ -859,7 +859,10 @@ def category(self, cat, *, context=False): Parameters ---------- cat : str, optional - The `rc` settings category. + 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. @@ -868,16 +871,19 @@ def category(self, cat, *, context=False): if cat not in RC_CATEGORIES: raise ValueError( f'Invalid rc category {cat!r}. Valid categories are ' - ', '.join(map(repr, RC_CATEGORIES)) + '.') + ', '.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.search(f'^{cat}[.][^.]+$', key): + 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 diff --git a/proplot/subplots.py b/proplot/subplots.py index a2379985e..de5325235 100644 --- a/proplot/subplots.py +++ b/proplot/subplots.py @@ -551,7 +551,7 @@ def _get_panelargs( width = units(width) if space is None: key = ('wspace' if s in 'lr' else 'hspace') - pad = (rc['axpad'] if figure else rc['panelpad']) + pad = (rc['subplots.axpad'] if figure else rc['subplots.panelpad']) space = _get_space(key, share, pad=pad) return share, width, space, space_user @@ -2192,10 +2192,11 @@ def subplots( right = _notNone(right, _get_space('right')) bottom = _notNone(bottom, _get_space('bottom')) top = _notNone(top, _get_space('top')) - wratios, hratios = [*wratios], [*hratios] # copies wspace, hspace = np.array(wspace), np.array(hspace) # also copies! wspace[wspace == None] = _get_space('wspace', sharex) # noqa hspace[hspace == None] = _get_space('hspace', sharey) # noqa + wratios, hratios = list(wratios), list(hratios) + wspace, hspace = list(wspace), list(hspace) # Parse arguments, fix dimensions in light of desired aspect ratio figsize, gridspec_kw, subplots_kw = _subplots_geometry( From c57c7b0c076818d30af16dfaef0bd074bdb62d1b Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 22:13:11 -0700 Subject: [PATCH 28/33] Prefer closing brackets on new lines --- proplot/axes.py | 239 +++++++++++++++++------------ proplot/axistools.py | 41 +++-- proplot/projs.py | 33 ++-- proplot/rctools.py | 22 ++- proplot/styletools.py | 189 ++++++++++++++--------- proplot/subplots.py | 224 ++++++++++++++++----------- proplot/utils.py | 18 ++- proplot/wrappers.py | 346 +++++++++++++++++++++++++----------------- 8 files changed, 667 insertions(+), 445 deletions(-) diff --git a/proplot/axes.py b/proplot/axes.py index 267c699ec..6997b4cde 100644 --- a/proplot/axes.py +++ b/proplot/axes.py @@ -473,7 +473,8 @@ def _sharex_setup(self, sharex, level): 'Level can be 0 (share nothing), ' '1 (do not share limits, just hide axis labels), ' '2 (share limits, but do not hide tick labels), or ' - '3 (share limits and hide tick labels). Got {level}.') + '3 (share limits and hide tick labels). Got {level}.' + ) # enforce, e.g. if doing panel sharing self._sharex_level = max(self._sharex_level, level) self._share_short_axis(sharex, 'l', level) @@ -488,7 +489,8 @@ def _sharey_setup(self, sharey, level): 'Level can be 0 (share nothing), ' '1 (do not share limits, just hide axis labels), ' '2 (share limits, but do not hide tick labels), or ' - '3 (share limits and hide tick labels). Got {level}.') + '3 (share limits and hide tick labels). Got {level}.' + ) self._sharey_level = max(self._sharey_level, level) self._share_short_axis(sharey, 'b', level) self._share_short_axis(sharey, 't', level) @@ -780,13 +782,15 @@ def format( abcstyle = kwargs.pop('abcformat') _warn_proplot( f'rc setting "abcformat" is deprecated. ' - f'Please use "abcstyle".') + f'Please use "abcstyle".' + ) if abcstyle and self.number is not None: if not isinstance(abcstyle, str) or ( abcstyle.count('a') != 1 and abcstyle.count('A') != 1): raise ValueError( f'Invalid abcstyle {abcstyle!r}. ' - 'Must include letter "a" or "A".') + 'Must include letter "a" or "A".' + ) abcedges = abcstyle.split('a' if 'a' in abcstyle else 'A') text = abcedges[0] + _abc(self.number - 1) + abcedges[-1] if 'A' in abcstyle: @@ -829,7 +833,8 @@ def format( for ikey, ititle in kwargs.items(): if not ikey[-5:] == 'title': raise TypeError( - f'format() got an unexpected keyword argument {ikey!r}.') + f'format() got an unexpected keyword argument {ikey!r}.' + ) iloc, iobj, ikw = self._get_title_props(loc=ikey[:-5]) if ititle is not None: ikw['text'] = ititle @@ -864,10 +869,12 @@ def boxes(self, *args, **kwargs): """Alias for `~matplotlib.axes.Axes.boxplot`.""" return self.boxplot(*args, **kwargs) - def colorbar(self, *args, loc=None, pad=None, - length=None, width=None, space=None, frame=None, frameon=None, - alpha=None, linewidth=None, edgecolor=None, facecolor=None, - **kwargs): + def colorbar( + self, *args, loc=None, pad=None, + length=None, width=None, space=None, frame=None, frameon=None, + alpha=None, linewidth=None, edgecolor=None, facecolor=None, + **kwargs + ): """ Add an *inset* colorbar or *outer* colorbar along the outside edge of the axes. See `~proplot.wrappers.colorbar_wrapper` for details. @@ -926,7 +933,8 @@ def colorbar(self, *args, loc=None, pad=None, """ # TODO: add option to pad inset away from axes edge! kwargs.update({'edgecolor': edgecolor, 'linewidth': linewidth}) - loc = self._loc_translate(loc, rc['colorbar.loc']) + if loc != '_fill': + loc = self._loc_translate(loc, rc['colorbar.loc']) if not isinstance(loc, str): # e.g. 2-tuple or ndarray raise ValueError(f'Invalid colorbar location {loc!r}.') if loc == 'best': # white lie @@ -956,7 +964,8 @@ def colorbar(self, *args, loc=None, pad=None, if length <= 0 or length > 1: raise ValueError( f'Panel colorbar length must satisfy 0 < length <= 1, ' - f'got length={length!r}.') + f'got length={length!r}.' + ) if side in ('bottom', 'top'): gridspec = mgridspec.GridSpecFromSubplotSpec( nrows=1, ncols=3, wspace=0, @@ -974,7 +983,7 @@ def colorbar(self, *args, loc=None, pad=None, with self.figure._authorize_add_subplot(): ax = self.figure.add_subplot(subplotspec, projection=None) if ax is self: - raise ValueError(f'Uh oh.') + raise ValueError # should never happen self.add_child_axes(ax) # Location @@ -1078,12 +1087,14 @@ def colorbar(self, *args, loc=None, pad=None, if orient is not None and orient != 'horizontal': _warn_proplot( f'Orientation for inset colorbars must be horizontal, ' - f'ignoring orient={orient!r}.') + f'ignoring orient={orient!r}.' + ) ticklocation = kwargs.pop('tickloc', None) ticklocation = kwargs.pop('ticklocation', None) or ticklocation if ticklocation is not None and ticklocation != 'bottom': _warn_proplot( - f'Inset colorbars can only have ticks on the bottom.') + f'Inset colorbars can only have ticks on the bottom.' + ) kwargs.update({'orientation': 'horizontal', 'ticklocation': 'bottom'}) kwargs.setdefault('maxn', 5) @@ -1138,7 +1149,8 @@ def legend(self, *args, loc=None, width=None, space=None, **kwargs): *args, **kwargs Passed to `~proplot.wrappers.legend_wrapper`. """ - loc = self._loc_translate(loc, rc['legend.loc']) + if loc != '_fill': + loc = self._loc_translate(loc, rc['legend.loc']) if isinstance(loc, np.ndarray): loc = loc.tolist() @@ -1216,8 +1228,10 @@ def heatmap(self, *args, **kwargs): ) return obj - def inset_axes(self, bounds, *, transform=None, zorder=4, - zoom=True, zoom_kw=None, **kwargs): + def inset_axes( + self, bounds, *, transform=None, zorder=4, + zoom=True, zoom_kw=None, **kwargs + ): """ Like the builtin `~matplotlib.axes.Axes.inset_axes` method, but draws an inset `XYAxes` axes and adds some options. @@ -1275,9 +1289,11 @@ def inset_axes(self, bounds, *, transform=None, zorder=4, ax.indicate_inset_zoom(**zoom_kw) return ax - def indicate_inset_zoom(self, alpha=None, - lw=None, linewidth=None, zorder=3.5, - color=None, edgecolor=None, **kwargs): + def indicate_inset_zoom( + self, alpha=None, + lw=None, linewidth=None, zorder=3.5, + color=None, edgecolor=None, **kwargs + ): """ Called automatically when using `~Axes.inset` with ``zoom=True``. Like `~matplotlib.axes.Axes.indicate_inset_zoom`, but *refreshes* the @@ -1369,9 +1385,11 @@ def panel_axes(self, side, **kwargs): @_standardize_1d @_cmap_changer - def parametric(self, *args, values=None, - cmap=None, norm=None, - interp=0, **kwargs): + def parametric( + self, *args, values=None, + cmap=None, norm=None, + interp=0, **kwargs + ): """ Draw a line whose color changes as a function of the parametric coordinate ``values`` using the input colormap ``cmap``. @@ -1415,11 +1433,13 @@ def parametric(self, *args, values=None, if x.ndim != 1 or y.ndim != 1 or values.ndim != 1: raise ValueError( f'x ({x.ndim}d), y ({y.ndim}d), and values ({values.ndim}d)' - ' must be 1-dimensional.') + ' must be 1-dimensional.' + ) if len(x) != len(y) or len(x) != len(values) or len(y) != len(values): raise ValueError( f'{len(x)} xs, {len(y)} ys, but {len(values)} ' - ' colormap values.') + ' colormap values.' + ) # Interpolate values to allow for smooth gradations between values # (bins=False) or color switchover halfway between points (bins=True) @@ -1497,7 +1517,7 @@ def number(self): @number.setter def number(self, num): - if not isinstance(num, Integral) or num < 1: + if num is not None and (not isinstance(num, Integral) or num < 1): raise ValueError(f'Invalid number {num!r}. Must be integer >=1.') self._number = num @@ -1885,43 +1905,44 @@ def _sharey_setup(self, sharey, level): self._shared_y_axes.join(self, sharey) def format( - self, *, - aspect=None, - xloc=None, yloc=None, - xspineloc=None, yspineloc=None, - xtickloc=None, ytickloc=None, fixticks=False, - xlabelloc=None, ylabelloc=None, - xticklabelloc=None, yticklabelloc=None, - xtickdir=None, ytickdir=None, - xgrid=None, ygrid=None, - xgridminor=None, ygridminor=None, - xtickminor=None, ytickminor=None, - xticklabeldir=None, yticklabeldir=None, - xtickrange=None, ytickrange=None, - xreverse=None, yreverse=None, - xlabel=None, ylabel=None, - xlim=None, ylim=None, - xscale=None, yscale=None, - xrotation=None, yrotation=None, - xformatter=None, yformatter=None, - xticklabels=None, yticklabels=None, - xticks=None, xminorticks=None, - xlocator=None, xminorlocator=None, - yticks=None, yminorticks=None, - ylocator=None, yminorlocator=None, - xbounds=None, ybounds=None, - xmargin=None, ymargin=None, - xcolor=None, ycolor=None, - xlinewidth=None, ylinewidth=None, - xgridcolor=None, ygridcolor=None, - xticklen=None, yticklen=None, - xlabel_kw=None, ylabel_kw=None, - xscale_kw=None, yscale_kw=None, - xlocator_kw=None, ylocator_kw=None, - xformatter_kw=None, yformatter_kw=None, - xminorlocator_kw=None, yminorlocator_kw=None, - patch_kw=None, - **kwargs): + self, *, + aspect=None, + xloc=None, yloc=None, + xspineloc=None, yspineloc=None, + xtickloc=None, ytickloc=None, fixticks=False, + xlabelloc=None, ylabelloc=None, + xticklabelloc=None, yticklabelloc=None, + xtickdir=None, ytickdir=None, + xgrid=None, ygrid=None, + xgridminor=None, ygridminor=None, + xtickminor=None, ytickminor=None, + xticklabeldir=None, yticklabeldir=None, + xtickrange=None, ytickrange=None, + xreverse=None, yreverse=None, + xlabel=None, ylabel=None, + xlim=None, ylim=None, + xscale=None, yscale=None, + xrotation=None, yrotation=None, + xformatter=None, yformatter=None, + xticklabels=None, yticklabels=None, + xticks=None, xminorticks=None, + xlocator=None, xminorlocator=None, + yticks=None, yminorticks=None, + ylocator=None, yminorlocator=None, + xbounds=None, ybounds=None, + xmargin=None, ymargin=None, + xcolor=None, ycolor=None, + xlinewidth=None, ylinewidth=None, + xgridcolor=None, ygridcolor=None, + xticklen=None, yticklen=None, + xlabel_kw=None, ylabel_kw=None, + xscale_kw=None, yscale_kw=None, + xlocator_kw=None, ylocator_kw=None, + xformatter_kw=None, yformatter_kw=None, + xminorlocator_kw=None, yminorlocator_kw=None, + patch_kw=None, + **kwargs + ): """ Modify the *x* and *y* axis labels, tick locations, tick labels, axis scales, spine settings, and more. Unknown keyword arguments @@ -2682,17 +2703,19 @@ def __init__(self, *args, **kwargs): for axis in (self.xaxis, self.yaxis): axis.set_tick_params(which='both', size=0) - def format(self, *args, - r0=None, theta0=None, thetadir=None, - thetamin=None, thetamax=None, thetalim=None, - rmin=None, rmax=None, rlim=None, - rlabelpos=None, rscale=None, rborder=None, - thetalocator=None, rlocator=None, thetalines=None, rlines=None, - thetaformatter=None, rformatter=None, - thetalabels=None, rlabels=None, - thetalocator_kw=None, rlocator_kw=None, - thetaformatter_kw=None, rformatter_kw=None, - **kwargs): + def format( + self, *args, + r0=None, theta0=None, thetadir=None, + thetamin=None, thetamax=None, thetalim=None, + rmin=None, rmax=None, rlim=None, + rlabelpos=None, rscale=None, rborder=None, + thetalocator=None, rlocator=None, thetalines=None, rlines=None, + thetaformatter=None, rformatter=None, + thetalabels=None, rlabels=None, + thetalocator_kw=None, rlocator_kw=None, + thetaformatter_kw=None, rformatter_kw=None, + **kwargs + ): """ Modify radial gridline locations, gridline labels, limits, and more. Unknown keyword arguments are passed to `Axes.format` and @@ -2767,14 +2790,16 @@ def format(self, *args, if rmin is not None or rmax is not None: _warn_proplot( f'Conflicting keyword args rmin={rmin}, rmax={rmax}, ' - f'and rlim={rlim}. Using "rlim".') + f'and rlim={rlim}. Using "rlim".' + ) rmin, rmax = rlim if thetalim is not None: if thetamin is not None or thetamax is not None: _warn_proplot( f'Conflicting keyword args thetamin={thetamin}, ' f'thetamax={thetamax}, and thetalim={thetalim}. ' - f'Using "thetalim".') + f'Using "thetalim".' + ) thetamin, thetamax = thetalim thetalocator = _notNone( thetalines, thetalocator, None, @@ -2949,13 +2974,14 @@ def __init__(self, *args, **kwargs): self._latlines_labels = None super().__init__(*args, **kwargs) - def format(self, *, - lonlim=None, latlim=None, boundinglat=None, grid=None, - lonlines=None, lonlocator=None, - latlines=None, latlocator=None, latmax=None, - labels=None, latlabels=None, lonlabels=None, - patch_kw=None, **kwargs, - ): + def format( + self, *, + lonlim=None, latlim=None, boundinglat=None, grid=None, + lonlines=None, lonlocator=None, + latlines=None, latlocator=None, latmax=None, + labels=None, latlabels=None, lonlabels=None, + patch_kw=None, **kwargs, + ): """ Modify the meridian and parallel labels, longitude and latitude map limits, geographic features, and more. Unknown keyword arguments are @@ -3229,7 +3255,8 @@ def __init__(self, *args, map_projection=None, **kwargs): import cartopy.crs as ccrs if not isinstance(map_projection, ccrs.Projection): raise ValueError( - 'GeoAxes requires map_projection=cartopy.crs.Projection.') + 'GeoAxes requires map_projection=cartopy.crs.Projection.' + ) super().__init__(*args, map_projection=map_projection, **kwargs) # Zero out ticks so gridlines are not offset @@ -3250,8 +3277,10 @@ def __init__(self, *args, map_projection=None, **kwargs): else: self.set_global() - def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, - lonlines, latlines, latmax, lonarray, latarray): + def _format_apply( + self, patch_kw, lonlim, latlim, boundinglat, + lonlines, latlines, latmax, lonarray, latarray + ): """Apply formatting to cartopy axes.""" import cartopy.feature as cfeature import cartopy.crs as ccrs @@ -3297,7 +3326,8 @@ def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, if (lonlim is not None or latlim is not None): _warn_proplot( f'{proj!r} extent is controlled by "boundinglat", ' - f'ignoring lonlim={lonlim!r} and latlim={latlim!r}.') + f'ignoring lonlim={lonlim!r} and latlim={latlim!r}.' + ) if self._boundinglat is None: if isinstance(self.projection, projs.NorthPolarGnomonic): boundinglat = 30 @@ -3317,7 +3347,8 @@ def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, if boundinglat is not None: _warn_proplot( f'{proj!r} extent is controlled by "lonlim" and "latlim", ' - f'ignoring boundinglat={boundinglat!r}.') + f'ignoring boundinglat={boundinglat!r}.' + ) if lonlim is not None or latlim is not None: lonlim = lonlim or [None, None] latlim = latlim or [None, None] @@ -3371,12 +3402,14 @@ def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, if latarray is not None and any(latarray): _warn_proplot( 'Cannot add gridline labels to cartopy ' - f'{type(self.projection).__name__} projection.') + f'{type(self.projection).__name__} projection.' + ) latarray = [0] * 4 if lonarray is not None and any(lonarray): _warn_proplot( 'Cannot add gridline labels to cartopy ' - f'{type(self.projection).__name__} projection.') + f'{type(self.projection).__name__} projection.' + ) lonarray = [0] * 4 if latarray is not None: gl.ylabels_left = latarray[0] @@ -3595,26 +3628,30 @@ def __init__(self, *args, map_projection=None, **kwargs): import mpl_toolkits.basemap as mbasemap # verify package is available if not isinstance(map_projection, mbasemap.Basemap): raise ValueError( - 'BasemapAxes requires map_projection=basemap.Basemap') + 'BasemapAxes requires map_projection=basemap.Basemap' + ) self._map_projection = map_projection self._map_boundary = None self._has_recurred = False # use this to override plotting methods super().__init__(*args, **kwargs) - def _format_apply(self, patch_kw, lonlim, latlim, boundinglat, - lonlines, latlines, latmax, lonarray, latarray): + def _format_apply( + self, patch_kw, lonlim, latlim, boundinglat, + lonlines, latlines, latmax, lonarray, latarray + ): """Apply changes to the basemap axes.""" # Checks if (lonlim is not None or latlim is not None or boundinglat is not None): - _warn_proplot(f'Got lonlim={lonlim!r}, latlim={latlim!r}, ' - f'boundinglat={boundinglat!r}, but you cannot "zoom ' - 'into" a basemap projection after creating it. ' - 'Pass proj_kw in your call to subplots ' - 'with any of the following basemap keywords: ' - "'boundinglat', 'llcrnrlon', 'llcrnrlat', " - "'urcrnrlon', 'urcrnrlat', 'llcrnrx', 'llcrnry', " - "'urcrnrx', 'urcrnry', 'width', or 'height'.") + _warn_proplot( + f'Got lonlim={lonlim!r}, latlim={latlim!r}, ' + f'boundinglat={boundinglat!r}, but you cannot "zoom into" a ' + 'basemap projection after creating it. Pass proj_kw in your ' + 'call to subplots with any of the following basemap keywords: ' + "'boundinglat', 'llcrnrlon', 'llcrnrlat', " + "'urcrnrlon', 'urcrnrlat', 'llcrnrx', 'llcrnry', " + "'urcrnrx', 'urcrnry', 'width', or 'height'." + ) # Map boundary # * First have to *manually replace* the old boundary by just diff --git a/proplot/axistools.py b/proplot/axistools.py index c1cf2e67c..074640f79 100644 --- a/proplot/axistools.py +++ b/proplot/axistools.py @@ -127,7 +127,8 @@ def Locator(locator, *args, **kwargs): if locator not in locators: raise ValueError( f'Unknown locator {locator!r}. Options are ' - + ', '.join(map(repr, locators.keys())) + '.') + + ', '.join(map(repr, locators.keys())) + '.' + ) locator = locators[locator](*args, **kwargs) elif isinstance(locator, Number): # scalar variable locator = mticker.MultipleLocator(locator, *args, **kwargs) @@ -266,7 +267,8 @@ def Formatter(formatter, *args, date=False, index=False, **kwargs): if formatter not in formatters: raise ValueError( f'Unknown formatter {formatter!r}. Options are ' - + ', '.join(map(repr, formatters.keys())) + '.') + + ', '.join(map(repr, formatters.keys())) + '.' + ) formatter = formatters[formatter](*args, **kwargs) elif callable(formatter): formatter = mticker.FuncFormatter(formatter, *args, **kwargs) @@ -348,7 +350,8 @@ def Scale(scale, *args, **kwargs): if args or kwargs: _warn_proplot( f'Scale {scale!r} is a scale *preset*. Ignoring positional ' - 'argument(s): {args} and keyword argument(s): {kwargs}. ') + 'argument(s): {args} and keyword argument(s): {kwargs}. ' + ) scale, *args = SCALE_PRESETS[scale] # Get scale scale = scale.lower() @@ -357,7 +360,8 @@ def Scale(scale, *args, **kwargs): else: raise ValueError( f'Unknown scale or preset {scale!r}. Options are ' - + ', '.join(map(repr, list(scales) + list(SCALE_PRESETS))) + '.') + + ', '.join(map(repr, list(scales) + list(SCALE_PRESETS))) + '.' + ) return scale(*args, **kwargs) @@ -382,9 +386,11 @@ class AutoFormatter(mticker.ScalarFormatter): 3. Allows user to add arbitrary prefix or suffix to every tick label string. """ - def __init__(self, *args, - zerotrim=None, precision=None, tickrange=None, - prefix=None, suffix=None, negpos=None, **kwargs): + def __init__( + self, *args, + zerotrim=None, precision=None, tickrange=None, + prefix=None, suffix=None, negpos=None, **kwargs + ): """ Parameters ---------- @@ -543,7 +549,8 @@ def _scale_factory(scale, axis, *args, **kwargs): if scale not in scales: raise ValueError( f'Unknown scale {scale!r}. Options are ' - + ', '.join(map(repr, scales.keys())) + '.') + + ', '.join(map(repr, scales.keys())) + '.' + ) return scales[scale](*args, **kwargs) @@ -761,11 +768,12 @@ class FuncScale(_ScaleBase, mscale.ScaleBase): name = 'function' """The registered scale name.""" - def __init__(self, arg, invert=False, parent_scale=None, - major_locator=None, minor_locator=None, - major_formatter=None, minor_formatter=None, - smart_bounds=None, - ): + def __init__( + self, arg, invert=False, parent_scale=None, + major_locator=None, minor_locator=None, + major_formatter=None, minor_formatter=None, + smart_bounds=None, + ): """ Parameters ---------- @@ -1014,8 +1022,8 @@ class ExpScale(_ScaleBase, mscale.ScaleBase): """The registered scale name.""" def __init__( - self, a=np.e, b=1, c=1, inverse=False, minpos=1e-300, - **kwargs): + self, a=np.e, b=1, c=1, inverse=False, minpos=1e-300, **kwargs + ): """ Parameters ---------- @@ -1320,7 +1328,8 @@ def __init__(self, threshs, scales, zero_dists=None): any((dists == 0) != (scales == 0)) or zero_dists is None): raise ValueError( 'Got zero scales and distances in different places or ' - 'zero_dists is None.') + 'zero_dists is None.' + ) self._scales = scales self._threshs = threshs with np.errstate(divide='ignore', invalid='ignore'): diff --git a/proplot/projs.py b/proplot/projs.py index ff21702b6..6e7fd6b30 100644 --- a/proplot/projs.py +++ b/proplot/projs.py @@ -171,11 +171,13 @@ def Proj(name, basemap=False, **kwargs): if 'boundinglat' in kwargs: raise ValueError( f'"boundinglat" must be passed to the ax.format() command ' - 'for cartopy axes.') + 'for cartopy axes.' + ) if crs is None: raise ValueError( f'Unknown projection {name!r}. Options are: ' - + ', '.join(map(repr, cartopy_names.keys()))) + + ', '.join(map(repr, cartopy_names.keys())) + ) proj = crs(**kwargs) aspect = (np.diff(proj.x_limits) / np.diff(proj.y_limits))[0] return proj, aspect @@ -197,8 +199,10 @@ def __init__(self, central_longitude=0, globe=None, a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS b = globe.semiminor_axis or a if b != a or globe.ellipse is not None: - _warn_proplot(f'The {self.name!r} projection does not handle ' - 'elliptical globes.') + _warn_proplot( + f'The {self.name!r} projection does not handle ' + 'elliptical globes.' + ) proj4_params = {'proj': 'hammer', 'lon_0': central_longitude} super().__init__(proj4_params, central_longitude, @@ -230,8 +234,10 @@ def __init__(self, central_longitude=0, globe=None, a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS b = globe.semiminor_axis or a if b != a or globe.ellipse is not None: - _warn_proplot(f'The {self.name!r} projection does not handle ' - 'elliptical globes.') + _warn_proplot( + f'The {self.name!r} projection does not handle ' + 'elliptical globes.' + ) proj4_params = {'proj': 'aitoff', 'lon_0': central_longitude} super().__init__(proj4_params, central_longitude, @@ -263,8 +269,10 @@ def __init__(self, central_longitude=0, globe=None, a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS b = globe.semiminor_axis or a if b != a or globe.ellipse is not None: - _warn_proplot(f'The {self.name!r} projection does not handle ' - 'elliptical globes.') + _warn_proplot( + f'The {self.name!r} projection does not handle ' + 'elliptical globes.' + ) proj4_params = {'proj': 'kav7', 'lon_0': central_longitude} super().__init__(proj4_params, central_longitude, @@ -296,8 +304,10 @@ def __init__(self, central_longitude=0, globe=None, a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS b = globe.semiminor_axis or a if b != a or globe.ellipse is not None: - _warn_proplot(f'The {self.name!r} projection does not handle ' - 'elliptical globes.') + _warn_proplot( + f'The {self.name!r} projection does not handle ' + 'elliptical globes.' + ) proj4_params = {'proj': 'wintri', 'lon_0': central_longitude} super().__init__(proj4_params, central_longitude, @@ -473,4 +483,5 @@ def __init__(self, central_longitude=0.0, globe=None): if _unavail: _warn_proplot( f'Cartopy projection(s) {", ".join(map(repr, _unavail))} are ' - f'unavailable. Consider updating to cartopy >= 0.17.0.') + f'unavailable. Consider updating to cartopy >= 0.17.0.' + ) diff --git a/proplot/rctools.py b/proplot/rctools.py index 015b2058a..94e811859 100644 --- a/proplot/rctools.py +++ b/proplot/rctools.py @@ -448,14 +448,9 @@ def _tabulate(rcdict): 'rivers.linewidth', 'subplots.axpad', 'subplots.axwidth', - 'subplots.innerspace', 'subplots.pad', 'subplots.panelpad', - 'subplots.panelspace', 'subplots.panelwidth', - 'subplots.titlespace', - 'subplots.xlabspace', - 'subplots.ylabspace', 'suptitle.color', 'suptitle.size', 'suptitle.weight', @@ -551,7 +546,8 @@ def _get_synced_params(key, value): mcolors.ListedColormap)) raise ValueError( f'Invalid cycle name {cycle!r}. Options are: ' - ', '.join(map(repr, cycles)) + '.') + ', '.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 @@ -751,7 +747,8 @@ def __enter__(self): """Apply settings from the most recent context block.""" if not self._context: raise RuntimeError( - f'rc context must be initialized with rc.context().') + f'rc object must be initialized with rc.context().' + ) *_, kwargs, cache, restore = self._context[-1] def _update(rcdict, newdict): @@ -768,7 +765,8 @@ def __exit__(self, *args): """Restore settings from the most recent context block.""" if not self._context: raise RuntimeError( - f'rc context must be initialized with rc.context().') + 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) @@ -990,10 +988,7 @@ def fill(self, props, *, context=False): Parameters ---------- props : dict-like - Dictionary whose values are names of settings. The values - are replaced with the corresponding property only if - `~rc_configurator.__getitem__` does not return ``None``. Otherwise, - that key, value pair is omitted from the output dictionary. + 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. @@ -1152,7 +1147,8 @@ def ipython_matplotlib(backend=None, fmt=None): else: raise ValueError( f'Invalid inline backend format {fmt!r}. ' - 'Must be string or list thereof.') + '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 diff --git a/proplot/styletools.py b/proplot/styletools.py index bd837a613..43b8bb7f7 100644 --- a/proplot/styletools.py +++ b/proplot/styletools.py @@ -586,16 +586,19 @@ def _make_segmentdata_array(values, coords=None, ratios=None): if ratios is not None: _warn_proplot( f'Segment coordinates were provided, ignoring ' - f'ratios={ratios!r}.') + f'ratios={ratios!r}.' + ) if len(coords) != len(values) or coords[0] != 0 or coords[-1] != 1: raise ValueError( - f'Coordinates must range from 0 to 1, got {coords!r}.') + f'Coordinates must range from 0 to 1, got {coords!r}.' + ) elif ratios is not None: coords = np.atleast_1d(ratios) if len(coords) != len(values) - 1: raise ValueError( f'Need {len(values)-1} ratios for {len(values)} colors, ' - f'but got {len(ratios)} ratios.') + f'but got {len(ratios)} ratios.' + ) coords = np.concatenate(([0], np.cumsum(coords))) coords = coords / np.max(coords) # normalize to 0-1 else: @@ -678,7 +681,8 @@ def make_mapping_array(N, data, gamma=1.0, inverse=False): if len(gammas) != 1 and len(gammas) != shape[0] - 1: raise ValueError( f'Need {shape[0]-1} gammas for {shape[0]}-level mapping array, ' - f'but got {len(gamma)}.') + f'but got {len(gamma)}.' + ) if len(gammas) == 1: gammas = np.repeat(gammas, shape[:1]) @@ -688,10 +692,12 @@ def make_mapping_array(N, data, gamma=1.0, inverse=False): y1 = data[:, 2] if x[0] != 0.0 or x[-1] != 1.0: raise ValueError( - 'Data mapping points must start with x=0 and end with x=1.') + 'Data mapping points must start with x=0 and end with x=1.' + ) if (np.diff(x) < 0).any(): raise ValueError( - 'Data mapping points must have x in increasing order.') + 'Data mapping points must have x in increasing order.' + ) x = x * (N - 1) # Get distances from the segmentdata entry to the *left* for each requested @@ -759,7 +765,8 @@ def _get_data(self, ext): else: raise ValueError( f'Invalid extension {ext!r}. Options are "hex", "txt", ' - f'"rgb", or "rgba".') + f'"rgb", or "rgba".' + ) return data def _parse_path(self, path, dirname='.', ext=''): @@ -854,18 +861,21 @@ def concatenate(self, *args, ratios=1, name=None, **kwargs): # Try making a simple copy if not args: raise ValueError( - f'Got zero positional args, you must provide at least one.') + f'Got zero positional args, you must provide at least one.' + ) if not all(isinstance(cmap, type(self)) for cmap in args): raise ValueError( f'Colormaps {cmap.name + ": " + repr(cmap) for cmap in args} ' - f'must all belong to the same class.') + f'must all belong to the same class.' + ) cmaps = (self, *args) spaces = {cmap.name: getattr(cmap, '_space', None) for cmap in cmaps} if len({*spaces.values(), }) > 1: raise ValueError( 'Cannot merge PerceptuallyUniformColormaps that use ' 'different colorspaces: ' - + ', '.join(map(repr, spaces)) + '.') + + ', '.join(map(repr, spaces)) + '.' + ) N = kwargs.pop('N', None) N = N or len(cmaps) * rcParams['image.lut'] if name is None: @@ -913,7 +923,8 @@ def xyy(ix, funcs=funcs): xyy[:, 0] = xyy[:, 0] / xyy[:, 0].max(axis=0) # fix fp errors else: raise ValueError( - 'Mixed callable and non-callable colormap values.') + 'Mixed callable and non-callable colormap values.' + ) segmentdata[key] = xyy # Handle gamma values if key == 'saturation': @@ -938,7 +949,8 @@ def xyy(ix, funcs=funcs): _warn_proplot( 'Cannot use multiple segment gammas when ' 'concatenating callable segments. Using the first ' - f'gamma of {gamma[0]}.') + f'gamma of {gamma[0]}.' + ) gamma = gamma[0] kwargs[ikey] = gamma @@ -1160,7 +1172,8 @@ def shifted(self, shift=None, name=None, **kwargs): _warn_proplot( f'Shifting non-cyclic colormap {self.name!r}. ' f'Use cmap.set_cyclic(True) or Colormap(..., cyclic=True) to ' - 'suppress this warning.') + 'suppress this warning.' + ) self._cyclic = True # Decompose shift into two truncations followed by concatenation @@ -1242,16 +1255,18 @@ def xyy(x, func=xyy): _warn_proplot( 'Cannot use multiple segment gammas when ' 'truncating colormap. Using the first gamma ' - f'of {gamma[0]}.') + f'of {gamma[0]}.' + ) gamma = gamma[0] else: gamma = gamma[l - 1:r + 1] kwargs[ikey] = gamma return self.updated(name, segmentdata, **kwargs) - def updated(self, name=None, segmentdata=None, N=None, *, - alpha=None, gamma=None, cyclic=None, - ): + def updated( + self, name=None, segmentdata=None, N=None, *, + alpha=None, gamma=None, cyclic=None + ): """ Returns a new colormap, with relevant properties copied from this one if they were not provided as keyword arguments. @@ -1327,10 +1342,12 @@ def concatenate(self, *args, name=None, N=None, **kwargs): """ if not args: raise ValueError( - f'Got zero positional args, you must provide at least one.') + f'Got zero positional args, you must provide at least one.' + ) if not all(isinstance(cmap, type(self)) for cmap in args): raise ValueError( - f'Input arguments {args} must all be ListedColormap.') + f'Input arguments {args} must all be ListedColormap.' + ) cmaps = (self, *args) if name is None: name = '_'.join(cmap.name for cmap in cmaps) @@ -1457,9 +1474,10 @@ class PerceptuallyUniformColormap(LinearSegmentedColormap, _Colormap): either the HCL colorspace or the HSL or HPL scalings of HCL.""" @docstring.dedent_interpd def __init__( - self, name, segmentdata, N=None, space=None, clip=True, - gamma=None, gamma1=None, gamma2=None, - **kwargs): + self, name, segmentdata, N=None, space=None, clip=True, + gamma=None, gamma1=None, gamma2=None, + **kwargs + ): """ Parameters ---------- @@ -1515,7 +1533,8 @@ def __init__( target = {'hue', 'saturation', 'luminance', 'alpha'} if not keys <= target: raise ValueError( - f'Invalid segmentdata dictionary with keys {keys!r}.') + f'Invalid segmentdata dictionary with keys {keys!r}.' + ) # Convert color strings to channel values for key, array in segmentdata.items(): if callable(array): # permit callable @@ -1612,8 +1631,9 @@ def from_color(name, color, fade=None, space='hsl', **kwargs): @staticmethod def from_hsl( - name, hue=0, saturation=100, luminance=(100, 20), alpha=None, - ratios=None, **kwargs): + name, hue=0, saturation=100, luminance=(100, 20), alpha=None, + ratios=None, **kwargs + ): """ Makes a `~PerceptuallyUniformColormap` by specifying the hue, saturation, and luminance transitions individually. @@ -1736,9 +1756,10 @@ def set_gamma(self, gamma=None, gamma1=None, gamma2=None): self._init() def updated( - self, name=None, segmentdata=None, N=None, *, - alpha=None, gamma=None, cyclic=None, - clip=None, gamma1=None, gamma2=None, space=None): + self, name=None, segmentdata=None, N=None, *, + alpha=None, gamma=None, cyclic=None, + clip=None, gamma1=None, gamma2=None, space=None + ): """ Returns a new colormap, with relevant properties copied from this one if they were not provided as keyword arguments. @@ -1830,14 +1851,16 @@ def __getitem__(self, key): else: raise KeyError( f'Item of type {type(value).__name__!r} ' - 'does not have shifted() method.') + 'does not have shifted() method.' + ) if reverse: if hasattr(value, 'reversed'): value = value.reversed() else: raise KeyError( f'Item of type {type(value).__name__!r} ' - 'does not have reversed() method.') + 'does not have reversed() method.' + ) return value def __setitem__(self, key, item, sort=True): @@ -1859,7 +1882,8 @@ def __setitem__(self, key, item, sort=True): raise ValueError( f'Invalid colormap {item}. Must be instance of ' 'matplotlib.colors.ListedColormap or ' - 'matplotlib.colors.LinearSegmentedColormap.') + 'matplotlib.colors.LinearSegmentedColormap.' + ) key = self._sanitize_key(key, mirror=False) record = cycles if isinstance(item, ListedColormap) else cmaps record.append(key) @@ -1920,7 +1944,8 @@ def update(self, *args, **kwargs): kwargs.update(args[0]) elif len(args) > 1: raise TypeError( - f'update() expected at most 1 arguments, got {len(args)}.') + f'update() expected at most 1 arguments, got {len(args)}.' + ) for key, value in kwargs.items(): self[key] = value @@ -1967,14 +1992,16 @@ def __getitem__(self, key): raise ValueError( f'Color cycle sample for {rgb[0]!r} cycle must be ' f'between 0 and {len(cmap.colors)-1}, ' - f'got {rgb[1]}.') + f'got {rgb[1]}.' + ) # draw color from the list of colors, using index rgb = cmap.colors[rgb[1]] else: if not 0 <= rgb[1] <= 1: raise ValueError( f'Colormap sample for {rgb[0]!r} colormap must be ' - f'between 0 and 1, got {rgb[1]}.') + f'between 0 and 1, got {rgb[1]}.' + ) # interpolate color from colormap, using key in range 0-1 rgb = cmap(rgb[1]) rgba = mcolors.to_rgba(rgb, alpha) @@ -1989,11 +2016,11 @@ def Colors(*args, **kwargs): return [dict_['color'] for dict_ in cycle] -def Colormap(*args, name=None, listmode='perceptual', - fade=None, cycle=None, - shift=None, cut=None, left=None, right=None, reverse=False, - save=False, save_kw=None, - **kwargs): +def Colormap( + *args, name=None, listmode='perceptual', fade=None, cycle=None, + shift=None, cut=None, left=None, right=None, reverse=False, + save=False, save_kw=None, **kwargs +): """ Generate or retrieve colormaps and optionally merge and manipulate them in a variety of ways. Used to interpret the `cmap` and `cmap_kw` @@ -2088,11 +2115,13 @@ def Colormap(*args, name=None, listmode='perceptual', # Initial stuff if not args: raise ValueError( - f'Colormap() requires at least one positional argument.') + f'Colormap() requires at least one positional argument.' + ) if listmode not in ('listed', 'linear', 'perceptual'): raise ValueError( f'Invalid listmode={listmode!r}. Options are ' - '"listed", "linear", and "perceptual".') + '"listed", "linear", and "perceptual".' + ) tmp = '_no_name' cmaps = [] for i, cmap in enumerate(args): @@ -2105,7 +2134,8 @@ def Colormap(*args, name=None, listmode='perceptual', cmap, cmap=(listmode != 'listed')) else: raise FileNotFoundError( - f'Colormap or cycle file {cmap!r} not found.') + f'Colormap or cycle file {cmap!r} not found.' + ) else: try: cmap = mcm.cmap_d[cmap] @@ -2195,12 +2225,12 @@ def Colormap(*args, name=None, listmode='perceptual', def Cycle( - *args, samples=None, name=None, - marker=None, alpha=None, dashes=None, linestyle=None, linewidth=None, - markersize=None, markeredgewidth=None, - markeredgecolor=None, markerfacecolor=None, - save=False, save_kw=None, - **kwargs): + *args, samples=None, name=None, + marker=None, alpha=None, dashes=None, linestyle=None, linewidth=None, + markersize=None, markeredgewidth=None, + markeredgecolor=None, markerfacecolor=None, + save=False, save_kw=None, **kwargs +): """ Generate and merge `~cycler.Cycler` instances in a variety of ways. Used to interpret the `cycle` and `cycle_kw` arguments when passed to @@ -2292,7 +2322,8 @@ def Cycle( if isinstance(value, str) or not np.iterable(value): raise ValueError( f'Invalid {key!r} property {value!r}. ' - f'Must be list or tuple of properties.') + f'Must be list or tuple of properties.' + ) nprops = max(nprops, len(value)) props[key] = [*value] # ensure mutable list # If args is non-empty, means we want color cycle; otherwise is always @@ -2407,13 +2438,15 @@ def Norm(norm, levels=None, **kwargs): if norm_out is None: raise ValueError( f'Unknown normalizer {norm!r}. Options are ' - + ', '.join(map(repr, normalizers.keys())) + '.') + + ', '.join(map(repr, normalizers.keys())) + '.' + ) # Instantiate class if norm_out is LinearSegmentedNorm: if not np.iterable(levels): raise ValueError( f'Need levels for normalizer {norm!r}. ' - 'Received levels={levels!r}.') + f'Received levels={levels!r}.' + ) kwargs.update({'levels': levels}) norm_out = norm_out(**kwargs) # initialize else: @@ -2458,8 +2491,10 @@ class BinNorm(mcolors.BoundaryNorm): # WARNING: Must be child of BoundaryNorm. Many methods in ColorBarBase # test for class membership, crucially including _process_values(), which # if it doesn't detect BoundaryNorm will try to use BinNorm.inverse(). - def __init__(self, levels, norm=None, clip=False, - step=1.0, extend='neither'): + def __init__( + self, levels, norm=None, clip=False, + step=1.0, extend='neither' + ): """ Parameters ---------- @@ -2499,11 +2534,13 @@ def __init__(self, levels, norm=None, clip=False, elif ((levels[1:] - levels[:-1]) <= 0).any(): raise ValueError( f'Levels {levels} passed to Normalize() must be ' - 'monotonically increasing.') + 'monotonically increasing.' + ) if extend not in ('both', 'min', 'max', 'neither'): raise ValueError( f'Unknown extend option {extend!r}. Choose from ' - '"min", "max", "both", "neither".') + '"min", "max", "both", "neither".' + ) # Determine color ids for levels, i.e. position in 0-1 space # Length of these ids should be N + 1 -- that is, N - 1 colors @@ -2519,11 +2556,13 @@ def __init__(self, levels, norm=None, clip=False, elif not isinstance(norm, mcolors.Normalize): raise ValueError( 'Normalizer must be matplotlib.colors.Normalize, ' - f'got {type(norm)}.') + f'got {type(norm)}.' + ) elif isinstance(norm, mcolors.BoundaryNorm): raise ValueError( f'Normalizer cannot be an instance of ' - 'matplotlib.colors.BoundaryNorm.') + 'matplotlib.colors.BoundaryNorm.' + ) x_b = norm(levels) x_m = (x_b[1:] + x_b[:-1]) / 2 # get level centers after norm scaling y = (x_m - x_m.min()) / (x_m.max() - x_m.min()) @@ -2611,7 +2650,8 @@ def __init__(self, levels, vmin=None, vmax=None, **kwargs): elif ((levels[1:] - levels[:-1]) <= 0).any(): raise ValueError( f'Levels {levels} passed to LinearSegmentedNorm must be ' - 'monotonically increasing.') + 'monotonically increasing.' + ) vmin, vmax = levels.min(), levels.max() super().__init__(vmin, vmax, **kwargs) # second level superclass self._x = levels @@ -2687,7 +2727,8 @@ def __call__(self, xq, clip=None): if self.vmin >= self._midpoint or self.vmax <= self._midpoint: raise ValueError( f'Midpoint {self._midpoint} outside of vmin {self.vmin} ' - f'and vmax {self.vmax}.') + f'and vmax {self.vmax}.' + ) x = np.array([self.vmin, self._midpoint, self.vmax]) y = np.array([0, 0.5, 1]) xq = np.atleast_1d(xq) @@ -2768,14 +2809,16 @@ def _load_cmap_cycle(filename, cmap=False): except ValueError: _warn_proplot( f'Failed to load {filename!r}. Expected a table of comma ' - 'or space-separated values.') + 'or space-separated values.' + ) return None, None # Build x-coordinates and standardize shape data = np.array(data) if data.shape[1] != len(ext): _warn_proplot( f'Failed to load {filename!r}. Got {data.shape[1]} columns, ' - f'but expected {len(ext)}.') + f'but expected {len(ext)}.' + ) return None, None if ext[0] != 'x': # i.e. no x-coordinates specified explicitly x = np.linspace(0, 1, data.shape[0]) @@ -2797,12 +2840,14 @@ def _load_cmap_cycle(filename, cmap=False): if any(key not in s.attrib for key in 'xrgb'): _warn_proplot( f'Failed to load {filename!r}. Missing an x, r, g, or b ' - 'specification inside one or more tags.') + 'specification inside one or more tags.' + ) return None, None if 'o' in s.attrib and 'a' in s.attrib: _warn_proplot( f'Failed to load {filename!r}. Contains ' - 'ambiguous opacity key.') + 'ambiguous opacity key.' + ) return None, None # Get data color = [] @@ -2816,7 +2861,8 @@ def _load_cmap_cycle(filename, cmap=False): if not all(len(data[0]) == len(color) for color in data): _warn_proplot( f'File {filename!r} has some points with alpha channel ' - 'specified, some without.') + 'specified, some without.' + ) return None, None # Read hex strings @@ -2826,14 +2872,16 @@ def _load_cmap_cycle(filename, cmap=False): data = re.findall('#[0-9a-fA-F]{6}', string) # list of strings if len(data) < 2: _warn_proplot( - f'Failed to load {filename!r}. Hex strings not found.') + f'Failed to load {filename!r}. Hex strings not found.' + ) return None, None # Convert to array x = np.linspace(0, 1, len(data)) data = [to_rgb(color) for color in data] else: _warn_proplot( - f'Colormap or cycle file {filename!r} has unknown extension.') + f'Colormap or cycle file {filename!r} has unknown extension.' + ) return None, None # Standardize and reverse if necessary to cmap @@ -3004,7 +3052,8 @@ def register_colors(nmax=np.inf): if not all(len(pair) == 2 for pair in pairs): raise RuntimeError( f'Invalid color names file {file!r}. ' - f'Every line must be formatted as "name: color".') + f'Every line must be formatted as "name: color".' + ) # Categories for which we add *all* colors if cat == 'open' or i == 1: @@ -3116,8 +3165,10 @@ def register_fonts(): fonts[:] = [*fonts_proplot, *fonts_system] -def show_channels(*args, N=100, rgb=True, saturation=True, - minhue=0, maxsat=1000, axwidth=None, width=100): +def show_channels( + *args, N=100, rgb=True, saturation=True, + minhue=0, maxsat=1000, axwidth=None, width=100 +): """ Show how arbitrary colormap(s) vary with respect to the hue, chroma, luminance, HSL saturation, and HPL saturation channels, and optionally diff --git a/proplot/subplots.py b/proplot/subplots.py index de5325235..a1fab71a8 100644 --- a/proplot/subplots.py +++ b/proplot/subplots.py @@ -85,7 +85,8 @@ def __init__(self, objs, n=1, order='C'): """ if not all(isinstance(obj, axes.Axes) for obj in objs): raise ValueError( - f'Axes grid must be filled with Axes instances, got {objs!r}.') + f'Axes grid must be filled with Axes instances, got {objs!r}.' + ) super().__init__(objs) self._n = n self._order = order @@ -197,7 +198,8 @@ def __getattr__(self, attr): """ if not self: raise AttributeError( - f'Invalid attribute {attr!r}, axes grid {self!r} is empty.') + f'Invalid attribute {attr!r}, axes grid {self!r} is empty.' + ) objs = (*(getattr(ax, attr) for ax in self),) # may raise error # Objects @@ -313,13 +315,12 @@ def __init__(self, figure, nrows=1, ncols=1, **kwargs): self._nrows_active = nrows self._ncols_active = ncols wratios, hratios, kwargs = self._spaces_as_ratios(**kwargs) - super().__init__(self._nrows, self._ncols, - hspace=0, wspace=0, # replaced with "hidden" slots - width_ratios=wratios, - height_ratios=hratios, - figure=figure, - **kwargs, - ) + super().__init__( + self._nrows, self._ncols, + hspace=0, wspace=0, # replaced with "hidden" slots + width_ratios=wratios, height_ratios=hratios, + figure=figure, **kwargs + ) def __getitem__(self, key): """Magic obfuscation that renders `~matplotlib.gridspec.GridSpec` @@ -364,9 +365,10 @@ def _normalize(key, size): raise IndexError(f'Invalid index: {key} with size {size}.') def _spaces_as_ratios( - self, hspace=None, wspace=None, # spacing between axes - height_ratios=None, width_ratios=None, - **kwargs): + self, hspace=None, wspace=None, # spacing between axes + height_ratios=None, width_ratios=None, + **kwargs + ): """For keyword arg usage, see `GridSpec`.""" # Parse flexible input nrows, ncols = self.get_active_geometry() @@ -390,15 +392,18 @@ def _spaces_as_ratios( raise ValueError(f'Got {nrows} rows, but {len(hratios)} hratios.') if len(wratios) != ncols: raise ValueError( - f'Got {ncols} columns, but {len(wratios)} wratios.') + f'Got {ncols} columns, but {len(wratios)} wratios.' + ) if len(wspace) != ncols - 1: raise ValueError( f'Require {ncols-1} width spacings for {ncols} columns, ' - f'got {len(wspace)}.') + f'got {len(wspace)}.' + ) if len(hspace) != nrows - 1: raise ValueError( f'Require {nrows-1} height spacings for {nrows} rows, ' - f'got {len(hspace)}.') + f'got {len(hspace)}.' + ) # Assign spacing as ratios nrows, ncols = self.get_geometry() @@ -534,8 +539,9 @@ def _preprocess(self, *args, **kwargs): def _get_panelargs( - side, share=None, width=None, space=None, - filled=False, figure=False): + side, share=None, width=None, space=None, + filled=False, figure=False +): """Return default properties for new axes and figure panels.""" s = side[0] if s not in 'lrbt': @@ -617,27 +623,33 @@ def _subplots_geometry(**kwargs): if len(hratios) != nrows: raise ValueError( f'Expected {nrows} width ratios for {nrows} rows, ' - f'got {len(hratios)}.') + f'got {len(hratios)}.' + ) if len(wratios) != ncols: raise ValueError( f'Expected {ncols} width ratios for {ncols} columns, ' - f'got {len(wratios)}.') + f'got {len(wratios)}.' + ) if len(hspace) != nrows - 1: raise ValueError( f'Expected {nrows - 1} hspaces for {nrows} rows, ' - f'got {len(hspace)}.') + f'got {len(hspace)}.' + ) if len(wspace) != ncols - 1: raise ValueError( f'Expected {ncols - 1} wspaces for {ncols} columns, ' - f'got {len(wspace)}.') + f'got {len(wspace)}.' + ) if len(hpanels) != nrows: raise ValueError( f'Expected {nrows} hpanel toggles for {nrows} rows, ' - f'got {len(hpanels)}.') + f'got {len(hpanels)}.' + ) if len(wpanels) != ncols: raise ValueError( f'Expected {ncols} wpanel toggles for {ncols} columns, ' - f'got {len(wpanels)}.') + f'got {len(wpanels)}.' + ) # Get indices corresponding to main axes or main axes space slots idxs_ratios, idxs_space = [], [] @@ -680,7 +692,8 @@ def _subplots_geometry(**kwargs): if rwratio == 0 or rhratio == 0: raise RuntimeError( f'Something went wrong, got wratio={rwratio!r} ' - f'and hratio={rhratio!r} for reference axes.') + f'and hratio={rhratio!r} for reference axes.' + ) if np.iterable(aspect): aspect = aspect[0] / aspect[1] @@ -728,12 +741,14 @@ def _subplots_geometry(**kwargs): raise ValueError( f'Not enough room for axes (would have width {axwidth_all}). ' 'Try using tight=False, increasing figure width, or decreasing ' - "'left', 'right', or 'wspace' spaces.") + "'left', 'right', or 'wspace' spaces." + ) if axheight_all < 0: raise ValueError( f'Not enough room for axes (would have height {axheight_all}). ' 'Try using tight=False, increasing figure height, or decreasing ' - "'top', 'bottom', or 'hspace' spaces.") + "'top', 'bottom', or 'hspace' spaces." + ) # Reconstruct the ratios array with physical units for subplot slots # The panel slots are unchanged because panels have fixed widths @@ -784,14 +799,15 @@ class Figure(mfigure.Figure): panels is changed to accommodate subplot content. Figure dimensions may be automatically scaled to preserve subplot aspect ratios.""" - def __init__(self, - tight=None, - ref=1, pad=None, axpad=None, panelpad=None, - includepanels=False, - autoformat=True, - gridspec_kw=None, subplots_kw=None, subplots_orig_kw=None, - fallback_to_cm=None, - **kwargs): + def __init__( + self, tight=None, + ref=1, pad=None, axpad=None, panelpad=None, + includepanels=False, + autoformat=True, + gridspec_kw=None, subplots_kw=None, subplots_orig_kw=None, + fallback_to_cm=None, + **kwargs + ): """ Parameters ---------- @@ -853,7 +869,8 @@ def __init__(self, f'Ignoring tight_layout={tight_layout} and ' f'contrained_layout={constrained_layout}. ProPlot uses its ' 'own tight layout algorithm, activated by default or with ' - 'tight=True.') + 'tight=True.' + ) self._authorized_add_subplot = False self._is_preprocessing = False self._is_resizing = False @@ -913,7 +930,8 @@ def _add_axes_panel(self, ax, side, filled=False, **kwargs): idx2 = slice(col1, col2 + 1) gridspec_prev = self._gridspec_main gridspec = self._insert_row_column( - side, iratio, width, space, space_orig, figure=False) + side, iratio, width, space, space_orig, figure=False + ) if gridspec is not gridspec_prev: if s == 't': idx1 += 1 @@ -925,7 +943,8 @@ def _add_axes_panel(self, ax, side, filled=False, **kwargs): pax = self.add_subplot( gridspec[idx1, idx2], sharex=ax._sharex_level, sharey=ax._sharey_level, - projection='xy') + projection='xy', + ) getattr(ax, '_' + s + 'panels').append(pax) pax._panel_side = side pax._panel_share = share @@ -941,9 +960,10 @@ def _add_axes_panel(self, ax, side, filled=False, **kwargs): return pax - def _add_figure_panel(self, side, - span=None, row=None, col=None, rows=None, cols=None, - **kwargs): + def _add_figure_panel( + self, side, span=None, row=None, col=None, rows=None, cols=None, + **kwargs + ): """Adds figure panels. Also modifies the panel attribute stored on the figure to include these panels.""" # Interpret args and enforce sensible keyword args @@ -959,7 +979,8 @@ def _add_figure_panel(self, side, if value is not None: raise ValueError( f'Invalid keyword arg {key!r} for figure panel ' - f'on side {side!r}.') + f'on side {side!r}.' + ) span = _notNone(span, row, rows, None, names=('span', 'row', 'rows')) else: @@ -967,7 +988,8 @@ def _add_figure_panel(self, side, if value is not None: raise ValueError( f'Invalid keyword arg {key!r} for figure panel ' - f'on side {side!r}.') + f'on side {side!r}.' + ) span = _notNone(span, col, cols, None, names=('span', 'col', 'cols')) @@ -989,7 +1011,8 @@ def _add_figure_panel(self, side, if span[0] < 1 or span[1] > nalong: raise ValueError( f'Invalid coordinates in span={span!r}. Coordinates ' - f'must satisfy 1 <= c <= {nalong}.') + f'must satisfy 1 <= c <= {nalong}.' + ) start, stop = span[0] - 1, span[1] # zero-indexed # See if there is room for panel in current figure panels @@ -1020,7 +1043,7 @@ def _add_figure_panel(self, side, # Get gridspec and subplotspec indices idxs, = np.where(np.array(panels) == '') if len(idxs) != nalong: - raise RuntimeError('Wut?') + raise RuntimeError if s in 'lr': idx1 = slice(idxs[start], idxs[stop - 1] + 1) idx2 = max(iratio, 0) @@ -1153,7 +1176,6 @@ def _adjust_tight_layout(self, renderer, resize=True): if idx1.size > 1 or idx2.size > 2: _warn_proplot('This should never happen.') continue - # raise RuntimeError('This should never happen.') elif not idx1.size or not idx2.size: continue idx1, idx2 = idx1[0], idx2[0] @@ -1233,7 +1255,8 @@ def _align_axislabels(self, b=True): elif align: _warn_proplot( f'Aligning *x* and *y* axis labels required ' - f'matplotlib >=3.1.0') + f'matplotlib >=3.1.0' + ) if not span: continue # Get spanning label position @@ -1424,8 +1447,9 @@ def _get_renderer(self): return renderer def _insert_row_column( - self, side, idx, - ratio, space, space_orig, figure=False): + self, side, idx, + ratio, space, space_orig, figure=False, + ): """Helper function that "overwrites" the main figure gridspec to make room for a panel. The `side` is the panel side, the `idx` is the slot you want the panel to occupy, and the remaining args are the @@ -1518,7 +1542,8 @@ def _insert_row_column( igridspec._subplot_spec = subplotspec_new else: raise ValueError( - f'Unexpected GridSpecFromSubplotSpec nesting.') + f'Unexpected GridSpecFromSubplotSpec nesting.' + ) # Update parent or child position ax.update_params() ax.set_position(ax.figbox) @@ -1549,7 +1574,8 @@ def _update_labels(self, ax, side, labels, **kwargs): if len(labels) != len(axs): raise ValueError( f'Got {len(labels)} {s}labels, but there are {len(axs)} axes ' - 'along that side.') + 'along that side.' + ) for ax, label in zip(axs, labels): obj = getattr(ax, '_' + s + 'label') if label is not None and obj.get_text() != label: @@ -1568,10 +1594,12 @@ def add_subplot(self, *args, **kwargs): ax = super().add_subplot(*args, **kwargs) return ax - def colorbar(self, *args, - loc='r', width=None, space=None, - row=None, col=None, rows=None, cols=None, span=None, - **kwargs): + def colorbar( + self, *args, + loc='r', width=None, space=None, + row=None, col=None, rows=None, cols=None, span=None, + **kwargs + ): """ Draws a colorbar along the left, right, bottom, or top side of the figure, centered between the leftmost and rightmost (or @@ -1642,10 +1670,12 @@ def draw(self, renderer): self._align_labels(renderer) return super().draw(renderer) - def legend(self, *args, - loc='r', width=None, space=None, - row=None, col=None, rows=None, cols=None, span=None, - **kwargs): + def legend( + self, *args, + loc='r', width=None, space=None, + row=None, col=None, rows=None, cols=None, span=None, + **kwargs + ): """ Draws a legend along the left, right, bottom, or top side of the figure, centered between the leftmost and rightmost (or @@ -1739,8 +1769,9 @@ def set_size_inches(self, w, h=None, forward=True, auto=False): else: width, height = w, h if not all(np.isfinite(_) for _ in (width, height)): - raise ValueError('Figure size must be finite, not ' - f'({width}, {height}).') + raise ValueError( + 'Figure size must be finite, not ({width}, {height}).' + ) width_true, height_true = self.get_size_inches() width_trunc = int(self.bbox.width) / self.dpi height_trunc = int(self.bbox.height) / self.dpi @@ -1784,7 +1815,8 @@ def _journals(journal): raise ValueError( f'Unknown journal figure size specifier {journal!r}. ' 'Current options are: ' - + ', '.join(map(repr, JOURNAL_SPECS.keys()))) + + ', '.join(map(repr, JOURNAL_SPECS.keys())) + ) # Return width, and optionally also the height width, height = None, None try: @@ -1813,7 +1845,8 @@ def _axes_dict(naxs, value, kw=False, default=None): elif not all(nested): raise ValueError( 'Pass either of dictionary of key value pairs or ' - 'a dictionary of dictionaries of key value pairs.') + 'a dictionary of dictionaries of key value pairs.' + ) # Then *unfurl* keys that contain multiple axes numbers, i.e. are meant # to indicate properties for multiple axes at once kwargs = {} @@ -1835,27 +1868,29 @@ def _axes_dict(naxs, value, kw=False, default=None): if {*range(1, naxs + 1)} != {*kwargs.keys()}: raise ValueError( f'Have {naxs} axes, but {value} has properties for axes ' - + ', '.join(repr(i) for i in sorted(kwargs.keys())) + '.') + + ', '.join(repr(i) for i in sorted(kwargs.keys())) + '.' + ) return kwargs def subplots( - array=None, ncols=1, nrows=1, - ref=1, order='C', - aspect=1, figsize=None, - width=None, height=None, journal=None, - axwidth=None, axheight=None, - hspace=None, wspace=None, space=None, - hratios=None, wratios=None, - width_ratios=None, height_ratios=None, - flush=None, wflush=None, hflush=None, - left=None, bottom=None, right=None, top=None, - span=None, spanx=None, spany=None, - align=None, alignx=None, aligny=None, - share=None, sharex=None, sharey=None, - basemap=False, proj=None, projection=None, - proj_kw=None, projection_kw=None, - **kwargs): + array=None, ncols=1, nrows=1, + ref=1, order='C', + aspect=1, figsize=None, + width=None, height=None, journal=None, + axwidth=None, axheight=None, + hspace=None, wspace=None, space=None, + hratios=None, wratios=None, + width_ratios=None, height_ratios=None, + flush=None, wflush=None, hflush=None, + left=None, bottom=None, right=None, top=None, + span=None, spanx=None, spany=None, + align=None, alignx=None, aligny=None, + share=None, sharex=None, sharey=None, + basemap=False, proj=None, projection=None, + proj_kw=None, projection_kw=None, + **kwargs +): """ Analogous to `matplotlib.pyplot.subplots`, creates a figure with a single axes or arbitrary grids of axes, any of which can be map projections. @@ -2024,7 +2059,8 @@ def subplots( if order not in ('C', 'F'): # better error message raise ValueError( f'Invalid order {order!r}. Choose from "C" (row-major, default) ' - f'and "F" (column-major).') + f'and "F" (column-major).' + ) if array is None: array = np.arange(1, nrows * ncols + 1)[..., None] array = array.reshape((nrows, ncols), order=order) @@ -2035,12 +2071,15 @@ def subplots( # interpret as single row or column array = array[None, :] if order == 'C' else array[:, None] elif array.ndim != 2: - raise ValueError + raise ValueError( + 'array must be 1-2 dimensional, but got {array.ndim} dims' + ) array[array == None] = 0 # use zero for placeholder # noqa except (TypeError, ValueError): raise ValueError( f'Invalid subplot array {array!r}. ' - 'Must be 1d or 2d array of integers.') + 'Must be 1d or 2d array of integers.' + ) # Get other props nums = np.unique(array[array != 0]) naxs = len(nums) @@ -2048,11 +2087,13 @@ def subplots( raise ValueError( f'Invalid subplot array {array!r}. Numbers must span integers ' '1 to naxs (i.e. cannot skip over numbers), with 0 representing ' - 'empty spaces.') + 'empty spaces.' + ) if ref not in nums: raise ValueError( f'Invalid reference number {ref!r}. For array {array!r}, must be ' - 'one of {nums}.') + 'one of {nums}.' + ) nrows, ncols = array.shape # Figure out rows and columns "spanned" by each axes in list, for @@ -2065,7 +2106,8 @@ def subplots( '1 (sharing, but keep all tick labels), ' '2 (sharing, keep one set of tick labels), ' 'or 3 (sharing, keep one axis label and one set of tick labels)' - 'Got sharex={sharex} and sharey={sharey}.') + 'Got sharex={sharex} and sharey={sharey}.' + ) spanx = _notNone(spanx, span, 0 if sharex == 0 else None, rc['span']) spany = _notNone(spany, span, 0 if sharey == 0 else None, rc['span']) alignx = _notNone(alignx, align) @@ -2073,7 +2115,8 @@ def subplots( if (spanx and alignx) or (spany and aligny): _warn_proplot( f'The "alignx" and "aligny" args have no effect when ' - '"spanx" and "spany" are True.') + '"spanx" and "spany" are True.' + ) alignx = _notNone(alignx, rc['align']) aligny = _notNone(alignx, rc['align']) # Get some axes properties, where locations are sorted by axes id. @@ -2141,7 +2184,8 @@ def subplots( if value is not None: _warn_proplot( f'You specified both {spec} and {name}={value!r}. ' - f'Ignoring {name!r}.') + f'Ignoring {name!r}.' + ) # Standardized dimensions width, height = units(width), units(height) @@ -2157,13 +2201,15 @@ def subplots( if len(wspace) != ncols - 1: raise ValueError( f'Require {ncols-1} width spacings for {ncols} columns, ' - 'got {len(wspace)}.') + 'got {len(wspace)}.' + ) if len(hspace) == 1: hspace = np.repeat(hspace, (nrows - 1,)) if len(hspace) != nrows - 1: raise ValueError( f'Require {nrows-1} height spacings for {nrows} rows, ' - 'got {len(hspace)}.') + 'got {len(hspace)}.' + ) # Standardized user input ratios wratios = np.atleast_1d(_notNone(width_ratios, wratios, 1, names=('width_ratios', 'wratios'))) diff --git a/proplot/utils.py b/proplot/utils.py index fbc0f28db..572bc6d96 100644 --- a/proplot/utils.py +++ b/proplot/utils.py @@ -118,7 +118,8 @@ def _notNone(*args, names=None): if len(names) != len(args) - 1: raise ValueError( f'Need {len(args)+1} names for {len(args)} args, ' - f'but got {len(names)} names.') + f'but got {len(names)} names.' + ) names = [*names, ''] for name, arg in zip(names, args): if arg is not None: @@ -129,7 +130,8 @@ def _notNone(*args, names=None): if len(kwargs) > 1: _warn_proplot( f'Got conflicting or duplicate keyword args, ' - f'using the first one: {kwargs}') + f'using the first one: {kwargs}' + ) return first @@ -329,7 +331,8 @@ def units(value, units='in', axes=None, figure=None, width=True): except KeyError: raise ValueError( f'Invalid destination units {units!r}. Valid units are ' - + ', '.join(map(repr, unit_dict.keys())) + '.') + + ', '.join(map(repr, unit_dict.keys())) + '.' + ) # Convert units for each value in list result = [] @@ -341,12 +344,14 @@ def units(value, units='in', axes=None, figure=None, width=True): elif not isinstance(val, str): raise ValueError( f'Size spec must be string or number or list thereof. ' - 'Got {value!r}.') + 'Got {value!r}.' + ) regex = NUMBER.match(val) if not regex: raise ValueError( f'Invalid size spec {val!r}. Valid units are ' - + ', '.join(map(repr, unit_dict.keys())) + '.') + + ', '.join(map(repr, unit_dict.keys())) + '.' + ) number, _, units = regex.groups() # second group is exponential try: result.append( @@ -354,7 +359,8 @@ def units(value, units='in', axes=None, figure=None, width=True): except (KeyError, ValueError): raise ValueError( f'Invalid size spec {val!r}. Valid units are ' - + ', '.join(map(repr, unit_dict.keys())) + '.') + + ', '.join(map(repr, unit_dict.keys())) + '.' + ) if singleton: result = result[0] return result diff --git a/proplot/wrappers.py b/proplot/wrappers.py index 94faad1c1..223eb40b2 100644 --- a/proplot/wrappers.py +++ b/proplot/wrappers.py @@ -258,7 +258,8 @@ def standardize_1d(self, func, *args, **kwargs): x = _atleast_array(x) if x.ndim != 1: raise ValueError( - f'x coordinates must be 1-dimensional, but got {x.ndim}.') + f'x coordinates must be 1-dimensional, but got {x.ndim}.' + ) # Auto formatting xi = None # index version of 'x' @@ -434,7 +435,8 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): Zs.append(Z) if not all(Zs[0].shape == Z.shape for Z in Zs): raise ValueError( - f'Zs must be same shape, got shapes {[Z.shape for Z in Zs]}.') + f'Zs must be same shape, got shapes {[Z.shape for Z in Zs]}.' + ) # Retrieve coordinates if x is None and y is None: @@ -458,12 +460,14 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): if x.ndim != y.ndim: raise ValueError( f'x coordinates are {x.ndim}-dimensional, ' - f'but y coordinates are {y.ndim}-dimensional.') + f'but y coordinates are {y.ndim}-dimensional.' + ) for s, array in zip(('x', 'y'), (x, y)): if array.ndim not in (1, 2): raise ValueError( f'{s} coordinates are {array.ndim}-dimensional, ' - f'but must be 1 or 2-dimensional.') + f'but must be 1 or 2-dimensional.' + ) # Auto formatting kw = {} @@ -510,7 +514,8 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): for Z in Zs: if Z.ndim != 2: raise ValueError( - f'Input arrays must be 2D, instead got shape {Z.shape}.') + f'Input arrays must be 2D, instead got shape {Z.shape}.' + ) elif Z.shape[1] == xlen and Z.shape[0] == ylen: if all(z.ndim == 1 and z.size > 1 and z.dtype != 'object' for z in (x, y)): @@ -527,7 +532,8 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): raise ValueError( f'Input shapes x {x.shape} and y {y.shape} must match ' f'Z centers {Z.shape} or ' - f'Z borders {tuple(i+1 for i in Z.shape)}.') + f'Z borders {tuple(i+1 for i in Z.shape)}.' + ) # Optionally re-order # TODO: Double check this if order == 'F': @@ -536,7 +542,8 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): elif order != 'C': raise ValueError( f'Invalid order {order!r}. Choose from ' - '"C" (row-major, default) and "F" (column-major).') + '"C" (row-major, default) and "F" (column-major).' + ) # Enforce centers else: @@ -546,7 +553,8 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): for Z in Zs: if Z.ndim != 2: raise ValueError( - f'Input arrays must be 2D, instead got shape {Z.shape}.') + f'Input arrays must be 2D, instead got shape {Z.shape}.' + ) elif Z.shape[1] == xlen - 1 and Z.shape[0] == ylen - 1: if all(z.ndim == 1 and z.size > 1 and z.dtype != 'object' for z in (x, y)): @@ -565,7 +573,8 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): raise ValueError( f'Input shapes x {x.shape} and y {y.shape} ' f'must match Z centers {Z.shape} ' - f'or Z borders {tuple(i+1 for i in Z.shape)}.') + f'or Z borders {tuple(i+1 for i in Z.shape)}.' + ) # Optionally re-order # TODO: Double check this if order == 'F': @@ -574,7 +583,8 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): elif order != 'C': raise ValueError( f'Invalid order {order!r}. Choose from ' - '"C" (row-major, default) and "F" (column-major).') + '"C" (row-major, default) and "F" (column-major).' + ) # Cartopy projection axes if (getattr(self, 'name', '') == 'geo' @@ -634,7 +644,8 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): Z = ma.concatenate((Zq, Z, Zq), axis=1) else: raise ValueError( - 'Unexpected shape of longitude/latitude/data arrays.') + 'Unexpected shape of longitude/latitude/data arrays.' + ) iZs.append(Z) x, Zs = ix, iZs @@ -663,7 +674,8 @@ def _errorbar_values(data, idata, bardata=None, barrange=None, barstd=False): or err.shape[1] != idata.shape[-1]: raise ValueError( f'bardata must have shape (2, {idata.shape[-1]}), ' - f'but got {err.shape}.') + f'but got {err.shape}.' + ) elif barstd: err = np.array(idata) + np.std( data, axis=0)[None, :] * np.array(barrange)[:, None] @@ -675,16 +687,17 @@ def _errorbar_values(data, idata, bardata=None, barrange=None, barstd=False): def add_errorbars( - self, func, *args, - medians=False, means=False, - boxes=None, bars=None, - boxdata=None, bardata=None, - boxstd=False, barstd=False, - boxmarker=True, boxmarkercolor='white', - boxrange=(25, 75), barrange=(5, 95), boxcolor=None, barcolor=None, - boxlw=None, barlw=None, capsize=None, - boxzorder=3, barzorder=3, - **kwargs): + self, func, *args, + medians=False, means=False, + boxes=None, bars=None, + boxdata=None, bardata=None, + boxstd=False, barstd=False, + boxmarker=True, boxmarkercolor='white', + boxrange=(25, 75), barrange=(5, 95), boxcolor=None, barcolor=None, + boxlw=None, barlw=None, capsize=None, + boxzorder=3, barzorder=3, + **kwargs +): """ Wraps %(methods)s, adds support for drawing error bars. Includes options for interpreting columns of data as ranges, representing the mean @@ -765,7 +778,8 @@ def add_errorbars( if y.ndim != 2: raise ValueError( f'Need 2D data array for means=True or medians=True, ' - f'got {y.ndim}D array.') + f'got {y.ndim}D array.' + ) if means: iy = np.mean(y, axis=0) elif medians: @@ -850,16 +864,17 @@ def plot_wrapper(self, func, *args, cmap=None, values=None, **kwargs): def scatter_wrapper( - self, func, *args, - s=None, size=None, markersize=None, - c=None, color=None, markercolor=None, - smin=None, smax=None, - cmap=None, cmap_kw=None, vmin=None, vmax=None, norm=None, norm_kw=None, - lw=None, linewidth=None, linewidths=None, - markeredgewidth=None, markeredgewidths=None, - edgecolor=None, edgecolors=None, - markeredgecolor=None, markeredgecolors=None, - **kwargs): + self, func, *args, + s=None, size=None, markersize=None, + c=None, color=None, markercolor=None, + smin=None, smax=None, + cmap=None, cmap_kw=None, vmin=None, vmax=None, norm=None, norm_kw=None, + lw=None, linewidth=None, linewidths=None, + markeredgewidth=None, markeredgewidths=None, + edgecolor=None, edgecolors=None, + markeredgecolor=None, markeredgecolors=None, + **kwargs +): """ Wraps `~matplotlib.axes.Axes.scatter`, adds optional keyword args more consistent with the `~matplotlib.axes.Axes.plot` keywords. @@ -949,9 +964,11 @@ def scatter_wrapper( **kwargs) -def _fill_between_apply(self, func, *args, - negcolor='blue', poscolor='red', negpos=False, - **kwargs): +def _fill_between_apply( + self, func, *args, + negcolor='blue', poscolor='red', negpos=False, + **kwargs +): """Parse args and call function.""" # Allow common keyword usage x = 'y' if 'x' in func.__name__ else 'y' @@ -983,7 +1000,8 @@ def _fill_between_apply(self, func, *args, raise ValueError(f'When "negpos" is True, y must be 1-dimensional.') if kwargs.get('where', None) is not None: raise ValueError( - 'When "negpos" is True, you cannot set the "where" keyword.') + 'When "negpos" is True, you cannot set the "where" keyword.' + ) for i in range(2): kw = {**kwargs} kw.setdefault('color', negcolor if i == 0 else poscolor) @@ -1038,21 +1056,24 @@ def hist_wrapper(self, func, x, bins=None, **kwargs): return func(self, x, bins=bins, **kwargs) -def barh_wrapper(self, func, y=None, width=None, - height=0.8, left=None, **kwargs): +def barh_wrapper( + self, func, y=None, width=None, height=0.8, left=None, **kwargs +): """Wraps %(methods)s, usage is same as `bar_wrapper`.""" kwargs.setdefault('orientation', 'horizontal') if y is None and width is None: raise ValueError( - f'barh() requires at least 1 positional argument, got 0.') + f'barh() requires at least 1 positional argument, got 0.' + ) return self.bar(x=left, height=height, width=width, bottom=y, **kwargs) def bar_wrapper( - self, func, x=None, height=None, width=0.8, bottom=None, *, left=None, - vert=None, orientation='vertical', stacked=False, - lw=None, linewidth=0.7, edgecolor='k', - **kwargs): + self, func, x=None, height=None, width=0.8, bottom=None, *, left=None, + vert=None, orientation='vertical', stacked=False, + lw=None, linewidth=0.7, edgecolor='k', + **kwargs +): """ Wraps %(methods)s, permits bar stacking and bar grouping. @@ -1091,11 +1112,13 @@ def bar_wrapper( # sense do document here; figure out way to move it here? if left is not None: _warn_proplot( - f'The "left" keyword with bar() is deprecated. Use "x" instead.') + f'The "left" keyword with bar() is deprecated. Use "x" instead.' + ) x = left if x is None and height is None: raise ValueError( - f'bar() requires at least 1 positional argument, got 0.') + f'bar() requires at least 1 positional argument, got 0.' + ) elif height is None: x, height = None, x @@ -1110,17 +1133,18 @@ def bar_wrapper( def boxplot_wrapper( - self, func, *args, - color='k', fill=True, fillcolor=None, fillalpha=0.7, - lw=None, linewidth=0.7, orientation=None, - marker=None, markersize=None, - boxcolor=None, boxlw=None, - capcolor=None, caplw=None, - meancolor=None, meanlw=None, - mediancolor=None, medianlw=None, - whiskercolor=None, whiskerlw=None, - fliercolor=None, flierlw=None, - **kwargs): + self, func, *args, + color='k', fill=True, fillcolor=None, fillalpha=0.7, + lw=None, linewidth=0.7, orientation=None, + marker=None, markersize=None, + boxcolor=None, boxlw=None, + capcolor=None, caplw=None, + meancolor=None, meanlw=None, + mediancolor=None, medianlw=None, + whiskercolor=None, whiskerlw=None, + fliercolor=None, flierlw=None, + **kwargs +): """ Wraps %(methods)s, adds convenient keyword args. Fills the objects with a cycle color by default. @@ -1162,7 +1186,8 @@ def boxplot_wrapper( elif orientation != 'vertical': raise ValueError( 'Orientation must be "horizontal" or "vertical", ' - f'got {orientation!r}.') + f'got {orientation!r}.' + ) obj = func(self, *args, **kwargs) if not args: return obj @@ -1207,10 +1232,11 @@ def boxplot_wrapper( def violinplot_wrapper( - self, func, *args, - lw=None, linewidth=0.7, fillcolor=None, edgecolor='k', - fillalpha=0.7, orientation=None, - **kwargs): + self, func, *args, + lw=None, linewidth=0.7, fillcolor=None, edgecolor='k', + fillalpha=0.7, orientation=None, + **kwargs +): """ Wraps %(methods)s, adds convenient keyword args. Makes the style shown in right plot of `this matplotlib example \ @@ -1245,7 +1271,8 @@ def violinplot_wrapper( elif orientation != 'vertical': raise ValueError( 'Orientation must be "horizontal" or "vertical", ' - f'got {orientation!r}.') + f'got {orientation!r}.' + ) # Sanitize input lw = _notNone(lw, linewidth, None, names=('lw', 'linewidth')) @@ -1295,11 +1322,12 @@ def _get_transform(self, transform): def text_wrapper( - self, func, - x=0, y=0, text='', transform='data', - fontfamily=None, fontname=None, fontsize=None, size=None, - border=False, bordercolor='w', invert=False, lw=None, linewidth=2, - **kwargs): + self, func, + x=0, y=0, text='', transform='data', + fontfamily=None, fontname=None, fontsize=None, size=None, + border=False, bordercolor='w', invert=False, lw=None, linewidth=2, + **kwargs +): """ Wraps %(methods)s, and enables specifying `tranform` with a string name and adds feature for drawing borders around text. @@ -1356,7 +1384,8 @@ def text_wrapper( else: _warn_proplot( f'Font {fontname!r} unavailable. Available fonts are ' - + ', '.join(map(repr, styletools.fonts)) + '.') + + ', '.join(map(repr, styletools.fonts)) + '.' + ) size = _notNone(fontsize, size, None, names=('fontsize', 'size')) if size is not None: kwargs['fontsize'] = units(size, 'pt') @@ -1382,13 +1411,14 @@ def text_wrapper( def cycle_changer( - self, func, *args, - cycle=None, cycle_kw=None, - markers=None, linestyles=None, - label=None, labels=None, values=None, - legend=None, legend_kw=None, - colorbar=None, colorbar_kw=None, - **kwargs): + self, func, *args, + cycle=None, cycle_kw=None, + markers=None, linestyles=None, + label=None, labels=None, values=None, + legend=None, legend_kw=None, + colorbar=None, colorbar_kw=None, + **kwargs +): """ Wraps methods that use the property cycler (%(methods)s), adds features for controlling colors in the property cycler and drawing @@ -1587,7 +1617,8 @@ def cycle_changer( if len(labels) != ncols: raise ValueError( f'Got {ncols} columns in data array, ' - f'but {len(labels)} labels.') + f'but {len(labels)} labels.' + ) label = labels[i] values, label_leg = _auto_label(iy, axis=1) if label_leg and label is None: @@ -1660,16 +1691,17 @@ def cycle_changer( def cmap_changer( - self, func, *args, cmap=None, cmap_kw=None, - extend='neither', norm=None, norm_kw=None, - N=None, levels=None, values=None, centers=None, vmin=None, vmax=None, - locator=None, symmetric=False, locator_kw=None, - edgefix=None, labels=False, labels_kw=None, fmt=None, precision=2, - colorbar=False, colorbar_kw=None, - lw=None, linewidth=None, linewidths=None, - ls=None, linestyle=None, linestyles=None, - color=None, colors=None, edgecolor=None, edgecolors=None, - **kwargs): + self, func, *args, cmap=None, cmap_kw=None, + extend='neither', norm=None, norm_kw=None, + N=None, levels=None, values=None, centers=None, vmin=None, vmax=None, + locator=None, symmetric=False, locator_kw=None, + edgefix=None, labels=False, labels_kw=None, fmt=None, precision=2, + colorbar=False, colorbar_kw=None, + lw=None, linewidth=None, linewidths=None, + ls=None, linestyle=None, linestyles=None, + color=None, colors=None, edgecolor=None, edgecolors=None, + **kwargs +): """ Wraps methods that take a `cmap` argument (%(methods)s), adds several new keyword args and features. @@ -1846,7 +1878,8 @@ def cmap_changer( kwargs[style_kw[key]] = value else: raise ValueError( - f'Unknown keyword arg {key!r} for function {name!r}.') + f'Unknown keyword arg {key!r} for function {name!r}.' + ) # Check input for key, val in (('levels', levels), ('values', values)): if not np.iterable(val): @@ -1856,7 +1889,8 @@ def cmap_changer( if len(val) < 2 or any(np.diff(val) <= 0): raise ValueError( f'{key!r} must be monotonically increasing and ' - f'at least length 2, got {val}.') + f'at least length 2, got {val}.' + ) # Get level edges from level centers if values is not None: @@ -1884,7 +1918,8 @@ def cmap_changer( else: raise ValueError( f'Unexpected input values={values!r}. ' - 'Must be integer or list of numbers.') + 'Must be integer or list of numbers.' + ) # Input colormap, for methods that accept a colormap and normalizer # contour, tricontour, i.e. not a method where cmap is optional @@ -1897,7 +1932,8 @@ def cmap_changer( if cyclic and extend != 'neither': _warn_proplot( f'Cyclic colormap requires extend="neither". ' - 'Overriding user input extend={extend!r}.') + 'Overriding user input extend={extend!r}.' + ) extend = 'neither' kwargs['cmap'] = cmap @@ -2106,7 +2142,8 @@ def cmap_changer( if not isinstance(loc, str): raise ValueError( f'Invalid on-the-fly location {loc!r}. ' - f'Must be a preset location. See Axes.colorbar.') + f'Must be a preset location. See Axes.colorbar.' + ) if 'label' not in colorbar_kw and self.figure._auto_format: _, label = _auto_label(args[-1]) # last one is data, we assume if label: @@ -2120,12 +2157,13 @@ def cmap_changer( def legend_wrapper( - self, handles=None, labels=None, ncol=None, ncols=None, - center=None, order='C', loc=None, label=None, title=None, - fontsize=None, fontweight=None, fontcolor=None, - color=None, marker=None, lw=None, linewidth=None, - dashes=None, linestyle=None, markersize=None, frameon=None, frame=None, - **kwargs): + self, handles=None, labels=None, ncol=None, ncols=None, + center=None, order='C', loc=None, label=None, title=None, + fontsize=None, fontweight=None, fontcolor=None, + color=None, marker=None, lw=None, linewidth=None, + dashes=None, linestyle=None, markersize=None, frameon=None, frame=None, + **kwargs +): """ Wraps `~proplot.axes.Axes` `~proplot.axes.Axes.legend` and `~proplot.subplots.Figure` `~proplot.subplots.Figure.legend`, adds some @@ -2198,7 +2236,8 @@ def legend_wrapper( if order not in ('F', 'C'): raise ValueError( f'Invalid order {order!r}. Choose from ' - '"C" (row-major, default) and "F" (column-major).') + '"C" (row-major, default) and "F" (column-major).' + ) # may still be None, wait till later ncol = _notNone(ncols, ncol, None, names=('ncols', 'ncol')) title = _notNone(label, title, None, names=('label', 'title')) @@ -2224,7 +2263,8 @@ def legend_wrapper( if self._filled: raise ValueError( 'You must pass a handles list for panel axes ' - '"filled" with a legend.') + '"filled" with a legend.' + ) else: # ignores artists with labels '_nolegend_' handles, labels_default = self.get_legend_handles_labels() @@ -2234,7 +2274,8 @@ def legend_wrapper( raise ValueError( 'No labeled artists found. To generate a legend without ' 'providing the artists explicitly, pass label="label" in ' - 'your plotting commands.') + 'your plotting commands.' + ) if not np.iterable(handles): # e.g. a mappable object handles = [handles] if labels is not None and (not np.iterable( @@ -2249,7 +2290,8 @@ def legend_wrapper( for handle in handles) and len(handles) > 1: raise ValueError( f'Handles must be objects with get_facecolor attributes or ' - 'a single mappable object from which we can draw colors.') + 'a single mappable object from which we can draw colors.' + ) # Build pairs of handles and labels # This allows alternative workflow where user specifies labels when @@ -2264,35 +2306,41 @@ def legend_wrapper( for ihandle in handle: if not hasattr(ihandle, 'get_label'): raise ValueError( - f'Object {ihandle} must have "get_label" method.') + f'Object {ihandle} must have "get_label" method.' + ) ipairs.append((ihandle, ihandle.get_label())) pairs.append(ipairs) else: if not hasattr(handle, 'get_label'): raise ValueError( - f'Object {handle} must have "get_label" method.') + f'Object {handle} must have "get_label" method.' + ) pairs.append((handle, handle.get_label())) else: if len(labels) != len(handles): raise ValueError( - f'Got {len(labels)} labels, but {len(handles)} handles.') + f'Got {len(labels)} labels, but {len(handles)} handles.' + ) for label, handle in zip(labels, handles): if list_of_lists: ipairs = [] if not np.iterable(label) or isinstance(label, str): raise ValueError( - f'Got list of lists of handles, but list of labels.') + f'Got list of lists of handles, but list of labels.' + ) elif len(label) != len(handle): raise ValueError( f'Got {len(label)} labels in sublist, ' - f'but {len(handle)} handles.') + f'but {len(handle)} handles.' + ) for ilabel, ihandle in zip(label, handle): ipairs.append((ihandle, ilabel)) pairs.append(ipairs) else: if not isinstance(label, str) and np.iterable(label): raise ValueError( - f'Got list of lists of labels, but list of handles.') + f'Got list of lists of labels, but list of handles.' + ) pairs.append((handle, label)) # Manage pairs in context of 'center' option @@ -2301,7 +2349,8 @@ def legend_wrapper( elif center and list_of_lists and ncol is not None: _warn_proplot( 'Detected list of *lists* of legend handles. ' - 'Ignoring user input property "ncol".') + 'Ignoring user input property "ncol".' + ) elif not center and list_of_lists: # standardize format based on input list_of_lists = False # no longer is list of lists pairs = [pair for ipairs in pairs for pair in ipairs] @@ -2349,9 +2398,11 @@ def legend_wrapper( if prop is not None: overridden.append(override) if overridden: - _warn_proplot(f'For centered-row legends, must override ' - 'user input properties ' - ', '.join(map(repr, overridden)) + '.') + _warn_proplot( + f'For centered-row legends, must override ' + 'user input properties ' + + ', '.join(map(repr, overridden)) + '.' + ) # Determine space we want sub-legend to occupy as fraction of height # NOTE: Empirical testing shows spacing fudge factor necessary to # exactly replicate the spacing of standard aligned legends. @@ -2368,16 +2419,19 @@ def legend_wrapper( raise NotImplementedError( f'When center=True, ProPlot vertically stacks successive ' 'single-row legends. Column-major (order="F") ordering ' - 'is un-supported.') + 'is un-supported.' + ) loc = _notNone(loc, 'upper center') if not isinstance(loc, str): raise ValueError( f'Invalid location {loc!r} for legend with center=True. ' - 'Must be a location *string*.') + 'Must be a location *string*.' + ) elif loc == 'best': _warn_proplot( 'For centered-row legends, cannot use "best" location. ' - 'Defaulting to "upper center".') + 'Using "upper center" instead.' + ) for i, ipairs in enumerate(pairs): if i == 1: kwargs.pop('title', None) @@ -2485,21 +2539,22 @@ def legend_wrapper( def colorbar_wrapper( - self, mappable, values=None, - extend=None, extendsize=None, - title=None, label=None, - grid=None, tickminor=None, - tickloc=None, ticklocation=None, - locator=None, ticks=None, maxn=None, maxn_minor=None, - minorlocator=None, minorticks=None, - locator_kw=None, minorlocator_kw=None, - formatter=None, ticklabels=None, formatter_kw=None, - norm=None, norm_kw=None, # normalizer to use when passing colors/lines - orientation='horizontal', - edgecolor=None, linewidth=None, - labelsize=None, labelweight=None, labelcolor=None, - ticklabelsize=None, ticklabelweight=None, ticklabelcolor=None, - **kwargs): + self, mappable, values=None, + extend=None, extendsize=None, + title=None, label=None, + grid=None, tickminor=None, + tickloc=None, ticklocation=None, + locator=None, ticks=None, maxn=None, maxn_minor=None, + minorlocator=None, minorticks=None, + locator_kw=None, minorlocator_kw=None, + formatter=None, ticklabels=None, formatter_kw=None, + norm=None, norm_kw=None, # normalizer to use when passing colors/lines + orientation='horizontal', + edgecolor=None, linewidth=None, + labelsize=None, labelweight=None, labelcolor=None, + ticklabelsize=None, ticklabelweight=None, ticklabelcolor=None, + **kwargs +): """ Wraps `~proplot.axes.Axes` `~proplot.axes.Axes.colorbar` and `~proplot.subplots.Figure` `~proplot.subplots.Figure.colorbar`, adds some @@ -2625,12 +2680,18 @@ def colorbar_wrapper( # Parse flexible input label = _notNone(title, label, None, names=('title', 'label')) locator = _notNone(ticks, locator, None, names=('ticks', 'locator')) - formatter = _notNone(ticklabels, formatter, 'auto', - names=('ticklabels', 'formatter')) - minorlocator = _notNone(minorticks, minorlocator, - None, names=('minorticks', 'minorlocator')) - ticklocation = _notNone(tickloc, ticklocation, None, - names=('tickloc', 'ticklocation')) + formatter = _notNone( + ticklabels, formatter, 'auto', + names=('ticklabels', 'formatter') + ) + minorlocator = _notNone( + minorticks, minorlocator, None, + names=('minorticks', 'minorlocator') + ) + ticklocation = _notNone( + tickloc, ticklocation, None, + names=('tickloc', 'ticklocation') + ) # Colorbar kwargs # WARNING: PathCollection scatter objects have an extend method! @@ -2717,7 +2778,8 @@ def colorbar_wrapper( raise ValueError( 'Input mappable must be a matplotlib artist, ' 'list of objects, list of colors, or colormap. ' - f'Got {mappable!r}.') + f'Got {mappable!r}.' + ) if values is None: if np.iterable(mappable) and not isinstance( mappable, str): # e.g. list of colors @@ -2732,7 +2794,8 @@ def colorbar_wrapper( if np.iterable(mappable) and len(values) != len(mappable): raise ValueError( f'Passed {len(values)} values, but only {len(mappable)} ' - f'objects or colors.') + f'objects or colors.' + ) import warnings with warnings.catch_warnings(): warnings.simplefilter('ignore') @@ -2819,7 +2882,8 @@ def colorbar_wrapper( elif not hasattr(cb, '_ticker'): _warn_proplot( 'Matplotlib colorbar API has changed. ' - 'Cannot use custom minor tick locator.') + 'Cannot use custom minor tick locator.' + ) if tickminor: cb.minorticks_on() else: @@ -2850,7 +2914,8 @@ def colorbar_wrapper( cmap._init() if any(cmap._lut[:-1, 3] < 1): _warn_proplot( - f'Using manual alpha-blending for {cmap.name!r} colorbar solids.') + f'Using manual alpha-blending for {cmap.name!r} colorbar solids.' + ) # Generate "secret" copy of the colormap! lut = cmap._lut.copy() cmap = mcolors.Colormap('_colorbar_fix', N=cmap.N) @@ -2885,6 +2950,7 @@ def colorbar_wrapper( kw['color'] = edgecolor if linewidth: kw['width'] = linewidth + print('hi!', kw) axis.set_tick_params(which=which, **kw) axis.set_ticks_position(ticklocation) From 578aa8b4be55f469153fc97658aa8ac3f2a2a3f1 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 22:13:23 -0700 Subject: [PATCH 29/33] Better install instructions --- INSTALL.rst | 2 +- README.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index 14aa865d5..f92503266 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -1,7 +1,7 @@ Installation ============ -ProPlot can be installed with `pip `__ or `conda `__: +ProPlot is published on `PyPi `__ and `conda-forge `__. It can be installed with ``pip`` or ``conda`` as follows: .. code-block:: bash diff --git a/README.rst b/README.rst index 7b6b83ef5..520ddf07c 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ A comprehensive, easy-to-use `matplotlib `__ wrapper fo Installation ============ -ProPlot can be installed with `pip `__ or `conda `__: +ProPlot is published on `PyPi `__ and `conda-forge `__. It can be installed with ``pip`` or ``conda`` as follows: .. code-block:: bash From 7acffb6a510716f73d5a8c9055b10cab5d29e369 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 22:21:50 -0700 Subject: [PATCH 30/33] Update changelog --- CHANGELOG.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 382ba694e..245167da3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,10 +19,6 @@ ProPlot v0.5.0 (2020-##-##) =========================== .. rubric:: Deprecated -- Remove ``subplots.innerspace``, ``subplots.titlespace``, - ``subplots.xlabspace``, and ``subplots.ylabspace`` spacing arguments, - automatically calculate default non-tight spacing using `~proplot.subplots._get_space` - based on current tick lengths, label sizes, etc. - Rename `basemap_defaults` to `~proplot.projs.basemap_kwargs` and `cartopy_projs` to `~proplot.projs.cartopy_names` (:commit:`431a06ce`). @@ -53,7 +49,12 @@ ProPlot v0.4.0 (2020-##-##) - Remove redundant `~proplot.rctools.use_fonts`, use ``rcParams['sans-serif']`` precedence instead (:pr:`95`). -- `~proplot.axes.Axes.dualx` and `~proplot.axes.Axes.dualx` no longer accept "scale-spec" arguments, must be a function, two functions, or an axis scale instance (:pr:`96`). +- `~proplot.axes.Axes.dualx` and `~proplot.axes.Axes.dualx` no longer accept "scale-spec" arguments. + Must be a function, two functions, or an axis scale instance (:pr:`96`). +- Remove ``subplots.innerspace``, ``subplots.titlespace``, + ``subplots.xlabspace``, and ``subplots.ylabspace`` spacing arguments, + automatically calculate default non-tight spacing using `~proplot.subplots._get_space` + based on current tick lengths, label sizes, etc. .. rubric:: Features From 8da19f507c84f0a9f39044c82e67383081d24144 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Mon, 6 Jan 2020 22:29:13 -0700 Subject: [PATCH 31/33] Remove outdated cache=False ref, remove debug statement --- proplot/wrappers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/proplot/wrappers.py b/proplot/wrappers.py index 223eb40b2..2cf72684a 100644 --- a/proplot/wrappers.py +++ b/proplot/wrappers.py @@ -2465,7 +2465,7 @@ def legend_wrapper( 'edgecolor': 'axes.edgecolor', 'facecolor': 'axes.facecolor', 'alpha': 'legend.framealpha', - }, cache=False) + }) for key in (*outline,): if key != 'linewidth': if kwargs.get(key, None): @@ -2950,7 +2950,6 @@ def colorbar_wrapper( kw['color'] = edgecolor if linewidth: kw['width'] = linewidth - print('hi!', kw) axis.set_tick_params(which=which, **kw) axis.set_ticks_position(ticklocation) From 3ca9a0245e9f2f429dac2c0f40ed88cbcb1b7a95 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Tue, 7 Jan 2020 00:15:57 -0700 Subject: [PATCH 32/33] Minor BasemapAxes bugfix --- proplot/axes.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/proplot/axes.py b/proplot/axes.py index 6997b4cde..92fa22be9 100644 --- a/proplot/axes.py +++ b/proplot/axes.py @@ -3684,7 +3684,7 @@ def _format_apply( p.set_clip_on(False) # so edges denoting boundary aren't cut off self._map_boundary = p else: - self.patch.update(edgecolor='none', **kw_face) + self.patch.update({**kw_face, 'edgecolor': 'none'}) for spine in self.spines.values(): spine.update(kw_edge) @@ -3718,7 +3718,8 @@ def _format_apply( latlines = _notNone(latlines, self._latlines_values) latarray = _notNone(latarray, self._latlines_labels, [0] * 4) p = self.projection.drawparallels( - latlines, latmax=ilatmax, labels=latarray, ax=self) + latlines, latmax=ilatmax, labels=latarray, ax=self + ) for pi in p.values(): # returns dict, where each one is tuple # Tried passing clip_on to the below, but it does nothing # Must set for lines created after the fact @@ -3739,7 +3740,8 @@ def _format_apply( lonlines = _notNone(lonlines, self._lonlines_values) lonarray = _notNone(lonarray, self._lonlines_labels, [0] * 4) p = self.projection.drawmeridians( - lonlines, latmax=ilatmax, labels=lonarray, ax=self) + lonlines, latmax=ilatmax, labels=lonarray, ax=self, + ) for pi in p.values(): for obj in [i for j in pi for i in j]: if isinstance(obj, mtext.Text): From 22af8f91de4424e3bcca25f974c44697653dfc6b Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Tue, 7 Jan 2020 00:17:09 -0700 Subject: [PATCH 33/33] Notebook examples formatting improvements --- docs/1dplots.ipynb | 157 ++++++++++++------ docs/2dplots.ipynb | 97 +++++++---- docs/axis.ipynb | 314 ++++++++++++++++++++++++----------- docs/basics.ipynb | 58 +++++-- docs/colorbars_legends.ipynb | 54 ++++-- docs/colormaps.ipynb | 122 +++++++++----- docs/colors_fonts.ipynb | 20 ++- docs/cycles.ipynb | 26 ++- docs/insets_panels.ipynb | 41 +++-- docs/projection.ipynb | 225 ++++++++++++++++--------- docs/subplots.ipynb | 91 ++++++---- 11 files changed, 809 insertions(+), 396 deletions(-) diff --git a/docs/1dplots.ipynb b/docs/1dplots.ipynb index 1f1252606..46a2152b3 100644 --- a/docs/1dplots.ipynb +++ b/docs/1dplots.ipynb @@ -58,6 +58,7 @@ "import xarray as xr\n", "import numpy as np\n", "import pandas as pd\n", + "\n", "# DataArray\n", "state = np.random.RandomState(51423)\n", "data = np.sin(np.linspace(0, 2*np.pi, 20))[:, None] \\\n", @@ -66,6 +67,7 @@ " 'x': xr.DataArray(np.linspace(0, 1, 20), dims=('x',), attrs={'long_name': 'distance', 'units': 'km'}),\n", " 'cat': xr.DataArray(np.arange(0, 80, 10), dims=('cat',), attrs={'long_name': 'parameter', 'units': 'K'})\n", "}, name='position series')\n", + "\n", "# DataFrame\n", "ts = pd.date_range('1/1/2000', periods=20)\n", "data = (np.cos(np.linspace(0, 2*np.pi, 20))**4)[:, None] + state.rand(20, 5)**2\n", @@ -84,9 +86,11 @@ "import proplot as plot\n", "f, axs = plot.subplots(ncols=2, axwidth=2.2, share=0)\n", "axs.format(suptitle='Automatic subplot formatting')\n", + "\n", "# Plot DataArray\n", "cycle = plot.Cycle(plot.shade('light blue', 0.4), fade=90, space='hpl')\n", "axs[0].plot(da, cycle=cycle, lw=3, colorbar='ul', colorbar_kw={'locator': 20})\n", + "\n", "# Plot Dataframe\n", "cycle = plot.Cycle(plot.shade('jade', 0.4), fade=90, space='hpl')\n", "axs[1].plot(df, cycle=cycle, lw=3, legend='uc')" @@ -122,28 +126,36 @@ "state = np.random.RandomState(51423)\n", "data = state.rand(20, 8).cumsum(axis=0).cumsum(axis=1)[:, ::-1] \\\n", " + 20*state.normal(size=(20, 8)) + 30\n", - "f, axs = plot.subplots(nrows=3, aspect=1.5, axwidth=3,\n", - " share=0, hratios=(2, 1, 1))\n", + "f, axs = plot.subplots(\n", + " nrows=3, aspect=1.5, axwidth=3,\n", + " share=0, hratios=(2, 1, 1)\n", + ")\n", "axs.format(suptitle='Error bars with various plotting commands')\n", + "axs[1:].format(xlabel='column number', xticks=1, xgrid=False)\n", + "\n", "# Asking add_errorbars to calculate bars\n", "ax = axs[0]\n", "obj = ax.barh(data, color='red orange', means=True)\n", "ax.format(title='Column statistics')\n", + "ax.format(ylabel='column number', title='Bar plot', ygrid=False)\n", + "\n", "# Showing a standard deviation range instead of percentile range\n", "ax = axs[1]\n", - "ax.scatter(data, color='k', marker='x', markersize=50, barcolor='gray5',\n", - " medians=True, barstd=True, barrange=(-1, 1), barzorder=0, boxes=False, capsize=2)\n", + "ax.scatter(\n", + " data, color='k', marker='x', markersize=50, barcolor='gray5',\n", + " medians=True, barstd=True, barrange=(-1, 1), barzorder=0, boxes=False, capsize=2\n", + ")\n", + "ax.format(title='Scatter plot')\n", + "\n", "# Supplying error bar data manually\n", "ax = axs[2]\n", "boxdata = np.percentile(data, (25, 75), axis=0)\n", "bardata = np.percentile(data, (5, 95), axis=0)\n", - "ax.plot(data.mean(axis=0), boxes=False, marker='o', markersize=5,\n", - " edgecolor='k', color='cerulean', boxdata=boxdata, bardata=bardata)\n", - "# Formatting\n", - "axs[0].format(ylabel='column number', title='Bar plot', ygrid=False)\n", - "axs[1].format(title='Scatter plot')\n", - "axs[2].format(title='Line plot')\n", - "axs[1:].format(xlabel='column number', xticks=1, xgrid=False)\n", + "ax.plot(\n", + " data.mean(axis=0), boxes=False, marker='o', markersize=5,\n", + " edgecolor='k', color='cerulean', boxdata=boxdata, bardata=bardata\n", + ")\n", + "ax.format(title='Line plot')\n", "plot.rc.reset()" ] }, @@ -177,17 +189,28 @@ "f, axs = plot.subplots(nrows=2, aspect=2, axwidth=3.5, share=0, hratios=(3, 2))\n", "state = np.random.RandomState(51423)\n", "data = state.rand(5, 5).cumsum(axis=0).cumsum(axis=1)[:, ::-1]\n", - "data = pd.DataFrame(data,\n", - " columns=pd.Index(np.arange(1, 6), name='column'),\n", - " index=pd.Index(['a', 'b', 'c', 'd', 'e'], name='row idx'))\n", + "data = pd.DataFrame(\n", + " data, columns=pd.Index(np.arange(1, 6), name='column'),\n", + " index=pd.Index(['a', 'b', 'c', 'd', 'e'], name='row idx')\n", + ")\n", + "\n", + "# Side-by-side bars\n", "ax = axs[0]\n", - "obj = ax.bar(data, cycle='Reds', colorbar='ul',\n", - " edgecolor='red9', colorbar_kw={'frameon': False})\n", - "ax.format(xlocator=1, xminorlocator=0.5, ytickminor=False,\n", - " title='Side-by-side', suptitle='Bar plot wrapper demo')\n", + "obj = ax.bar(\n", + " data, cycle='Reds', colorbar='ul',\n", + " edgecolor='red9', colorbar_kw={'frameon': False}\n", + ")\n", + "ax.format(\n", + " xlocator=1, xminorlocator=0.5, ytickminor=False,\n", + " title='Side-by-side', suptitle='Bar plot wrapper demo'\n", + ")\n", + "\n", + "# Stacked bars\n", "ax = axs[1]\n", - "obj = ax.barh(data.iloc[::-1, :], cycle='Blues',\n", - " legend='ur', edgecolor='blue9', stacked=True)\n", + "obj = ax.barh(\n", + " data.iloc[::-1, :], cycle='Blues',\n", + " legend='ur', edgecolor='blue9', stacked=True\n", + ")\n", "ax.format(title='Stacked')\n", "axs.format(grid=False)\n", "plot.rc.reset()" @@ -225,18 +248,22 @@ "state = np.random.RandomState(51423)\n", "data = state.rand(5, 3).cumsum(axis=0)\n", "cycle = ('gray3', 'gray5', 'gray7')\n", - "# 2D arrays\n", + "\n", + "# Overlaid and stacked area patches\n", "ax = axs[0]\n", - "ax.areax(np.arange(5), data, data + state.rand(5)[:, None], cycle=cycle, alpha=0.5,\n", - " legend='uc', legend_kw={'center': True, 'ncols': 2, 'labels': ['z', 'y', 'qqqq']},\n", - " )\n", + "ax.area(\n", + " np.arange(5), data, data + state.rand(5)[:, None], cycle=cycle, alpha=0.5,\n", + " legend='uc', legend_kw={'center': True, 'ncols': 2, 'labels': ['z', 'y', 'qqqq']},\n", + ")\n", "ax.format(title='Fill between columns')\n", "ax = axs[1]\n", - "ax.area(np.arange(5), data, stacked=True, cycle=cycle, alpha=0.8,\n", - " legend='ul', legend_kw={'center': True, 'ncols': 2, 'labels': ['z', 'y', 'qqqq']},\n", - " )\n", + "ax.area(\n", + " np.arange(5), data, stacked=True, cycle=cycle, alpha=0.8,\n", + " legend='ul', legend_kw={'center': True, 'ncols': 2, 'labels': ['z', 'y', 'qqqq']},\n", + ")\n", "ax.format(title='Stack between columns')\n", - "# Positive and negative colors\n", + "\n", + "# Positive and negative color area patches\n", "ax = axs[2]\n", "data = 5*(state.rand(20)-0.5)\n", "ax.area(data, negpos=True, negcolor='blue7', poscolor='red7')\n", @@ -274,19 +301,30 @@ "state = np.random.RandomState(51423)\n", "f, axs = plot.subplots(ncols=2)\n", "data = state.normal(size=(N, 5)) + 2*(state.rand(N, 5)-0.5)*np.arange(5)\n", - "data = pd.DataFrame(data, columns=pd.Index(\n", - " ['a', 'b', 'c', 'd', 'e'], name='xlabel'))\n", + "data = pd.DataFrame(\n", + " data,\n", + " columns=pd.Index(['a', 'b', 'c', 'd', 'e'], name='xlabel')\n", + ")\n", + "axs.format(\n", + " ymargin=0.1, xmargin=0.1, grid=False,\n", + " suptitle='Boxes and violins demo'\n", + ")\n", + "\n", + "# Box plots\n", "ax = axs[0]\n", - "# , boxprops={'color':'C0'})#, labels=data.columns)\n", - "obj1 = ax.boxplot(data, lw=0.7, marker='x', fillcolor='gray5',\n", - " medianlw=1, mediancolor='k')\n", + "obj1 = ax.boxplot(\n", + " data, lw=0.7, marker='x', fillcolor='gray5',\n", + " medianlw=1, mediancolor='k'\n", + ")\n", "ax.format(title='Box plots', titleloc='uc')\n", + "\n", + "# Violin plots\n", "ax = axs[1]\n", - "obj2 = ax.violinplot(data, lw=0.7, fillcolor='gray7',\n", - " points=500, bw_method=0.3, means=True)\n", - "ax.format(title='Violin plots', titleloc='uc')\n", - "axs.format(ymargin=0.1, xmargin=0.1, grid=False,\n", - " suptitle='Boxes and violins demo')" + "obj2 = ax.violinplot(\n", + " data, lw=0.7, fillcolor='gray7',\n", + " points=500, bw_method=0.3, means=True\n", + ")\n", + "ax.format(title='Violin plots', titleloc='uc')" ] }, { @@ -316,19 +354,25 @@ "N = 50\n", "cmap = 'IceFire'\n", "values = np.linspace(-N/2, N/2, N)\n", - "f, axs = plot.subplots(share=0, ncols=2, wratios=(2, 1),\n", - " axwidth='6cm', aspect=(2, 1))\n", + "f, axs = plot.subplots(\n", + " share=0, ncols=2, wratios=(2, 1),\n", + " axwidth='7cm', aspect=(2, 1)\n", + ")\n", "axs.format(suptitle='Parametric plots demo')\n", - "# Smooth gradations\n", + "\n", + "# Parametric line with smooth gradations\n", "ax = axs[0]\n", "state = np.random.RandomState(51423)\n", "m = ax.plot((state.rand(N) - 0.5).cumsum(), state.rand(N),\n", " cmap=cmap, values=values, lw=7, extend='both')\n", - "ax.format(xlabel='xlabel', ylabel='ylabel',\n", - " title='Line with smooth gradations')\n", + "ax.format(\n", + " xlabel='xlabel', ylabel='ylabel',\n", + " title='Line with smooth gradations'\n", + ")\n", "ax.format(xlim=(-1, 5), ylim=(-0.2, 1.2))\n", "ax.colorbar(m, loc='b', label='parametric coordinate', locator=5)\n", - "# Step gradations\n", + "\n", + "# Parametric line with stepped gradations\n", "N = 12\n", "ax = axs[1]\n", "values = np.linspace(-N/2, N/2, N + 1)\n", @@ -337,8 +381,10 @@ "x = radii*np.cos(1.4*angles)\n", "y = radii*np.sin(1.4*angles)\n", "m = ax.plot(x, y, values=values, linewidth=15, interp=False, cmap=cmap)\n", - "ax.format(xlim=(-1, 1), ylim=(-1, 1), title='Step gradations',\n", - " xlabel='cosine angle', ylabel='sine angle')\n", + "ax.format(\n", + " xlim=(-1, 1), ylim=(-1, 1), title='Step gradations',\n", + " xlabel='cosine angle', ylabel='sine angle'\n", + ")\n", "ax.colorbar(m, loc='b', maxn=10, label=f'parametric coordinate')" ] }, @@ -374,17 +420,24 @@ "x = (state.rand(20)-0).cumsum()\n", "data = (state.rand(20, 4)-0.5).cumsum(axis=0)\n", "data = pd.DataFrame(data, columns=pd.Index(['a', 'b', 'c', 'd'], name='label'))\n", - "# Scatter demo\n", + "\n", + "# Scatter plot with property cycler\n", "ax = axs[0]\n", "ax.format(title='Extra prop cycle properties', suptitle='Scatter plot demo')\n", - "obj = ax.scatter(x, data, legend='ul', cycle='warm', legend_kw={'ncols': 2},\n", - " cycle_kw={'marker': ['x', 'o', 'x', 'o'], 'markersize': [5, 10, 20, 30]})\n", + "obj = ax.scatter(\n", + " x, data, legend='ul', cycle='warm', legend_kw={'ncols': 2},\n", + " cycle_kw={'marker': ['x', 'o', 'x', 'o'], 'markersize': [5, 10, 20, 30]}\n", + ")\n", + "\n", + "# Scatter plot with colormap\n", "ax = axs[1]\n", "ax.format(title='Scatter plot with cmap')\n", "data = state.rand(2, 100)\n", - "obj = ax.scatter(*data, color=data.sum(axis=0), size=state.rand(100), smin=3, smax=30,\n", - " marker='o', cmap='plum', colorbar='lr', vmin=0, vmax=2,\n", - " colorbar_kw={'label': 'label', 'locator':0.5})\n", + "obj = ax.scatter(\n", + " *data, color=data.sum(axis=0), size=state.rand(100), smin=3, smax=30,\n", + " marker='o', cmap='plum', colorbar='lr', vmin=0, vmax=2,\n", + " colorbar_kw={'label': 'label', 'locator':0.5}\n", + ")\n", "axs.format(xlabel='xlabel', ylabel='ylabel')" ] } diff --git a/docs/2dplots.ipynb b/docs/2dplots.ipynb index 21b5b65c1..91e753a2f 100644 --- a/docs/2dplots.ipynb +++ b/docs/2dplots.ipynb @@ -59,6 +59,7 @@ "import numpy as np\n", "import pandas as pd\n", "from string import ascii_lowercase\n", + "\n", "# DataArray\n", "state = np.random.RandomState(51423)\n", "data = 50*(np.sin(np.linspace(0, 2*np.pi, 20) + 0)**2) * \\\n", @@ -67,6 +68,7 @@ " 'plev': xr.DataArray(np.linspace(1000, 0, 20), dims=('plev',), attrs={'long_name': 'pressure', 'units': 'hPa'}),\n", " 'lat': xr.DataArray(np.linspace(-90, 90, 20), dims=('lat',), attrs={'units': 'degN'}),\n", "}, name='u', attrs={'long_name': 'zonal wind', 'units': 'm/s'})\n", + "\n", "# DataFrame\n", "data = state.rand(20, 20)\n", "df = pd.DataFrame(data.cumsum(axis=0).cumsum(\n", @@ -85,10 +87,13 @@ "import proplot as plot\n", "f, axs = plot.subplots(nrows=2, axwidth=2.2, share=0)\n", "axs.format(collabels=['Automatic subplot formatting'])\n", + "\n", "# Plot DataArray\n", - "axs[0].contourf(da, cmap='Greens', cmap_kw={\n", - " 'left': 0.05}, colorbar='l', linewidth=0.7, color='gray7')\n", + "axs[0].contourf(\n", + " da, cmap='Greens', cmap_kw={'left': 0.05}, colorbar='l', linewidth=0.7, color='gray7'\n", + ")\n", "axs[0].format(yreverse=True)\n", + "\n", "# Plot DataFrame\n", "axs[1].contourf(df, cmap='Blues', colorbar='r', linewidth=0.7, color='gray7')\n", "axs[1].format(xtickminor=False)" @@ -125,13 +130,14 @@ "source": [ "import proplot as plot\n", "import numpy as np\n", + "\n", + "# Pcolor plot with and without distinct levels\n", "f, axs = plot.subplots(ncols=2, axwidth=2)\n", - "cmap = 'spectral'\n", "state = np.random.RandomState(51423)\n", "data = (state.normal(0, 1, size=(33, 33))).cumsum(axis=0).cumsum(axis=1)\n", "axs.format(suptitle='Pcolor with levels demo')\n", "for ax, n, mode, side in zip(axs, (200, 10), ('Ambiguous', 'Discernible'), 'lr'):\n", - " ax.pcolor(data, cmap=cmap, N=n, symmetric=True, colorbar=side)\n", + " ax.pcolor(data, cmap='spectral', N=n, symmetric=True, colorbar=side)\n", " ax.format(title=f'{mode} level boundaries', yformatter='null')" ] }, @@ -143,21 +149,29 @@ "source": [ "import proplot as plot\n", "import numpy as np\n", - "f, axs = plot.subplots([[0, 0, 1, 1, 0, 0], [2, 3, 3, 4, 4, 5]],\n", - " wratios=(1.5, 0.5, 1, 1, 0.5, 1.5), axwidth=1.7, ref=1, right='2em')\n", + "f, axs = plot.subplots(\n", + " [[0, 0, 1, 1, 0, 0], [2, 3, 3, 4, 4, 5]],\n", + " wratios=(1.5, 0.5, 1, 1, 0.5, 1.5), axwidth=1.7, ref=1, right='2em'\n", + ")\n", "axs.format(suptitle='Demo of colorbar color-range standardization')\n", "levels = plot.arange(0, 360, 45)\n", "state = np.random.RandomState(51423)\n", "data = (20*(state.rand(20, 20) - 0.4).cumsum(axis=0).cumsum(axis=1)) % 360\n", - "# Show cyclic colorbar with distinct end colors\n", + "\n", + "# Cyclic colorbar with distinct end colors\n", "ax = axs[0]\n", - "ax.pcolormesh(data, levels=levels, cmap='phase', extend='neither',\n", - " colorbar='b', colorbar_kw={'locator': 90})\n", + "ax.pcolormesh(\n", + " data, levels=levels, cmap='phase', extend='neither',\n", + " colorbar='b', colorbar_kw={'locator': 90}\n", + ")\n", "ax.format(title='cyclic colormap\\nwith distinct end colors')\n", - "# Show colorbars with different extend values\n", + "\n", + "# Colorbars with different extend values\n", "for ax, extend in zip(axs[1:], ('min', 'max', 'neither', 'both')):\n", - " ax.pcolormesh(data[:, :10], levels=levels, cmap='oxy',\n", - " extend=extend, colorbar='b', colorbar_kw={'locator': 90})\n", + " ax.pcolormesh(\n", + " data[:, :10], levels=levels, cmap='oxy',\n", + " extend=extend, colorbar='b', colorbar_kw={'locator': 90}\n", + " )\n", " ax.format(title=f'extend={extend!r}')" ] }, @@ -190,16 +204,20 @@ "source": [ "import proplot as plot\n", "import numpy as np\n", + "\n", "# Linear segmented norm\n", - "f, axs = plot.subplots(ncols=2, axwidth=2.5, aspect=1.5)\n", "state = np.random.RandomState(51423)\n", "data = 10**(2*state.rand(20, 20).cumsum(axis=0)/7)\n", + "f, axs = plot.subplots(ncols=2, axwidth=2.5, aspect=1.5)\n", "ticks = [5, 10, 20, 50, 100, 200, 500, 1000]\n", "for i, (norm, title) in enumerate(zip(('linear', 'segments'), ('Linear normalizer', 'LinearSegmentedNorm'))):\n", - " m = axs[i].contourf(data, levels=ticks, extend='both',\n", - " cmap='Mako', norm=norm, colorbar='b')\n", + " m = axs[i].contourf(\n", + " data, levels=ticks, extend='both',\n", + " cmap='Mako', norm=norm, colorbar='b'\n", + " )\n", " axs[i].format(title=title)\n", "axs.format(suptitle='Level normalizers demo')\n", + "\n", "# Midpoint norm\n", "data1 = (state.rand(20, 20) - 0.43).cumsum(axis=0)\n", "data2 = (state.rand(20, 20) - 0.57).cumsum(axis=0)\n", @@ -237,27 +255,36 @@ "import proplot as plot\n", "import pandas as pd\n", "import numpy as np\n", - "# Heatmap with labels\n", "f, axs = plot.subplots([[1, 1, 2, 2], [0, 3, 3, 0]],\n", " axwidth=2, share=1, span=False, hratios=(1, 0.9))\n", "state = np.random.RandomState(51423)\n", "data = state.rand(6, 6)\n", "data = pd.DataFrame(data, index=pd.Index(['a', 'b', 'c', 'd', 'e', 'f']))\n", "axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Labels demo')\n", + "\n", + "# Heatmap with labeled boxes\n", "ax = axs[0]\n", - "m = ax.heatmap(data, cmap='rocket', labels=True,\n", - " precision=2, labels_kw={'weight': 'bold'})\n", + "m = ax.heatmap(\n", + " data, cmap='rocket', labels=True,\n", + " precision=2, labels_kw={'weight': 'bold'}\n", + ")\n", "ax.format(title='Heatmap plot with labels')\n", + "\n", "# Filled contours with labels\n", "ax = axs[1]\n", - "m = ax.contourf(data.cumsum(axis=0), labels=True,\n", - " cmap='rocket', labels_kw={'weight': 'bold'})\n", + "m = ax.contourf(\n", + " data.cumsum(axis=0), labels=True,\n", + " cmap='rocket', labels_kw={'weight': 'bold'}\n", + ")\n", "ax.format(title='Filled contour plot with labels')\n", - "# Simple contour plot\n", + "\n", + "# Line contours with labels\n", "ax = axs[2]\n", - "ax.contour(data.cumsum(axis=1) - 2, color='gray8',\n", - " labels=True, lw=2, labels_kw={'weight': 'bold'})\n", - "ax.format(title='Contour plot with labels')" + "ax.contour(\n", + " data.cumsum(axis=1) - 2, color='gray8',\n", + " labels=True, lw=2, labels_kw={'weight': 'bold'}\n", + ")\n", + "ax.format(title='Line contour plot with labels')" ] }, { @@ -285,19 +312,27 @@ "import proplot as plot\n", "import numpy as np\n", "import pandas as pd\n", - "f, ax = plot.subplots(axwidth=4)\n", + "\n", + "# Covariance data\n", "state = np.random.RandomState(51423)\n", "data = state.normal(size=(10, 10)).cumsum(axis=0)\n", "data = (data - data.mean(axis=0)) / data.std(axis=0)\n", "data = (data.T @ data) / data.shape[0]\n", "data[np.tril_indices(data.shape[0], -1)] = np.nan # fill half with empty boxes\n", "data = pd.DataFrame(data, columns=list('abcdefghij'), index=list('abcdefghij'))\n", - "m = ax.heatmap(data, cmap='ColdHot', vmin=-1, vmax=1, N=100,\n", - " lw=0.5, edgecolor='k', labels=True, labels_kw={'weight': 'bold'},\n", - " clip_on=False) # turn off clipping so box edges are not cut in half\n", - "ax.format(suptitle='Heatmap demo', title='Pseudo covariance matrix', alpha=0, linewidth=0,\n", - " ytickmajorpad=4, # the ytick.major.pad rc setting; add extra space\n", - " xloc='top', yloc='right', yreverse=True, ticklabelweight='bold')" + "\n", + "# Covariance matrix plot\n", + "f, ax = plot.subplots(axwidth=4)\n", + "m = ax.heatmap(\n", + " data, cmap='ColdHot', vmin=-1, vmax=1, N=100,\n", + " lw=0.5, edgecolor='k', labels=True, labels_kw={'weight': 'bold'},\n", + " clip_on=False, # turn off clipping so box edges are not cut in half\n", + ")\n", + "ax.format(\n", + " suptitle='Heatmap demo', title='Pseudo covariance matrix', alpha=0, linewidth=0,\n", + " xloc='top', yloc='right', yreverse=True, ticklabelweight='bold',\n", + " ytickmajorpad=4, # the ytick.major.pad rc setting; adds extra space\n", + ")" ] } ], diff --git a/docs/axis.ipynb b/docs/axis.ipynb index f98289c3a..ce1786bd9 100644 --- a/docs/axis.ipynb +++ b/docs/axis.ipynb @@ -44,26 +44,47 @@ "source": [ "import proplot as plot\n", "import numpy as np\n", - "# shade makes it a bit brighter, multiplies luminance channel by this much!\n", + "state = np.random.RandomState(51423)\n", "plot.rc.facecolor = plot.shade('powder blue', 1.15)\n", - "plot.rc.update(linewidth=1, small=10, large=12,\n", - " color='dark blue', suptitlecolor='dark blue')\n", - "f, axs = plot.subplots(nrows=5, axwidth=5, aspect=(8, 1), share=0, hspace=0.3)\n", + "plot.rc.update(\n", + " linewidth=1,\n", + " small=10, large=12,\n", + " color='dark blue', suptitlecolor='dark blue',\n", + " titleloc='upper center', titlecolor='dark blue', titleborder=False,\n", + ")\n", + "f, axs = plot.subplots(nrows=5, axwidth=5, aspect=(8, 1), share=0)\n", "axs.format(suptitle='Tick locators demo')\n", + "\n", "# Manual locations\n", - "axs[0].format(xlim=(0, 200), xminorlocator=10, xlocator=30)\n", - "axs[1].format(xlim=(0, 10), xminorlocator=0.1,\n", - " xlocator=[0, 0.3, 0.8, 1.6, 4.4, 8, 8.8, 10])\n", - "# Locator classes\n", - "state = np.random.RandomState(51423)\n", + "axs[0].format(\n", + " xlim=(0, 200), xminorlocator=10, xlocator=30,\n", + " title='MultipleLocator'\n", + ")\n", + "axs[1].format(\n", + " xlim=(0, 10), xminorlocator=0.1,\n", + " xlocator=[0, 0.3, 0.8, 1.6, 4.4, 8, 8.8, 10],\n", + " title='FixedLocator',\n", + ")\n", + "\n", "# Approx number of ticks you want, but not exact locations\n", - "axs[3].format(xlim=(1, 10), xlocator=('maxn', 20))\n", + "axs[3].format(\n", + " xlim=(1, 10), xlocator=('maxn', 20),\n", + " title='MaxNLocator',\n", + ")\n", + "\n", "# Log minor locator, automatically applied for log scale plots\n", - "axs[2].format(xlim=(1, 100), xlocator='log', xminorlocator='logminor')\n", + "axs[2].format(\n", + " xlim=(1, 100), xlocator='log', xminorlocator='logminor',\n", + " title='LogLocator',\n", + ")\n", + "\n", "# Index locator, only draws ticks where data is plotted\n", "axs[4].plot(np.arange(10) - 5, state.rand(10), alpha=0)\n", - "axs[4].format(xlim=(0, 6), ylim=(0, 1), xlocator='index',\n", - " xformatter=[r'$\\alpha$', r'$\\beta$', r'$\\gamma$', r'$\\delta$', r'$\\epsilon$'])\n", + "axs[4].format(\n", + " xlim=(0, 6), ylim=(0, 1), xlocator='index',\n", + " xformatter=[r'$\\alpha$', r'$\\beta$', r'$\\gamma$', r'$\\delta$', r'$\\epsilon$'],\n", + " title='IndexLocator',\n", + ")\n", "plot.rc.reset()" ] }, @@ -101,27 +122,44 @@ "source": [ "import proplot as plot\n", "import numpy as np\n", - "plot.rc.update(linewidth=1.2, small=10, large=12, facecolor='gray8', figurefacecolor='gray8',\n", - " suptitlecolor='w', gridcolor='w', color='w')\n", - "f, axs = plot.subplots(nrows=6, axwidth=5, aspect=(8, 1), share=0, hspace=0.3)\n", + "plot.rc.update(\n", + " linewidth=1.2, small=10, large=12, facecolor='gray8', figurefacecolor='gray8',\n", + " suptitlecolor='w', gridcolor='w', color='w',\n", + " titleloc='upper center', titlecolor='w', titleborder=False,\n", + ")\n", + "f, axs = plot.subplots(nrows=6, axwidth=5, aspect=(8, 1), share=0)\n", + "\n", "# Fraction formatters\n", - "axs[0].format(xlim=(0, 3*np.pi),\n", - " xlocator=plot.arange(0, 4, 0.25) * np.pi,\n", - " xformatter='pi')\n", - "axs[1].format(xlim=(0, 2*np.e),\n", - " xlocator=plot.arange(0, 2, 0.5) * np.e,\n", - " xticklabels='e')\n", + "axs[0].format(\n", + " xlim=(0, 3*np.pi), xlocator=plot.arange(0, 4, 0.25) * np.pi,\n", + " xformatter='pi', title='FracFormatter',\n", + ")\n", + "axs[1].format(\n", + " xlim=(0, 2*np.e), xlocator=plot.arange(0, 2, 0.5) * np.e,\n", + " xticklabels='e', title='FracFormatter',\n", + ")\n", + "\n", "# Geographic formatter\n", - "axs[2].format(xlim=(-90, 90), xlocator=plot.arange(-90, 90, 30),\n", - " xformatter='deglat')\n", + "axs[2].format(\n", + " xlim=(-90, 90), xlocator=plot.arange(-90, 90, 30),\n", + " xformatter='deglat', title='Geographic preset'\n", + ")\n", + "\n", "# User input labels\n", - "axs[3].format(xlim=(-1.01, 1), xlocator=0.5,\n", - " xticklabels=['a', 'b', 'c', 'd', 'e'])\n", + "axs[3].format(\n", + " xlim=(-1.01, 1), xlocator=0.5,\n", + " xticklabels=['a', 'b', 'c', 'd', 'e'], title='FixedFormatter',\n", + ")\n", + "\n", "# Custom style labels\n", - "axs[4].format(xlim=(0, 0.001), xlocator=0.0001,\n", - " xformatter='%.E')\n", - "axs[5].format(xlim=(0, 100), xtickminor=False,\n", - " xlocator=20, xformatter='{x:.1f}')\n", + "axs[4].format(\n", + " xlim=(0, 0.001), xlocator=0.0001,\n", + " xformatter='%.E', title='FormatStrFormatter',\n", + ")\n", + "axs[5].format(\n", + " xlim=(0, 100), xtickminor=False, xlocator=20,\n", + " xformatter='{x:.1f}', title='StrMethodFormatter',\n", + ")\n", "axs.format(ylocator='null', suptitle='Tick formatters demo')\n", "plot.rc.reset()" ] @@ -137,16 +175,23 @@ "plot.rc.small = plot.rc.large = 11\n", "locator = [0, 0.25, 0.5, 0.75, 1]\n", "f, axs = plot.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], axwidth=1.5, share=0)\n", + "\n", "# Formatter comparison\n", - "axs[0].format(xformatter='scalar', yformatter='scalar',\n", - " title='Matplotlib formatter')\n", + "axs[0].format(\n", + " xformatter='scalar', yformatter='scalar', title='Matplotlib formatter'\n", + ")\n", "axs[1].format(yticklabelloc='both', title='ProPlot formatter')\n", "axs[:2].format(xlocator=locator, ylocator=locator)\n", - "# Limited tick range\n", - "axs[2].format(title='Omitting tick labels', ticklen=5, xlim=(0, 5), ylim=(0, 5),\n", - " xtickrange=(0, 2), ytickrange=(0, 2), xlocator=1, ylocator=1)\n", - "axs.format(ytickloc='both', yticklabelloc='both',\n", - " titlepad='0.5em', suptitle='Default formatters demo')\n", + "\n", + "# Limiting the formatter tick range\n", + "axs[2].format(\n", + " title='Omitting tick labels', ticklen=5, xlim=(0, 5), ylim=(0, 5),\n", + " xtickrange=(0, 2), ytickrange=(0, 2), xlocator=1, ylocator=1\n", + ")\n", + "axs.format(\n", + " ytickloc='both', yticklabelloc='both',\n", + " titlepad='0.5em', suptitle='Default formatters demo'\n", + ")\n", "plot.rc.reset()" ] }, @@ -174,29 +219,49 @@ "source": [ "import proplot as plot\n", "import numpy as np\n", - "plot.rc.update(linewidth=1.2, small=10, large=12, ticklenratio=0.7)\n", - "plot.rc.update(figurefacecolor='w', facecolor=plot.shade('C0', 2.7))\n", - "f, axs = plot.subplots(nrows=6, axwidth=6, aspect=(8, 1), share=0)\n", - "# Default date locator enabled if you plot datetime data or set datetime limits\n", - "axs[0].format(xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-02')),\n", - " xrotation=0)\n", + "plot.rc.update(\n", + " linewidth=1.2, small=10, large=12, ticklenratio=0.7,\n", + " figurefacecolor='w', facecolor=plot.shade('C0', 2.7),\n", + " titleloc='upper center', titleborder=False,\n", + ")\n", + "f, axs = plot.subplots(nrows=5, axwidth=6, aspect=(8, 1), share=0)\n", + "axs[:4].format(xrotation=0) # no rotation for these examples\n", + "\n", + "# Default date locator\n", + "# This is enabled if you plot datetime data or set datetime limits\n", + "axs[0].format(\n", + " xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-02')),\n", + " title='Auto date locator and formatter'\n", + ")\n", + "\n", "# Concise date formatter introduced in matplotlib 3.1\n", - "axs[1].format(xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-01')),\n", - " xformatter='concise', xrotation=0)\n", + "axs[1].format(\n", + " xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-01')),\n", + " xformatter='concise', title='Concise date formatter',\n", + ")\n", + "\n", "# Minor ticks every year, major every 10 years\n", - "axs[2].format(xlim=(np.datetime64('2000-01-01'), np.datetime64('2050-01-01')), xrotation=0,\n", - " xlocator=('year', 10), xformatter='\\'%y')\n", + "axs[2].format(\n", + " xlim=(np.datetime64('2000-01-01'), np.datetime64('2050-01-01')),\n", + " xlocator=('year', 10), xformatter='\\'%y', title='Ticks every N units',\n", + ")\n", + "\n", "# Minor ticks every 10 minutes, major every 2 minutes\n", - "axs[3].format(xlim=(np.datetime64('2000-01-01T00:00:00'), np.datetime64('2000-01-01T12:00:00')), xrotation=0,\n", - " xlocator=('hour', range(0, 24, 2)), xminorlocator=('minute', range(0, 60, 10)), xformatter='T%H:%M:%S')\n", + "axs[3].format(\n", + " xlim=(np.datetime64('2000-01-01T00:00:00'), np.datetime64('2000-01-01T12:00:00')),\n", + " xlocator=('hour', range(0, 24, 2)), xminorlocator=('minute', range(0, 60, 10)),\n", + " xformatter='T%H:%M:%S', title='Ticks at specific intervals',\n", + ")\n", + "\n", "# Month and year labels, with default tick label rotation\n", - "axs[4].format(xlim=(np.datetime64('2000-01-01'), np.datetime64('2008-01-01')),\n", - " xlocator='year', xminorlocator='month', xformatter='%b %Y') # minor ticks every month\n", - "axs[5].format(xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-01')),\n", - " xgridminor=True, xgrid=False,\n", - " xlocator='month', xminorlocator='weekday', xformatter='%B') # minor ticks every Monday, major every month\n", - "axs.format(ylocator='null',\n", - " suptitle='Datetime locators and formatters demo')\n", + "axs[4].format(\n", + " xlim=(np.datetime64('2000-01-01'), np.datetime64('2008-01-01')),\n", + " xlocator='year', xminorlocator='month', # minor ticks every month\n", + " xformatter='%b %Y', title='Ticks with default rotation',\n", + ")\n", + "axs.format(\n", + " ylocator='null', suptitle='Datetime locators and formatters demo'\n", + ")\n", "plot.rc.reset()" ] }, @@ -234,18 +299,22 @@ "import numpy as np\n", "N = 200\n", "lw = 3\n", - "plot.rc.update({'linewidth': 1, 'ticklabelweight': 'bold',\n", - " 'axeslabelweight': 'bold'})\n", + "plot.rc.update({\n", + " 'linewidth': 1, 'ticklabelweight': 'bold', 'axeslabelweight': 'bold'\n", + "})\n", "f, axs = plot.subplots(ncols=2, nrows=2, axwidth=1.8, share=0)\n", "axs.format(suptitle='Axis scales demo', ytickminor=True)\n", + "\n", "# Linear and log scales\n", "axs[0].format(yscale='linear', ylabel='linear scale')\n", "axs[1].format(ylim=(1e-3, 1e3), yscale='log', ylabel='log scale')\n", "axs[:2].plot(np.linspace(0, 1, N), np.linspace(0, 1000, N), lw=lw)\n", + "\n", "# Symlog scale\n", "ax = axs[2]\n", "ax.format(yscale='symlog', ylabel='symlog scale')\n", "ax.plot(np.linspace(0, 1, N), np.linspace(-1000, 1000, N), lw=lw)\n", + "\n", "# Logit scale\n", "ax = axs[3]\n", "ax.format(yscale='logit', ylabel='logit scale')\n", @@ -279,12 +348,16 @@ "import numpy as np\n", "f, axs = plot.subplots(width=6, nrows=4, aspect=(5, 1), sharex=False)\n", "ax = axs[0]\n", + "\n", + "# Sample data\n", "x = np.linspace(0, 4*np.pi, 100)\n", "dy = np.linspace(-1, 1, 5)\n", "y1 = np.sin(x)\n", "y2 = np.cos(x)\n", "state = np.random.RandomState(51423)\n", "data = state.rand(len(dy)-1, len(x)-1)\n", + "\n", + "# Loop through various cutoff scale options\n", "titles = ('Zoom out of left', 'Zoom into left', 'Discrete jump', 'Fast jump')\n", "args = [\n", " (np.pi, 3), # speed up\n", @@ -300,10 +373,12 @@ " ax.pcolormesh(x, dy, data, cmap='grays', cmap_kw={'right': 0.8})\n", " for y, color in zip((y1, y2), ('coral', 'sky blue')):\n", " ax.plot(x, y, lw=4, color=color)\n", - " ax.format(xscale=('cutoff', *iargs), title=title,\n", - " xlim=(0, 4*np.pi), ylabel='wave amplitude',\n", - " xformatter='pi', xlocator=locator,\n", - " xtickminor=False, xgrid=True, ygrid=False, suptitle='Cutoff axis scales demo')" + " ax.format(\n", + " xscale=('cutoff', *iargs), title=title,\n", + " xlim=(0, 4*np.pi), ylabel='wave amplitude',\n", + " xformatter='pi', xlocator=locator,\n", + " xtickminor=False, xgrid=True, ygrid=False, suptitle='Cutoff axis scales demo'\n", + " )" ] }, { @@ -315,26 +390,35 @@ "import proplot as plot\n", "import numpy as np\n", "plot.rc.reset()\n", - "f, axs = plot.subplots(nrows=2, ncols=3, axwidth=1.6, share=0, order='F')\n", - "axs.format(collabels=['Power scales', 'Exponential scales', 'Cartographic scales'],\n", - " suptitle='Additional axis scales demo')\n", + "f, axs = plot.subplots(nrows=2, ncols=3, axwidth=1.7, share=0, order='F')\n", + "axs.format(\n", + " collabels=['Power scales', 'Exponential scales', 'Cartographic scales'],\n", + " suptitle='Additional axis scales demo'\n", + ")\n", "x = np.linspace(0, 1, 50)\n", "y = 10*x\n", "state = np.random.RandomState(51423)\n", - "data = state.rand(len(y)-1, len(x)-1)\n", + "data = state.rand(len(y) - 1, len(x) - 1)\n", + "\n", "# Power scales\n", "colors = ('coral', 'sky blue')\n", "for ax, power, color in zip(axs[:2], (2, 1/4), colors):\n", " ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8})\n", " ax.plot(x, y, lw=4, color=color)\n", - " ax.format(ylim=(0.1, 10), yscale=('power', power),\n", - " title=f'$x^{{{power}}}$')\n", + " ax.format(\n", + " ylim=(0.1, 10), yscale=('power', power),\n", + " title=f'$x^{{{power}}}$'\n", + " )\n", + " \n", "# Exp scales\n", "for ax, a, c, color in zip(axs[2:4], (np.e, 2), (0.5, -1), colors):\n", " ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8})\n", " ax.plot(x, y, lw=4, color=color)\n", - " ax.format(ylim=(0.1, 10), yscale=('exp', a, c),\n", - " title=f'${(a,\"e\")[a==np.e]}^{{{(c,\"-\")[c==-1]}x}}$')\n", + " ax.format(\n", + " ylim=(0.1, 10), yscale=('exp', a, c),\n", + " title=f'${(a,\"e\")[a==np.e]}^{{{(c,\"-\")[c==-1]}x}}$'\n", + " )\n", + " \n", "# Geographic scales\n", "n = 20\n", "x = np.linspace(-180, 180, n)\n", @@ -344,10 +428,11 @@ "for ax, scale, color in zip(axs[4:], ('sine', 'mercator'), ('coral', 'sky blue')):\n", " ax.plot(x, y, '-', color=color, lw=4)\n", " ax.pcolormesh(x, y2, data, cmap='grays', cmap_kw={'right': 0.8})\n", - " ax.format(title=scale.title() + ' y-axis', yscale=scale,\n", - " ytickloc='left',\n", - " yformatter='deg', grid=False, ylocator=20,\n", - " xscale='linear', xlim=None, ylim=(-85, 85))" + " ax.format(\n", + " title=scale.title() + ' y-axis', yscale=scale, ytickloc='left',\n", + " yformatter='deg', grid=False, ylocator=20,\n", + " xscale='linear', xlim=None, ylim=(-85, 85)\n", + " )" ] }, { @@ -378,26 +463,39 @@ "plot.rc.update({'grid.alpha': 0.4, 'linewidth': 1, 'grid.linewidth': 1})\n", "c1 = plot.shade('cerulean', 0.5)\n", "c2 = plot.shade('red', 0.5)\n", - "f, axs = plot.subplots([[1, 1, 2, 2], [0, 3, 3, 0]],\n", - " share=0, aspect=2.2, axwidth=3)\n", - "axs.format(suptitle='Duplicate axes with custom transformations',\n", - " xcolor=c1, gridcolor=c1,\n", - " ylocator=[], yformatter=[])\n", + "f, axs = plot.subplots(\n", + " [[1, 1, 2, 2], [0, 3, 3, 0]],\n", + " share=0, aspect=2.2, axwidth=3\n", + ")\n", + "axs.format(\n", + " suptitle='Duplicate axes with custom transformations',\n", + " xcolor=c1, gridcolor=c1,\n", + " ylocator=[], yformatter=[]\n", + ")\n", + "\n", "# Meters and kilometers\n", "ax = axs[0]\n", "ax.format(xlim=(0, 5000), xlabel='meters')\n", - "ax.dualx(lambda x: x*1e-3, label='kilometers',\n", - " grid=True, color=c2, gridcolor=c2)\n", + "ax.dualx(\n", + " lambda x: x*1e-3,\n", + " label='kilometers', grid=True, color=c2, gridcolor=c2\n", + ")\n", + "\n", "# Kelvin and Celsius\n", "ax = axs[1]\n", "ax.format(xlim=(200, 300), xlabel='temperature (K)')\n", - "ax.dualx(lambda x: x - 273.15, label='temperature (\\N{DEGREE SIGN}C)',\n", - " grid=True, color=c2, gridcolor=c2)\n", + "ax.dualx(\n", + " lambda x: x - 273.15,\n", + " label='temperature (\\N{DEGREE SIGN}C)', grid=True, color=c2, gridcolor=c2\n", + ")\n", + "\n", "# With symlog parent\n", "ax = axs[2]\n", "ax.format(xlim=(-100, 100), xscale='symlog', xlabel='MegaJoules')\n", - "ax.dualx(lambda x: x*1e6, label='Joules', formatter='log',\n", - " grid=True, color=c2, gridcolor=c2)\n", + "ax.dualx(\n", + " lambda x: x*1e6,\n", + " label='Joules', formatter='log', grid=True, color=c2, gridcolor=c2\n", + ")\n", "plot.rc.reset()" ] }, @@ -408,23 +506,33 @@ "outputs": [], "source": [ "import proplot as plot\n", - "# Vertical scales for atmospheric scientists, assumed scale height is 7km\n", "plot.rc.update({'grid.alpha': 0.4, 'linewidth': 1, 'grid.linewidth': 1})\n", "f, axs = plot.subplots(ncols=2, share=0, aspect=0.4, axwidth=1.8)\n", "axs.format(suptitle='Duplicate axes with special transformations')\n", "c1, c2 = plot.shade('cerulean', 0.5), plot.shade('red', 0.5)\n", + "\n", + "# Pressure as the linear scale, height on opposite axis (scale height 7km)\n", "ax = axs[0]\n", - "ax.format(xformatter='null', ylabel='pressure (hPa)',\n", - " ylim=(1000, 10), xlocator=[], ycolor=c1, gridcolor=c1)\n", + "ax.format(\n", + " xformatter='null', ylabel='pressure (hPa)',\n", + " ylim=(1000, 10), xlocator=[], ycolor=c1, gridcolor=c1\n", + ")\n", "scale = plot.Scale('height')\n", - "ax.dualy(scale, label='height (km)', ticks=2.5,\n", - " color=c2, gridcolor=c2, grid=True)\n", + "ax.dualy(\n", + " scale, label='height (km)', ticks=2.5, color=c2, gridcolor=c2, grid=True\n", + ")\n", + "\n", + "# Height as the linear scale, pressure on opposite axis (scale height 7km)\n", "ax = axs[1] # span\n", - "ax.format(xformatter='null', ylabel='height (km)', ylim=(0, 20), xlocator='null',\n", - " grid=True, gridcolor=c2, ycolor=c2)\n", + "ax.format(\n", + " xformatter='null', ylabel='height (km)', ylim=(0, 20), xlocator='null',\n", + " grid=True, gridcolor=c2, ycolor=c2\n", + ")\n", "scale = plot.Scale('pressure')\n", - "ax.dualy(scale, label='pressure (hPa)', locator=100,\n", - " color=c1, gridcolor=c1, grid=True)\n", + "ax.dualy(\n", + " scale, label='pressure (hPa)', locator=100,\n", + " color=c1, gridcolor=c1, grid=True\n", + ")\n", "plot.rc.reset()" ] }, @@ -436,22 +544,26 @@ "source": [ "import proplot as plot\n", "import numpy as np\n", - "# Inverse scaling, useful for frequency analysis\n", "plot.rc['axes.ymargin'] = 0\n", + "f, ax = plot.subplots(aspect=(3, 1), width=6)\n", + "\n", + "# Sample data\n", "cutoff = 1/5\n", "c1, c2 = plot.shade('cerulean', 0.5), plot.shade('red', 0.5)\n", "x = np.linspace(0.01, 0.5, 1000) # in wavenumber days\n", - "response = (np.tanh(-((x - cutoff)/0.03))\n", - " + 1) / 2 # imgarinary response function\n", - "f, ax = plot.subplots(aspect=(3, 1), width=6)\n", + "response = (np.tanh(-((x - cutoff)/0.03)) + 1) / 2 # response func\n", "ax.axvline(cutoff, lw=2, ls='-', color=c2)\n", "ax.fill_between([cutoff - 0.03, cutoff + 0.03], 0, 1, color=c2, alpha=0.3)\n", "ax.plot(x, response, color=c1, lw=2)\n", - "ax.format(xlabel='wavenumber (days$^{-1}$)', ylabel='response', grid=False)\n", + "\n", + "# Add inverse scale to top\n", "scale = plot.Scale('inverse')\n", + "ax.format(xlabel='wavenumber (days$^{-1}$)', ylabel='response', grid=False)\n", "ax = ax.dualx(scale, locator='log', locator_kw={'subs': (1, 2, 5)}, label='period (days)')\n", - "ax.format(title='Imaginary response function',\n", - " suptitle='Duplicate axes with wavenumber and period')\n", + "ax.format(\n", + " title='Imaginary response function',\n", + " suptitle='Duplicate axes with wavenumber and period'\n", + ")\n", "plot.rc.reset()" ] } diff --git a/docs/basics.ipynb b/docs/basics.ipynb index 4e3b8ce00..db11325df 100644 --- a/docs/basics.ipynb +++ b/docs/basics.ipynb @@ -43,23 +43,32 @@ "import numpy as np\n", "state = np.random.RandomState(51423)\n", "data = 2*(state.rand(100, 5) - 0.5).cumsum(axis=0)\n", + "\n", "# Simple plot\n", "f, axs = plot.subplots(ncols=2)\n", "axs[0].plot(data, lw=2)\n", "axs[0].format(xticks=20, xtickminor=False)\n", - "axs.format(suptitle='Simple subplot grid', title='Title',\n", - " xlabel='x axis', ylabel='y axis')\n", + "axs.format(\n", + " suptitle='Simple subplot grid', title='Title',\n", + " xlabel='x axis', ylabel='y axis'\n", + ")\n", + "\n", "# Complex grid\n", "array = [[1, 1, 2, 2], [0, 3, 3, 0]]\n", "f, axs = plot.subplots(array, axwidth=1.8)\n", - "axs.format(abc=True, abcloc='ul', suptitle='Complex subplot grid',\n", - " xlabel='xlabel', ylabel='ylabel')\n", + "axs.format(\n", + " abc=True, abcloc='ul', suptitle='Complex subplot grid',\n", + " xlabel='xlabel', ylabel='ylabel'\n", + ")\n", "axs[2].plot(data, lw=2)\n", + "\n", "# Really complex grid\n", "array = [[1, 1, 2], [1, 1, 6], [3, 4, 4], [3, 5, 5]]\n", "f, axs = plot.subplots(array, width=5, span=False)\n", - "axs.format(suptitle='Really complex subplot grid',\n", - " xlabel='xlabel', ylabel='ylabel', abc=True)\n", + "axs.format(\n", + " suptitle='Really complex subplot grid',\n", + " xlabel='xlabel', ylabel='ylabel', abc=True\n", + ")\n", "axs[0].plot(data, lw=2)" ] }, @@ -137,32 +146,41 @@ "source": [ "import proplot as plot\n", "import numpy as np\n", + "\n", "# Update global settings in several different ways\n", "plot.rc.cycle = 'colorblind'\n", "plot.rc.color = 'gray6'\n", "plot.rc.update({'fontname': 'DejaVu Sans'})\n", "plot.rc['figure.facecolor'] = 'gray3'\n", "plot.rc.axesfacecolor = 'gray4'\n", - "# Context settings applied to figure only\n", + "\n", + "# Apply settings to figure with context()\n", "with plot.rc.context({'suptitle.size': 11}, toplabelcolor='gray6', linewidth=1.5):\n", " f, axs = plot.subplots(ncols=2, aspect=1, width=6, span=False, sharey=2)\n", - "# Plot stuff\n", + " \n", + "# Plot lines \n", "N, M = 100, 6\n", "state = np.random.RandomState(51423)\n", "values = np.arange(1, M+1)\n", "for i, ax in enumerate(axs):\n", " data = np.cumsum(state.rand(N, M) - 0.5, axis=0)\n", " lines = ax.plot(data, linewidth=3, cycle='Grays')\n", - "axs.format(grid=False, xlabel='x label', ylabel='y label',\n", - " collabels=['Column label 1', 'Column label 2'],\n", - " suptitle='Rc settings demo',\n", - " suptitlecolor='gray7',\n", - " abc=True, abcloc='l', abcstyle='A)',\n", - " title='Title', titleloc='r', titlecolor='gray7')\n", + " \n", + "# Apply settings to axes with format()\n", + "axs.format(\n", + " grid=False, xlabel='x label', ylabel='y label',\n", + " collabels=['Column label 1', 'Column label 2'],\n", + " suptitle='Rc settings demo',\n", + " suptitlecolor='gray7',\n", + " abc=True, abcloc='l', abcstyle='A)',\n", + " title='Title', titleloc='r', titlecolor='gray7'\n", + ")\n", "ay = axs[-1].twinx()\n", "ay.format(ycolor='red', linewidth=1.5, ylabel='secondary axis')\n", "ay.plot((state.rand(100) - 0.2).cumsum(), color='r', lw=3)\n", - "plot.rc.reset() # reset persistent mods made at head of cell" + "\n", + "# Reset persistent modifications from head of cell\n", + "plot.rc.reset()" ] }, { @@ -193,14 +211,18 @@ "import numpy as np\n", "state = np.random.RandomState(51423)\n", "f, axs = plot.subplots(ncols=4, nrows=4, axwidth=1.2)\n", + "axs.format(\n", + " xlabel='xlabel', ylabel='ylabel', suptitle='Subplot grid demo',\n", + " grid=False, xlim=(0, 50), ylim=(-4, 4)\n", + ")\n", + "\n", + "# Various ways to select subplots in the subplot grid\n", "axs[:, 0].format(color='gray7', facecolor='gray3', linewidth=1)\n", "axs[0, :].format(color='red', facecolor='gray3', linewidth=1)\n", "axs[0].format(color='black', facecolor='gray5', linewidth=1.4)\n", "axs[1:, 1:].format(facecolor='gray1')\n", "for ax in axs[1:, 1:]:\n", - " ax.plot((state.rand(50, 5) - 0.5).cumsum(axis=0), cycle='Grays', lw=2)\n", - "axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Subplot grid demo',\n", - " grid=False, xlim=(0, 50), ylim=(-4, 4))" + " ax.plot((state.rand(50, 5) - 0.5).cumsum(axis=0), cycle='Grays', lw=2)" ] } ], diff --git a/docs/colorbars_legends.ipynb b/docs/colorbars_legends.ipynb index 75b340034..38ed8cc74 100644 --- a/docs/colorbars_legends.ipynb +++ b/docs/colorbars_legends.ipynb @@ -38,6 +38,7 @@ "import numpy as np\n", "with plot.rc.context(abc=True):\n", " f, axs = plot.subplots(ncols=2, share=0)\n", + "\n", "# Colorbars\n", "ax = axs[0]\n", "state = np.random.RandomState(51423)\n", @@ -45,11 +46,15 @@ "ax.colorbar(m, loc='r')\n", "ax.colorbar(m, loc='ll', label='colorbar label')\n", "ax.format(title='Axes colorbars', suptitle='Axes colorbars and legends demo')\n", + "\n", "# Legends\n", "ax = axs[1]\n", "ax.format(title='Axes legends', titlepad='0em')\n", - "hs = ax.plot((state.rand(10, 5)-0.5).cumsum(axis=0), lw=3, legend='t', cycle='sharp',\n", - " labels=list('abcde'), legend_kw={'ncols': 5, 'frame': False})\n", + "hs = ax.plot(\n", + " (state.rand(10, 5) - 0.5).cumsum(axis=0), linewidth=3,\n", + " cycle='sharp', legend='t',\n", + " labels=list('abcde'), legend_kw={'ncols': 5, 'frame': False}\n", + ")\n", "ax.legend(hs, loc='r', ncols=1, frame=False)\n", "ax.legend(hs, loc='ll', label='legend label')\n", "axs.format(xlabel='xlabel', ylabel='ylabel')" @@ -67,19 +72,25 @@ "axs.format(suptitle='Stacked colorbars demo')\n", "state = np.random.RandomState(51423)\n", "N = 10\n", + "# Repeat for both axes\n", "for j, ax in enumerate(axs):\n", - " ax.format(xlabel='data', xlocator=np.linspace(\n", - " 0, 0.8, 5), title=f'Subplot #{j+1}')\n", - " for i, (x0, y0, x1, y1, cmap, scale) in enumerate(\n", - " ((0, 0.5, 1, 1, 'grays', 0.5),\n", - " (0, 0, 0.5, 0.5, 'reds', 1),\n", - " (0.5, 0, 1, 0.5, 'blues', 2))):\n", + " ax.format(\n", + " xlabel='data', xlocator=np.linspace(0, 0.8, 5),\n", + " title=f'Subplot #{j+1}'\n", + " )\n", + " for i, (x0, y0, x1, y1, cmap, scale) in enumerate((\n", + " (0, 0.5, 1, 1, 'grays', 0.5),\n", + " (0, 0, 0.5, 0.5, 'reds', 1),\n", + " (0.5, 0, 1, 0.5, 'blues', 2)\n", + " )):\n", " if j == 1 and i == 0:\n", " continue\n", " data = state.rand(N, N)*scale\n", " x, y = np.linspace(x0, x1, N + 1), np.linspace(y0, y1, N + 1)\n", - " m = ax.pcolormesh(x, y, data, cmap=cmap,\n", - " levels=np.linspace(0, scale, 11))\n", + " m = ax.pcolormesh(\n", + " x, y, data, cmap=cmap,\n", + " levels=np.linspace(0, scale, 11)\n", + " )\n", " ax.colorbar(m, loc='l', label=f'dataset #{i+1}')" ] }, @@ -113,9 +124,12 @@ "state = np.random.RandomState(51423)\n", "m = axs.pcolormesh(\n", " state.rand(20, 20), cmap='grays',\n", - " levels=np.linspace(0, 1, 11), extend='both')[0]\n", - "axs.format(suptitle='Figure colorbars and legends demo', abc=True,\n", - " abcloc='l', abcstyle='a.', xlabel='xlabel', ylabel='ylabel')\n", + " levels=np.linspace(0, 1, 11), extend='both'\n", + ")[0]\n", + "axs.format(\n", + " suptitle='Figure colorbars and legends demo', abc=True,\n", + " abcloc='l', abcstyle='a.', xlabel='xlabel', ylabel='ylabel'\n", + ")\n", "f.colorbar(m, label='column 1', ticks=0.5, loc='b', col=1)\n", "f.colorbar(m, label='columns 2-3', ticks=0.2, loc='b', cols=(2, 3))\n", "f.colorbar(m, label='stacked colorbar', ticks=0.1, loc='b', minorticks=0.05)\n", @@ -130,8 +144,12 @@ "source": [ "import proplot as plot\n", "import numpy as np\n", - "f, axs = plot.subplots(ncols=2, nrows=2, axwidth=1.3,\n", - " share=0, wspace=0.3, order='F')\n", + "f, axs = plot.subplots(\n", + " ncols=2, nrows=2, axwidth=1.3,\n", + " share=0, wspace=0.3, order='F'\n", + ")\n", + "\n", + "# Plot data\n", "data = (np.random.rand(50, 50) - 0.1).cumsum(axis=0)\n", "m = axs[:2].contourf(data, cmap='grays', extend='both')\n", "colors = plot.Colors('grays', 5)\n", @@ -140,6 +158,8 @@ "for abc, color in zip('ABCDEF', colors):\n", " h = axs[2:].plot(state.rand(10), lw=3, color=color, label=f'line {abc}')\n", " hs.extend(h[0])\n", + "\n", + "# Add colorbars and legends\n", "f.colorbar(m[0], length=0.8, label='colorbar label', loc='b', col=1, locator=5)\n", "f.colorbar(m[0], label='colorbar label', loc='l')\n", "f.legend(hs, ncols=2, center=True, frame=False, loc='b', col=2)\n", @@ -177,6 +197,7 @@ "import proplot as plot\n", "import numpy as np\n", "f, axs = plot.subplots(share=0, ncols=2)\n", + "\n", "# Colorbars from lines\n", "ax = axs[0]\n", "state = np.random.RandomState(51423)\n", @@ -186,6 +207,7 @@ " 'length': '8em', 'label': 'from lines'})\n", "ax.colorbar(hs, loc='t', values=2*np.linspace(0.5, 9.5, 10),\n", " label='from lines', length=0.7, ticks=2)\n", + "\n", "# Colorbars from a mappable\n", "ax = axs[1]\n", "m = ax.contourf(data.T, extend='both', cmap='algae',\n", @@ -229,6 +251,7 @@ "labels = ['a', 'bb', 'ccc', 'dddd', 'eeeee']\n", "f, axs = plot.subplots(ncols=2, span=False, share=1)\n", "hs1, hs2 = [], []\n", + "\n", "# On-the-fly legends\n", "state = np.random.RandomState(51423)\n", "for i, label in enumerate(labels):\n", @@ -239,6 +262,7 @@ " h2 = axs[1].plot(data, lw=4, label=label, legend='r', cycle='floral',\n", " legend_kw={'ncols': 1, 'frame': False, 'title': 'no frame'}) # add to legend in right panel\n", " hs2.extend(h2)\n", + " \n", "# Outer legends\n", "ax = axs[0]\n", "ax.legend(hs1, loc='b', ncols=3, linewidth=2, title='row major', order='C',\n", diff --git a/docs/colormaps.ipynb b/docs/colormaps.ipynb index d57824d45..d1899d02c 100644 --- a/docs/colormaps.ipynb +++ b/docs/colormaps.ipynb @@ -88,8 +88,10 @@ "f = plot.show_colorspaces(axwidth=1.6, luminance=50)\n", "f = plot.show_colorspaces(axwidth=1.6, saturation=60)\n", "f = plot.show_colorspaces(axwidth=1.6, hue=0)\n", - "f = plot.show_channels('magma', 'rocket', 'fire', 'dusk',\n", - " axwidth=1.4, minhue=-180, maxsat=300, rgb=False)" + "f = plot.show_channels(\n", + " 'magma', 'rocket', 'fire', 'dusk',\n", + " axwidth=1.4, minhue=-180, maxsat=300, rgb=False\n", + ")" ] }, { @@ -125,43 +127,56 @@ "import proplot as plot\n", "import numpy as np\n", "state = np.random.RandomState(51423)\n", - "f, axs = plot.subplots([[0, 1, 1, 2, 2, 0], [3, 3, 4, 4, 5, 5]],\n", - " ncols=2, axwidth=2, aspect=1)\n", + "f, axs = plot.subplots(\n", + " [[0, 1, 1, 2, 2, 0], [3, 3, 4, 4, 5, 5]],\n", + " ncols=2, axwidth=2, aspect=1\n", + ")\n", + "\n", "# Monochromatic colormaps\n", - "axs.format(xlabel='x axis', ylabel='y axis', span=False,\n", - " suptitle='Building your own PerceptuallyUniformColormaps')\n", + "axs.format(\n", + " xlabel='x axis', ylabel='y axis', span=False,\n", + " suptitle='Building your own PerceptuallyUniformColormaps'\n", + ")\n", "data = state.rand(30, 30).cumsum(axis=1)\n", "axs[0].format(title='From single color')\n", "m = axs[0].contourf(data, cmap='ocean blue', cmap_kw={'name': 'water'})\n", "cmap1 = m.cmap\n", "axs[1].format(title='From three colors')\n", - "cmap2 = plot.Colormap('brick red_r', 'denim_r',\n", - " 'warm gray_r', fade=90, name='tricolor')\n", + "cmap2 = plot.Colormap(\n", + " 'brick red_r', 'denim_r', 'warm gray_r',\n", + " fade=90, name='tricolor'\n", + ")\n", "axs[1].contourf(data, cmap=cmap2, levels=12)\n", + "\n", "# Colormaps from channel value dictionaries\n", "axs[2:4].format(title='From channel values')\n", - "cmap3 = plot.Colormap(\n", - " {'hue': ['red-90', 'red+90'],\n", - " 'saturation': [50, 70, 30],\n", - " 'luminance': [20, 100]},\n", - " name='Matter', space='hcl')\n", + "cmap3 = plot.Colormap({\n", + " 'hue': ['red-90', 'red+90'],\n", + " 'saturation': [50, 70, 30],\n", + " 'luminance': [20, 100]\n", + "}, name='Matter', space='hcl')\n", "axs[2].pcolormesh(data, cmap=cmap3)\n", - "cmap4 = plot.Colormap(\n", - " {'hue': ['red', 'red-720'],\n", - " 'saturation': [80, 20],\n", - " 'luminance': [20, 100]},\n", - " name='cubehelix', space='hpl')\n", + "cmap4 = plot.Colormap({\n", + " 'hue': ['red', 'red-720'],\n", + " 'saturation': [80, 20],\n", + " 'luminance': [20, 100]\n", + "}, name='cubehelix', space='hpl')\n", "axs[3].pcolormesh(data, cmap=cmap4)\n", + "\n", "# Colormap from lists\n", - "m = axs[4].pcolormesh(data,\n", - " cmap=('maroon', 'desert sand'),\n", - " cmap_kw={'name': 'reddish'})\n", + "m = axs[4].pcolormesh(\n", + " data, cmap=('maroon', 'desert sand'),\n", + " cmap_kw={'name': 'reddish'}\n", + ")\n", "cmap5 = m.cmap\n", "axs[4].format(title='From list of colors')\n", + "\n", "# Test the channels\n", "f = plot.show_channels(cmap1, cmap2, axwidth=1.4, rgb=False)\n", - "f = plot.show_channels(cmap3, cmap4, cmap5, minhue=-180,\n", - " axwidth=1.4, rgb=False)" + "f = plot.show_channels(\n", + " cmap3, cmap4, cmap5, minhue=-180,\n", + " axwidth=1.4, rgb=False\n", + ")" ] }, { @@ -193,24 +208,33 @@ "f, axs = plot.subplots([[0, 1, 1, 0], [2, 2, 3, 3]], axwidth=2, span=False)\n", "state = np.random.RandomState(51423)\n", "data = state.rand(30, 30).cumsum(axis=1)\n", + "\n", "# Diverging colormap example\n", "title1 = 'Custom diverging map'\n", "cmap1 = plot.Colormap('Blue4_r', 'RedPurple3', name='Diverging', save=True)\n", + "\n", "# SciVisColor examples\n", "title2 = 'Custom complex map'\n", - "cmap2 = plot.Colormap('Green1_r', 'Orange5', 'Blue1_r',\n", - " 'Blue6', name='Complex', save=True)\n", + "cmap2 = plot.Colormap(\n", + " 'Green1_r', 'Orange5', 'Blue1_r', 'Blue6',\n", + " name='Complex', save=True\n", + ")\n", "title3 = 'SciVisColor example reproduction'\n", - "cmap3 = plot.Colormap('Green1_r', 'Orange5', 'Blue1_r',\n", - " 'Blue6', ratios=(1, 3, 5, 10), name='SciVisColor', save=True)\n", + "cmap3 = plot.Colormap(\n", + " 'Green1_r', 'Orange5', 'Blue1_r', 'Blue6',\n", + " ratios=(1, 3, 5, 10), name='SciVisColor', save=True\n", + ")\n", + "\n", "# Plot examples\n", "for ax, cmap, title in zip(axs, (cmap1, cmap2, cmap3), (title1, title2, title3)):\n", " func = (ax.pcolormesh if cmap is cmap1 else ax.contourf)\n", " m = func(data, cmap=cmap, levels=256)\n", " ax.colorbar(m, loc='b', locator='null', label=cmap.name)\n", " ax.format(title=title)\n", - "axs.format(xlabel='xlabel', ylabel='ylabel',\n", - " suptitle='Merging existing colormaps')" + "axs.format(\n", + " xlabel='xlabel', ylabel='ylabel',\n", + " suptitle='Merging existing colormaps'\n", + ")" ] }, { @@ -247,6 +271,7 @@ " axwidth=1.7, span=False)\n", "state = np.random.RandomState(51423)\n", "data = state.rand(40, 40).cumsum(axis=0) - 12\n", + "\n", "# Cutting left and right\n", "for ax, coord in zip(axs[:3], (None, 0.3, 0.7)):\n", " cmap = 'grays'\n", @@ -259,6 +284,7 @@ " ax.pcolormesh(data, cmap=cmap, cmap_kw=cmap_kw,\n", " colorbar='b', colorbar_kw={'locator': 'null'})\n", " ax.format(xlabel='x axis', ylabel='y axis', title=title)\n", + " \n", "# Cutting central colors\n", "levels = plot.arange(-10, 10, 2)\n", "for i, (ax, cut) in enumerate(zip(axs[3:], (None, None, 0.1, 0.2))):\n", @@ -270,19 +296,27 @@ " levels = plot.arange(-10, 10, 2)\n", " if cut is not None:\n", " title = f'cut = {cut}'\n", - " m = ax.contourf(data, cmap='Div', cmap_kw={'cut': cut},\n", - " extend='both', levels=levels)\n", - " ax.format(xlabel='x axis', ylabel='y axis', title=title,\n", - " suptitle='Truncating sequential and diverging colormaps')\n", + " m = ax.contourf(\n", + " data, cmap='Div', cmap_kw={'cut': cut},\n", + " extend='both', levels=levels\n", + " )\n", + " ax.format(\n", + " xlabel='x axis', ylabel='y axis', title=title,\n", + " suptitle='Truncating sequential and diverging colormaps'\n", + " )\n", " ax.colorbar(m, loc='b', locator='null')\n", + " \n", "# Rotating cyclic\n", "f, axs = plot.subplots(ncols=3, axwidth=1.7)\n", "data = (state.rand(50, 50) - 0.48).cumsum(axis=1).cumsum(axis=0) - 50\n", "for ax, shift in zip(axs, (0, 90, 180)):\n", " m = ax.contourf(data, cmap='twilight', cmap_kw={'shift': shift}, levels=12)\n", - " ax.format(xlabel='x axis', ylabel='y axis', title=f'shift = {shift}',\n", - " suptitle='Rotating cyclic colormaps')\n", + " ax.format(\n", + " xlabel='x axis', ylabel='y axis', title=f'shift = {shift}',\n", + " suptitle='Rotating cyclic colormaps'\n", + " )\n", " ax.colorbar(m, loc='b', locator='null')\n", + " \n", "# Changing gamma\n", "f, axs = plot.subplots(ncols=3, axwidth=1.7, aspect=1)\n", "data = state.rand(10, 10).cumsum(axis=1)\n", @@ -290,8 +324,10 @@ " cmap = plot.Colormap('boreal', name=str(gamma), gamma=gamma)\n", " m = ax.pcolormesh(data, cmap=cmap, levels=10, extend='both')\n", " ax.colorbar(m, loc='b', locator='none')\n", - " ax.format(title=f'gamma = {gamma}', xlabel='x axis', ylabel='y axis',\n", - " suptitle='Changing the colormap gamma')" + " ax.format(\n", + " title=f'gamma = {gamma}', xlabel='x axis', ylabel='y axis',\n", + " suptitle='Changing the colormap gamma'\n", + " )" ] }, { @@ -320,6 +356,18 @@ "language": "python", "name": "python3" }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + }, "toc": { "colors": { "hover_highlight": "#ece260", diff --git a/docs/colors_fonts.ipynb b/docs/colors_fonts.ipynb index 78f2fb298..887cb7cb5 100644 --- a/docs/colors_fonts.ipynb +++ b/docs/colors_fonts.ipynb @@ -70,6 +70,7 @@ "import numpy as np\n", "state = np.random.RandomState(51423)\n", "f, axs = plot.subplots(nrows=2, aspect=2, axwidth=3, share=0)\n", + "\n", "# Drawing from colormap\n", "ax = axs[0]\n", "cmap = 'deep'\n", @@ -77,20 +78,27 @@ "idxs = plot.arange(0, 1, 0.2)\n", "state.shuffle(idxs)\n", "for idx in idxs:\n", - " h = ax.plot((np.random.rand(20) - 0.4).cumsum(), lw=5, color=(cmap, idx),\n", - " label=f'idx {idx:.1f}', legend='r', legend_kw={'ncols': 1})\n", + " h = ax.plot(\n", + " (np.random.rand(20) - 0.4).cumsum(), lw=5, color=(cmap, idx),\n", + " label=f'idx {idx:.1f}', legend='r', legend_kw={'ncols': 1}\n", + " )\n", "ax.colorbar(m, loc='ul', locator=0.2, label='colormap')\n", "ax.format(title='Drawing from the Solar colormap', grid=True)\n", + "\n", "# Drawing from color cycle\n", "ax = axs[1]\n", "idxs = np.arange(6)\n", "state.shuffle(idxs)\n", "for idx in idxs:\n", - " h = ax.plot((np.random.rand(20)-0.4).cumsum(), lw=5, color=('qual1', idx),\n", - " label=f'idx {idx:.0f}', legend='r', legend_kw={'ncols': 1})\n", + " h = ax.plot(\n", + " (np.random.rand(20)-0.4).cumsum(), lw=5, color=('qual1', idx),\n", + " label=f'idx {idx:.0f}', legend='r', legend_kw={'ncols': 1}\n", + " )\n", "ax.format(title='Drawing from the ggplot color cycle')\n", - "axs.format(xlocator='null', abc=True, abcloc='ur', abcstyle='A.',\n", - " suptitle='Getting individual colors from colormaps and cycles')" + "axs.format(\n", + " xlocator='null', abc=True, abcloc='ur', abcstyle='A.',\n", + " suptitle='Getting individual colors from colormaps and cycles'\n", + ")" ] }, { diff --git a/docs/cycles.ipynb b/docs/cycles.ipynb index 02cdec7f5..706310545 100644 --- a/docs/cycles.ipynb +++ b/docs/cycles.ipynb @@ -68,15 +68,17 @@ "source": [ "import proplot as plot\n", "import numpy as np\n", + "lw = 5\n", "state = np.random.RandomState(51423)\n", "data = (state.rand(12, 12) - 0.45).cumsum(axis=0)\n", - "plot.rc.cycle = 'contrast'\n", - "lw = 5\n", "f, axs = plot.subplots(ncols=3, axwidth=1.7)\n", + "\n", "# Use the default cycle\n", "ax = axs[0]\n", + "plot.rc.cycle = 'contrast'\n", "ax.plot(data, lw=lw)\n", - "# Pass cycle to the plotting command (note this never resets the cycle)\n", + "\n", + "# Pass the cycle to a plotting command\n", "ax = axs[1]\n", "ax.plot(data, cycle='qual2', lw=lw)\n", "ax = axs[2]\n", @@ -116,20 +118,24 @@ "f, axs = plot.subplots(ncols=2, share=0, axwidth=2, aspect=1.2)\n", "state = np.random.RandomState(51423)\n", "data = (20*state.rand(10, 21) - 10).cumsum(axis=0)\n", - "# Example 1\n", + "\n", + "# Cycle from on-the-fly monochromatic colormap\n", "ax = axs[0]\n", "lines = ax.plot(data[:, :5], cycle='plum', cycle_kw={'left': 0.3}, lw=5)\n", "f.colorbar(lines, loc='b', col=1, values=np.arange(0, len(lines)))\n", "f.legend(lines, loc='b', col=1, labels=np.arange(0, len(lines)))\n", "ax.format(title='Cycle from color')\n", - "# Example 2\n", + "\n", + "# Cycle from registered colormaps\n", "ax = axs[1]\n", "cycle = plot.Cycle('blues', 'reds', 'oranges', 15, left=0.1)\n", "lines = ax.plot(data[:, :15], cycle=cycle, lw=5)\n", "f.colorbar(lines, loc='b', col=2, values=np.arange(0, len(lines)), locator=2)\n", "f.legend(lines, loc='b', col=2, labels=np.arange(0, len(lines)), ncols=4)\n", - "ax.format(title='Cycle from merged colormaps',\n", - " suptitle='Color cycles from colormaps demo')" + "ax.format(\n", + " title='Cycle from merged colormaps',\n", + " suptitle='Color cycles from colormaps demo'\n", + ")" ] }, { @@ -163,8 +169,10 @@ "data = pd.DataFrame(data, columns=pd.Index(['a', 'b', 'c', 'd'], name='label'))\n", "ax.format(suptitle='Plot without color cycle')\n", "cycle = plot.Cycle(dashes=[(1, 0.5), (1, 1.5), (3, 0.5), (3, 1.5)])\n", - "obj = ax.plot(data, lw=3, cycle=cycle, legend='ul',\n", - " legend_kw={'ncols': 2, 'handlelength': 3})" + "obj = ax.plot(\n", + " data, lw=3, cycle=cycle, legend='ul',\n", + " legend_kw={'ncols': 2, 'handlelength': 3}\n", + ")" ] }, { diff --git a/docs/insets_panels.ipynb b/docs/insets_panels.ipynb index 3045ef47f..33740e793 100644 --- a/docs/insets_panels.ipynb +++ b/docs/insets_panels.ipynb @@ -35,13 +35,18 @@ "source": [ "import proplot as plot\n", "f, axs = plot.subplots(axwidth=1.2, nrows=2, ncols=2, share=0)\n", + "# Panels never interfere with subplot layout and spacing\n", "for ax, side in zip(axs, 'tlbr'):\n", " ax.panel_axes(side)\n", - "axs.format(title='Title', suptitle='Panel axes demo', collabels=['Column 1', 'Column 2'],\n", - " abcloc='ul', titleloc='uc', xlabel='xlabel', ylabel='ylabel', abc=True, top=False)\n", - "axs.format(xlim=(0, 1), ylim=(0, 1),\n", - " ylocator=plot.arange(0.2, 0.8, 0.2),\n", - " xlocator=plot.arange(0.2, 0.8, 0.2))" + "axs.format(\n", + " title='Title', suptitle='Panel axes demo', collabels=['Column 1', 'Column 2'],\n", + " abcloc='ul', titleloc='uc', xlabel='xlabel', ylabel='ylabel', abc=True, top=False\n", + ")\n", + "axs.format(\n", + " xlim=(0, 1), ylim=(0, 1),\n", + " ylocator=plot.arange(0.2, 0.8, 0.2),\n", + " xlocator=plot.arange(0.2, 0.8, 0.2)\n", + ")" ] }, { @@ -54,15 +59,20 @@ "import numpy as np\n", "state = np.random.RandomState(51423)\n", "data = (state.rand(20, 20)-0.1).cumsum(axis=1)\n", - "f, axs = plot.subplots(axwidth=1.5, nrows=2, ncols=2,\n", - " share=0, panelpad=0.1, includepanels=True)\n", + "# Stacked panels with outer colorbars\n", + "f, axs = plot.subplots(\n", + " axwidth=1.5, nrows=2, ncols=2,\n", + " share=0, panelpad=0.1, includepanels=True\n", + ")\n", "maxs = axs.panel('r', space=0)\n", "saxs = axs.panel('r', space=0, share=False)\n", "axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Panel axes demo')\n", "for i, ax in enumerate(axs):\n", " ax.format(title=f'Dataset {i+1}')\n", - "axs.contourf(data, cmap='glacial', levels=plot.arange(-1, 11),\n", - " colorbar='b', colorbar_kw={'label': 'colorbar'}, extend='both')\n", + "axs.contourf(\n", + " data, cmap='glacial', levels=plot.arange(-1, 11),\n", + " colorbar='b', colorbar_kw={'label': 'colorbar'}, extend='both'\n", + ")\n", "maxs.plot(data.mean(axis=1), np.arange(20), color='gray7')\n", "maxs.format(title='Mean')\n", "saxs.plot(data.std(axis=1), np.arange(20), color='gray7', ls='--')\n", @@ -96,6 +106,7 @@ "import proplot as plot\n", "import numpy as np\n", "N = 20\n", + "# Inset axes representing a \"zoom\"\n", "state = np.random.RandomState(51423)\n", "f, ax = plot.subplots()\n", "x, y = np.arange(10), np.arange(10)\n", @@ -103,10 +114,14 @@ "m = ax.pcolormesh(data, cmap='Grays', levels=N)\n", "ax.colorbar(m, loc='b', label='label')\n", "ax.format(xlabel='xlabel', ylabel='ylabel')\n", - "axi = ax.inset([5, 5, 4, 4], transform='data', zoom=True,\n", - " zoom_kw={'color': 'red', 'lw': 2})\n", - "axi.format(xlim=(2, 4), ylim=(2, 4), color='red',\n", - " linewidth=1.5, ticklabelweight='bold')\n", + "axi = ax.inset(\n", + " [5, 5, 4, 4], transform='data', zoom=True,\n", + " zoom_kw={'color': 'red', 'lw': 2}\n", + ")\n", + "axi.format(\n", + " xlim=(2, 4), ylim=(2, 4), color='red',\n", + " linewidth=1.5, ticklabelweight='bold'\n", + ")\n", "axi.pcolormesh(data, cmap='Grays', levels=N)\n", "ax.format(suptitle='Inset axes demo')" ] diff --git a/docs/projection.ipynb b/docs/projection.ipynb index db9d56ad7..4fdbce316 100644 --- a/docs/projection.ipynb +++ b/docs/projection.ipynb @@ -42,22 +42,36 @@ "source": [ "import proplot as plot\n", "import numpy as np\n", - "f, axs = plot.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], proj='polar')\n", "state = np.random.RandomState(51423)\n", "N = 200\n", "x = np.linspace(0, 2*np.pi, N)\n", "y = 100*(state.rand(N, 5)-0.3).cumsum(axis=0)/N\n", + "f, axs = plot.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], proj='polar')\n", + "axs.format(\n", + " suptitle='Polar axes demo', linewidth=1,\n", + " ticklabelsize=9, rlines=0.5, rlim=(0, 19),\n", + " titlepad='1.5em' # matplotlib default title offset is incorrect \n", + ")\n", "for i in range(5):\n", " axs.plot(x + i*2*np.pi/5, y[:, i], cycle='contrast', zorder=0, lw=3)\n", - "axs.format(suptitle='Polar axes demo', linewidth=1,\n", - " ticklabelsize=9, rlines=0.5, rlim=(0, 19))\n", - "axs[0].format(title='Normal plot', thetaformatter='pi', rlines=5, gridalpha=1, gridlinestyle=':',\n", - " rlabelpos=180, color='gray8', ticklabelweight='bold')\n", - "axs[1].format(title='Sector plot', thetadir=-1, thetalines=90, thetalim=(0, 270), theta0='N',\n", - " rlim=(0, 22), rlines=5)\n", - "axs[2].format(title='Annular plot', thetadir=-1, thetalines=10,\n", - " r0=0, rlim=(10, 22), rformatter='null', rlocator=2)\n", - "axs.format(titlepad='1.5em') # matplotlib default title offset is incorrect" + "\n", + "# Standard polar plot\n", + "axs[0].format(\n", + " title='Normal plot', thetaformatter='pi', rlines=5, gridalpha=1, gridlinestyle=':',\n", + " rlabelpos=180, color='gray8', ticklabelweight='bold'\n", + ")\n", + "\n", + "# Sector plot\n", + "axs[1].format(\n", + " title='Sector plot', thetadir=-1, thetalines=90, thetalim=(0, 270), theta0='N',\n", + " rlim=(0, 22), rlines=5\n", + ")\n", + "\n", + "# Annular plot\n", + "axs[2].format(\n", + " title='Annular plot', thetadir=-1, thetalines=10,\n", + " r0=0, rlim=(10, 22), rformatter='null', rlocator=2\n", + ")" ] }, { @@ -91,30 +105,39 @@ "source": [ "import proplot as plot\n", "plot.rc.coastlinewidth = plot.rc.linewidth = 0.8\n", - "f, axs = plot.subplots(ncols=2, axwidth=2.5,\n", - " proj='robin', proj_kw={'lon_0': 180})\n", - "axs.format(suptitle='Simple projection axes demo',\n", - " coast=True, latlines=30, lonlines=60)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", + "\n", + "# Simple figure with just one projection\n", + "f, axs = plot.subplots(\n", + " ncols=2, axwidth=2.5,\n", + " proj='robin', proj_kw={'lon_0': 180}\n", + ")\n", + "axs.format(\n", + " suptitle='Figure with single projection',\n", + " coast=True, latlines=30, lonlines=60\n", + ")\n", + "\n", + "# Complex figure with different projections\n", "f, axs = plot.subplots(\n", " hratios=(1.5, 1, 1, 1, 1),\n", - " basemap={(1, 3, 5, 7, 9): False,\n", - " (2, 4, 6, 8, 10): True},\n", - " proj={(1, 2): 'mill', (3, 4): 'cyl', (5, 6): 'moll',\n", - " (7, 8): 'sinu', (9, 10): 'npstere'},\n", - " ncols=2, nrows=5) # , proj_kw={'lon_0':0})\n", - "axs.format(suptitle='Complex projection axes demo')\n", + " basemap={\n", + " (1, 3, 5, 7, 9): False,\n", + " (2, 4, 6, 8, 10): True\n", + " },\n", + " proj={\n", + " (1, 2): 'mill',\n", + " (3, 4): 'cyl',\n", + " (5, 6): 'moll',\n", + " (7, 8): 'sinu',\n", + " (9, 10): 'npstere'\n", + " },\n", + " ncols=2, nrows=5\n", + ")\n", + "axs.format(suptitle='Figure with several projections')\n", "axs.format(coast=True, latlines=30, lonlines=60)\n", "axs[:, 1].format(labels=True, lonlines=plot.arange(-180, 179, 60))\n", - "axs.format(collabels=['Cartopy examples', 'Basemap examples'])" + "axs[-1, -1].format(labels=True, lonlines=30)\n", + "axs.format(collabels=['Cartopy projections', 'Basemap projections'])\n", + "plot.rc.reset()" ] }, { @@ -145,33 +168,36 @@ "source": [ "import proplot as plot\n", "import numpy as np\n", - "projs = ['cyl', 'merc', 'mill', 'lcyl', 'tmerc',\n", - " 'robin', 'hammer', 'moll', 'kav7', 'aitoff', 'wintri', 'sinu',\n", - " 'geos', 'ortho', 'nsper', 'aea', 'eqdc', 'lcc', 'gnom',\n", - " 'npstere', 'nplaea', 'npaeqd', 'npgnom', 'igh',\n", - " 'eck1', 'eck2', 'eck3', 'eck4', 'eck5', 'eck6']\n", + "\n", + "# Table of cartopy projections\n", + "projs = [\n", + " 'cyl', 'merc', 'mill', 'lcyl', 'tmerc',\n", + " 'robin', 'hammer', 'moll', 'kav7', 'aitoff', 'wintri', 'sinu',\n", + " 'geos', 'ortho', 'nsper', 'aea', 'eqdc', 'lcc', 'gnom',\n", + " 'npstere', 'nplaea', 'npaeqd', 'npgnom', 'igh',\n", + " 'eck1', 'eck2', 'eck3', 'eck4', 'eck5', 'eck6'\n", + "]\n", "f, axs = plot.subplots(ncols=3, nrows=10, proj=projs)\n", - "axs.format(land=True, reso='lo', labels=False,\n", - " suptitle='Table of cartopy projections')\n", + "axs.format(\n", + " land=True, reso='lo', labels=False,\n", + " suptitle='Table of cartopy projections'\n", + ")\n", "for proj, ax in zip(projs, axs):\n", - " ax.format(title=proj, titleweight='bold', labels=False)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import proplot as plot\n", - "import numpy as np\n", - "projs = ['cyl', 'merc', 'mill', 'cea', 'gall', 'sinu',\n", - " 'eck4', 'robin', 'moll', 'kav7', 'hammer', 'mbtfpq',\n", - " 'geos', 'ortho', 'nsper',\n", - " 'vandg', 'aea', 'eqdc', 'gnom', 'cass', 'lcc',\n", - " 'npstere', 'npaeqd', 'nplaea']\n", + " ax.format(title=proj, titleweight='bold', labels=False)\n", + " \n", + "# Table of basemap projections\n", + "projs = [\n", + " 'cyl', 'merc', 'mill', 'cea', 'gall', 'sinu',\n", + " 'eck4', 'robin', 'moll', 'kav7', 'hammer', 'mbtfpq',\n", + " 'geos', 'ortho', 'nsper',\n", + " 'vandg', 'aea', 'eqdc', 'gnom', 'cass', 'lcc',\n", + " 'npstere', 'npaeqd', 'nplaea'\n", + "]\n", "f, axs = plot.subplots(ncols=3, nrows=8, basemap=True, proj=projs)\n", - "axs.format(land=True, labels=False, suptitle='Table of basemap projections')\n", + "axs.format(\n", + " land=True, labels=False,\n", + " suptitle='Table of basemap projections'\n", + ")\n", "for proj, ax in zip(projs, axs):\n", " ax.format(title=proj, titleweight='bold', labels=False)" ] @@ -204,32 +230,48 @@ "f, axs = plot.subplots(\n", " nrows=2, axwidth=4.5,\n", " proj='pcarree', basemap={1: False, 2: True},\n", - " proj_kw={2: {'llcrnrlon': -20, 'llcrnrlat': -10, 'urcrnrlon': 180, 'urcrnrlat': 50}})\n", + " proj_kw={2: {'llcrnrlon': -20, 'llcrnrlat': -10, 'urcrnrlon': 180, 'urcrnrlat': 50}}\n", + ")\n", + "\n", "# Ordinary projection\n", "axs.format(\n", " land=True, labels=True, lonlines=20,\n", - " latlines=20, suptitle='Zooming into projections')\n", - "axs[0].format(lonlim=(-140, 60), latlim=(-10, 50),\n", - " labels=True, title='Cartopy example')\n", + " latlines=20, suptitle='Zooming into projections'\n", + ")\n", + "axs[0].format(\n", + " lonlim=(-140, 60), latlim=(-10, 50),\n", + " labels=True, title='Cartopy example'\n", + ")\n", "axs[1].format(title='Basemap example')\n", + "\n", "# Polar projection\n", "f, axs = plot.subplots(\n", " ncols=2, axwidth=2.2,\n", - " proj={1: 'splaea', 2: 'npaeqd'}, basemap={1: False, 2: True},\n", - " proj_kw={2: {'boundinglat': 60}})\n", + " proj={1: 'splaea', 2: 'npaeqd'},\n", + " basemap={1: False, 2: True},\n", + " proj_kw={2: {'boundinglat': 60}}\n", + ")\n", "axs.format(\n", " land=True, latlines=10, latmax=80,\n", - " suptitle='Zooming into polar projections')\n", + " suptitle='Zooming into polar projections'\n", + ")\n", "axs[0].format(boundinglat=-60, title='Cartopy example')\n", "axs[1].format(title='Basemap example')\n", + "\n", "# Example from basemap website\n", "f, axs = plot.subplots(\n", - " ncols=2, axwidth=2, proj='lcc', basemap={1: False, 2: True},\n", - " proj_kw={1: {'lon_0': 0}, 2: {'lon_0': -100, 'lat_0': 45, 'width': 8e6, 'height': 8e6}})\n", + " ncols=2, axwidth=2, proj='lcc',\n", + " basemap={1: False, 2: True},\n", + " proj_kw={\n", + " 1: {'lon_0': 0},\n", + " 2: {'lon_0': -100, 'lat_0': 45, 'width': 8e6, 'height': 8e6}\n", + " }\n", + ")\n", "axs.format(suptitle='Zooming into specific regions', land=True)\n", "axs[0].format(\n", " title='Cartopy example', land=True,\n", - " lonlim=(-20, 50), latlim=(30, 70))\n", + " lonlim=(-20, 50), latlim=(30, 70)\n", + ")\n", "axs[1].format(title='Basemap example', land=True)" ] }, @@ -266,6 +308,8 @@ "y = plot.arange(-60, 60+1, 30)\n", "state = np.random.RandomState(51423)\n", "data = state.rand(len(y), len(x))\n", + "\n", + "# Same figure with and without \"global coverage\"\n", "titles = ('Geophysical data demo', 'Global coverage demo')\n", "for globe in (False, True,):\n", " f, axs = plot.subplots(\n", @@ -275,8 +319,10 @@ " cmap = ('sunset', 'sunrise')[i % 2]\n", " if i < 2:\n", " m = ax.contourf(x, y, data, cmap=cmap, globe=globe, extend='both')\n", - " f.colorbar(m, loc='b', span=i+1, label='values',\n", - " tickminor=False, extendsize='1.7em')\n", + " f.colorbar(\n", + " m, loc='b', span=i+1, label='values',\n", + " tickminor=False, extendsize='1.7em'\n", + " )\n", " else:\n", " ax.pcolor(x, y, data, cmap=cmap, globe=globe, extend='both')\n", " if globe:\n", @@ -285,10 +331,13 @@ " for cmd in (np.sin, np.cos):\n", " iy = cmd(ix*np.pi/180)*60\n", " ax.plot(ix, iy, color='k', lw=0, marker='o')\n", - " axs.format(suptitle=titles[globe],\n", - " collabels=['Cartopy example', 'Basemap example'],\n", - " rowlabels=['Contourf', 'Pcolor'], latlabels='r', lonlabels='b', lonlines=90,\n", - " abc=True, abcstyle='a)', abcloc='ul', abcborder=False)" + " axs.format(\n", + " suptitle=titles[globe],\n", + " collabels=['Cartopy example', 'Basemap example'],\n", + " rowlabels=['Contourf', 'Pcolor'],\n", + " latlabels='r', lonlabels='b', lonlines=90,\n", + " abc=True, abcstyle='a)', abcloc='ul', abcborder=False\n", + " )" ] }, { @@ -316,21 +365,33 @@ "import proplot as plot\n", "f, axs = plot.subplots(\n", " [[1, 1, 2], [3, 3, 3]],\n", - " axwidth=4, proj={1: 'robin', 2: 'ortho', 3: 'wintri'})\n", + " axwidth=4, proj={1: 'robin', 2: 'ortho', 3: 'wintri'}\n", + ")\n", + "axs.format(\n", + " suptitle='Projection axes formatting demo',\n", + " collabels=['Column 1', 'Column 2'],\n", + " abc=True, abcstyle='A.', abcloc='ul', abcborder=False, linewidth=1.5\n", + ")\n", + "\n", + "# Styling projections in different ways\n", "ax = axs[0]\n", - "ax.format(title='Robinson map', land=True, landcolor='navy blue', facecolor='pale blue',\n", - " coastcolor='gray5', borderscolor='gray5', innerborderscolor='gray5',\n", - " geogridlinewidth=1, geogridcolor='gray5', geogridalpha=1,\n", - " coast=True, innerborders=True, borders=True)\n", + "ax.format(\n", + " title='Robinson map', land=True, landcolor='navy blue', facecolor='pale blue',\n", + " coastcolor='gray5', borderscolor='gray5', innerborderscolor='gray5',\n", + " geogridlinewidth=1, geogridcolor='gray5', geogridalpha=1,\n", + " coast=True, innerborders=True, borders=True\n", + ")\n", "ax = axs[1]\n", - "ax.format(title='Ortho map', reso='med', land=True, coast=True, latlines=10, lonlines=15,\n", - " landcolor='mushroom', suptitle='Projection axes formatting demo',\n", - " facecolor='petrol', coastcolor='charcoal', coastlinewidth=0.8, geogridlinewidth=1)\n", + "ax.format(\n", + " title='Ortho map', reso='med', land=True, coast=True, latlines=10, lonlines=15,\n", + " landcolor='mushroom', suptitle='Projection axes formatting demo',\n", + " facecolor='petrol', coastcolor='charcoal', coastlinewidth=0.8, geogridlinewidth=1\n", + ")\n", "ax = axs[2]\n", - "ax.format(land=True, facecolor='ocean blue', landcolor='almond', title='Winkel tripel map',\n", - " lonlines=60, latlines=15)\n", - "axs.format(suptitle='Projection axes formatting demo', collabels=['col 1', 'col 2'],\n", - " abc=True, abcstyle='A.', abcloc='ul', abcborder=False, linewidth=1.5)" + "ax.format(\n", + " land=True, facecolor='ocean blue', landcolor='almond', title='Winkel tripel map',\n", + " lonlines=60, latlines=15\n", + ")" ] } ], diff --git a/docs/subplots.ipynb b/docs/subplots.ipynb index ca43226c8..35d0da2f0 100644 --- a/docs/subplots.ipynb +++ b/docs/subplots.ipynb @@ -49,20 +49,26 @@ "outputs": [], "source": [ "import proplot as plot\n", + "# Same plot with different reference axes\n", "for ref in (1, 2):\n", " f, axs = plot.subplots(\n", " ref=ref, nrows=3, ncols=3, wratios=(3, 2, 2),\n", - " axwidth=1.1, share=0)\n", + " axwidth=1.1, share=0\n", + " )\n", " axs[ref-1].format(\n", " title='reference axes', titleweight='bold',\n", - " titleloc='uc', titlecolor='red9')\n", + " titleloc='uc', titlecolor='red9'\n", + " )\n", " axs[4].format(\n", " title='title\\ntitle\\ntitle',\n", - " suptitle='Tight layout with simple grids')\n", + " suptitle='Tight layout with simple grids'\n", + " )\n", " axs[1].format(ylabel='ylabel\\nylabel\\nylabel')\n", " axs[:4:2].format(xlabel='xlabel\\nxlabel\\nxlabel')\n", - " axs.format(rowlabels=['Row 1', 'Row 2', 'Row 3'],\n", - " collabels=['Column 1', 'Column 2', 'Column 3'])" + " axs.format(\n", + " rowlabels=['Row 1', 'Row 2', 'Row 3'],\n", + " collabels=['Column 1', 'Column 2', 'Column 3']\n", + " )" ] }, { @@ -72,25 +78,33 @@ "outputs": [], "source": [ "import proplot as plot\n", - "for ref in (4, 2):\n", + "# Same plot with different reference axes\n", + "for ref in (3, 2):\n", " f, axs = plot.subplots(\n", - " [[1, 1, 2], [3, 3, 2], [4, 5, 5], [4, 6, 6]],\n", - " hratios=(1, 1, 2, 2), wratios=(3, 2, 3),\n", - " ref=ref, axwidth=1.1, span=False)\n", + " [[1, 1, 2], [3, 4, 4]],\n", + " hratios=(1, 1.5), wratios=(3, 2, 2),\n", + " ref=ref, axwidth=1.1, span=False\n", + " )\n", " axs[ref-1].format(\n", " title='reference axes', titleweight='bold',\n", - " titleloc='uc', titlecolor='red9')\n", + " titleloc='uc', titlecolor='red9'\n", + " )\n", " axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Super title')\n", " axs[0].format(xlabel='xlabel\\nxlabel\\nxlabel')\n", " axs[1].format(\n", " ylabel='ylabel\\nylabel\\nylabel', ytickloc='both',\n", - " yticklabelloc='both', ctitle='Title')\n", - " axs[2:4].format(\n", + " yticklabelloc='both', ctitle='Title'\n", + " )\n", + " axs[2].format(\n", " yformatter='null', ctitle='Title',\n", - " ytickloc='both', yticklabelloc='both')\n", - " axs[4:].format(yformatter='null', xlabel='xlabel\\nxlabel\\nxlabel')\n", - " axs.format(suptitle='Tight layout with complex grids', rowlabels=[\n", - " 'Row 1', 'Row 2', 'Row 3'], collabels=['Column 1', 'Column 2'])" + " ytickloc='both', yticklabelloc='both'\n", + " )\n", + " axs[3].format(yformatter='null', xlabel='xlabel\\nxlabel\\nxlabel')\n", + " axs.format(\n", + " suptitle='Tight layout with complex grids',\n", + " rowlabels=['Row 1', 'Row 2'],\n", + " collabels=['Column 1', 'Column 2']\n", + " )" ] }, { @@ -106,7 +120,7 @@ "raw_mimetype": "text/restructuredtext" }, "source": [ - "ProPlot supports *arbitrary physical units* for controlling the figure `width` and `height`; the reference subplot `axwidth` and `axheight`; the gridspec spacing values `left`, `right`, `bottom`, `top`, `wspace`, and `hspace`; and in a few other places.\n", + "ProPlot supports *arbitrary physical units* for controlling the figure `width` and `height`; the reference subplot `axwidth` and `axheight`; the gridspec spacing values `left`, `right`, `bottom`, `top`, `wspace`, and `hspace`; and in a few other places, e.g. `~proplot.axes.Axes.panel` and `~proplot.axes.Axes.colorbar` widths.\n", "\n", "If a sizing argument is numeric, the units are inches or points, and if string, the units are interpreted by `~proplot.utils.units`. A table of acceptable units is found in the `~proplot.utils.units` documentation. They include centimeters, millimeters, pixels, `em-heights `__, and `points `__." ] @@ -122,11 +136,13 @@ "with plot.rc.context(small='12px', large='15px', linewidth='0.5mm'):\n", " f, axs = plot.subplots(\n", " ncols=3, width='13cm', height='2in',\n", - " wspace=('10pt', '20pt'), right='10mm')\n", - "panel = axs[2].panel_axes('r', width='2em')\n", - "axs.format(suptitle='Arguments with arbitrary units',\n", - " xlabel='x axis', ylabel='y axis')\n", - "plot.rc.reset()" + " wspace=('10pt', '20pt'), right='10mm'\n", + " )\n", + " panel = axs[2].panel_axes('r', width='2em')\n", + "axs.format(\n", + " suptitle='Arguments with arbitrary units',\n", + " xlabel='x axis', ylabel='y axis'\n", + ")" ] }, { @@ -153,8 +169,10 @@ "source": [ "import proplot as plot\n", "f, axs = plot.subplots(nrows=8, ncols=8, axwidth=0.7, space=0)\n", - "axs.format(abc=True, abcloc='ur', xlabel='x axis', ylabel='y axis',\n", - " xticks=[], yticks=[], suptitle='Flush subplot grid')" + "axs.format(\n", + " abc=True, abcloc='ur', xlabel='x axis', ylabel='y axis',\n", + " xticks=[], yticks=[], suptitle='Flush subplot grid'\n", + ")" ] }, { @@ -193,13 +211,18 @@ "for scale in (1, 3, 7, 0.2):\n", " data = scale * (state.rand(N, M) - 0.5).cumsum(axis=0)[N//2:, :]\n", " datas.append(data)\n", + "# Same plot with different sharing and spanning settings\n", "for share in (0, 1, 2, 3):\n", - " f, axs = plot.subplots(ncols=4, aspect=1, axwidth=1.2,\n", - " sharey=share, spanx=share//2)\n", + " f, axs = plot.subplots(\n", + " ncols=4, aspect=1, axwidth=1.2,\n", + " sharey=share, spanx=share//2\n", + " )\n", " for ax, data in zip(axs, datas):\n", " ax.plot(data, cycle=colors)\n", - " ax.format(suptitle=f'Axis-sharing level: {share}, spanning labels {[\"off\",\"on\"][share//2]}',\n", - " grid=False, xlabel='spanning', ylabel='shared')" + " ax.format(\n", + " suptitle=f'Axis-sharing level: {share}, spanning labels {[\"off\",\"on\"][share//2]}',\n", + " grid=False, xlabel='spanning', ylabel='shared'\n", + " )" ] }, { @@ -214,15 +237,19 @@ "plot.rc.cycle = 'Set3'\n", "state = np.random.RandomState(51423)\n", "titles = ['With redundant labels', 'Without redundant labels']\n", + "# Same plot with and without default sharing settings\n", "for mode in (0, 1):\n", - " f, axs = plot.subplots(nrows=4, ncols=4, share=3*mode,\n", - " span=1*mode, axwidth=1)\n", + " f, axs = plot.subplots(\n", + " nrows=4, ncols=4, share=3*mode,\n", + " span=1*mode, axwidth=1\n", + " )\n", " for ax in axs:\n", " ax.plot((state.rand(100, 20) - 0.4).cumsum(axis=0))\n", " axs.format(\n", " xlabel='xlabel', ylabel='ylabel', suptitle=titles[mode],\n", - " abc=mode, abcloc='ul',\n", - " grid=False, xticks=25, yticks=5)" + " abc=True, abcloc='ul',\n", + " grid=False, xticks=25, yticks=5\n", + " )" ] } ],