"""
Plotting utilities and matplotlib rcParams customization.
"""
import os
import sys
import numpy as np
import matplotlib as mpl
from cycler import cycler
import matplotlib.pyplot as plt
# from matplotlib.colors import ListedColormap
from datopy.util._numpydoc_validate import numpydoc_validate_module
# plt.close()
# x = np.linspace(0, 1, 20)
# cmap = mpl.colormaps.get_cmap("viridis").resampled(20)
# plt.imshow(x[:, np.newaxis].T, cmap=cmap)
# plt.axis('off')
# plt.show()
[docs]
class outputoff:
"""
Define a context in which all outputs are suppressed.
Intended for use with pyplot to suppress intermittent printouts.
"""
def __enter__(self):
"""
Setup.
"""
self.stdout = sys.stdout
# Redirect stdout to null device
sys.stdout = open(os.devnull, 'w')
def __exit__(self, exc_type, exc_value, traceback):
"""
Teardown.
Parameters
----------
exc_type : Exception type
exc_value : Exception value
traceback : Traceback
"""
sys.stdout.close()
sys.stdout = self.stdout
# TODO: clean up plot examples using approach like this:
# https://github.com/arviz-devs/arviz/blob/main/arviz/plots/autocorrplot.py
# https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.plot.html#pandas.DataFrame.plot
[docs]
def customize_plots() -> None:
r"""
Set custom matplotlib rcParams.
Handles default styling for all matplotlib plotting functions and
functions built upon matplotlib (e.g., Seaborn plotters).
Notes
-----
Contrast with the default matplotlib rc file:
https://matplotlib.org/stable/users/explain/customizing.html#the-matplotlibrc-file.
Examples
--------
.. code-block:: python plot
.. plot::
:context: close-figs
>>> import matplotlib
>>> import matplotlib.pyplot as plt
>>> from datopy.stylesheet import customize_plots
Define some data
>>> import numpy as np
>>> x = np.linspace(0, 2 * np.pi, 1000)
Define a couple plotting functions
>>> def single_panel_plot(x):
... ax = plt.subplot(111)
... ax.plot(x, np.sin(x), label="$sin(x)$")
... ax.plot(x, np.cos(x), label="$cos(x)$")
... ax.set(xlabel="$x$", frame_on=False)
... ax.set_ylabel("$f(x)$", rotation=0)
... ax.legend()
... ax.grid()
... plt.show()
>>> def multi_panel_plot(x):
... fig, axs = plt.subplots(2, 2, sharex=True, sharey=True)
... axs = axs.flatten()
... for i, ax in enumerate(axs, start=1):
... ax.plot(x, np.sin(i/2 * x))
... ax.plot(x, np.cos(i/2 * x))
... ax.text(
... max(x) - 0.1, -1 + 0.1,
... f"$a = {i/2}$",
... size=12,
... ha="right",
... bbox={
... "boxstyle": "round",
... "alpha": 0.8,
... "facecolor": "white",
... }
... )
... ax.grid()
... fig.supylabel("$f(x)$", rotation=0)
... fig.supxlabel("$x$", x=.5)
... axs[0].legend(
... labels=["$sin(ax)$", "$cos(ax)$"],
... ncol=1,
... loc="lower left",
... )
... plt.show()
Create the plots
..
# The first two examples are before styling;
# the following two are after styling.
# >>> single_panel_plot() # /doctest: +SKIP
# >>> multi_panel_plot() # /doctest: +SKIP
>>> customize_plots()
>>> single_panel_plot(x) # /doctest: +SKIP
>>> multi_panel_plot(x) # /doctest: +SKIP
"""
# -- General properties --------------------------------------------------
# Font face and sizes
mpl.rcParams['font.family'] = 'sans-serif'
# NOTE: will need to be reverted to the default for use in a notebook.
# mpl.rcParams['font.sans-serif'] = "Verdana"
mpl.rcParams['font.size'] = 9 # default font sizes
mpl.rcParams['axes.titlesize'] = 14 # large
mpl.rcParams['axes.labelsize'] = 12 # medium
mpl.rcParams['xtick.labelsize'] = 10 # medium
mpl.rcParams['ytick.labelsize'] = 10 # medium
mpl.rcParams['legend.fontsize'] = 10 # medium
mpl.rcParams['legend.title_fontsize'] = 11 # None (same as default axes)
mpl.rcParams['figure.titlesize'] = 15 # large (suptitle size)
mpl.rcParams['figure.labelsize'] = 13 # large (sup[x|y]label size)
# Spines and ticks
mpl.rcParams['axes.spines.top'] = True
mpl.rcParams['axes.spines.right'] = True
mpl.rcParams['axes.linewidth'] = .7
mpl.rcParams['axes.edgecolor'] = 'black'
mpl.rcParams['xtick.direction'] = 'out'
mpl.rcParams['ytick.direction'] = 'out'
mpl.rcParams['xtick.major.size'] = 0 # default: 3.5
mpl.rcParams['ytick.major.size'] = 0 # default: 3.5
# mpl.rcParams['xtick.major.width'] = 0.8
# mpl.rcParams['ytick.major.width'] = 0.8
# Grid
# lines at {major, minor, both} ticks
mpl.rcParams['axes.grid.which'] = 'major'
mpl.rcParams['grid.linestyle'] = '-'
mpl.rcParams['grid.color'] = '#CCCCCC'
mpl.rcParams['grid.linewidth'] = 0.8
mpl.rcParams['grid.alpha'] = .2
# Label placement
mpl.rcParams['axes.titlelocation'] = 'center' # {left, right, center}
mpl.rcParams['axes.titlepad'] = 7.5 # 6
mpl.rcParams['axes.labelpad'] = 7.5 # 4
# mpl.rcParams['xtick.major.pad'] = 3.5 # dist to major tick label in pts
# mpl.rcParams['ytick.major.pad'] = 3.5
# Discrete color cycle (and continuous map)
# mpl.rcParams['axes.prop_cycle'] = cycler(
# color=['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728']
# )
# mpl.rcParams['axes.prop_cycle'] = cycler(
# color=sns.color_palette("PiYG", n_colors=6)
# )
mpl.rcParams['axes.prop_cycle'] = cycler(
color=mpl.cm.PiYG(np.linspace(0.15, .85, 6)) # type: ignore
)
# Legend properties
mpl.rcParams['legend.loc'] = 'best'
mpl.rcParams['legend.frameon'] = False
mpl.rcParams['legend.loc'] = 'best'
# Legend padding
# mpl.rcParams['legend.borderpad'] = 0.4 # border whitespace
# mpl.rcParams['legend.labelspacing'] = 0.5 # vert space between entries
mpl.rcParams['legend.handlelength'] = 1.35 # length of the legend lines
# mpl.rcParams['legend.handleheight'] = 0.7 # height of the legend handle
mpl.rcParams['legend.handletextpad'] = 0.8 # space btwn leg lines/text
mpl.rcParams['legend.borderaxespad'] = 0.5 # border btwn axes/leg edge
mpl.rcParams['legend.columnspacing'] = 1.0 # column separation
# Space-filling object properties (e.g., polygons/circles, bars/scatter)
mpl.rcParams['patch.edgecolor'] = 'black' # if forced, else not filled
mpl.rcParams['patch.force_edgecolor'] = 1
mpl.rcParams['patch.linewidth'] = .4 # edgewidth (default: .5)
# -- Object-specific properties ------------------------------------------
# Scatter properties
# mpl.rcParams['scatter.edgecolors'] = 'black' # alt: 'face' (match edges)
# Line properties
mpl.rcParams['lines.markersize'] = 6
mpl.rcParams['lines.linewidth'] = 2
# Bar properties
# NOTE: No global styling parameter exists for the following:
# mpl.rcParams['bar.width'] = 0.8
# Error properties
mpl.rcParams['errorbar.capsize'] = 3
# NOTE: No global styling parameter exists for the following:
# mpl.rcParams['errorbar.color'] = 'black'
# mpl.rcParams['errorbar.linewidth'] = 1.5
# Contour properties
# if `none`, falls back to line.linewidth
mpl.rcParams['contour.linewidth'] = 1
# Histogram properties
# hist.bins: 10 # the default number of histogram bins or 'auto'
# Box properties
# box
mpl.rcParams['boxplot.boxprops.linewidth'] = 0 # box outline (0.5)
# mpl.rcParams['boxplot.boxprops.color'] = 'none' # alt: 'black' (check)
# box line to cap
mpl.rcParams['boxplot.whiskerprops.linewidth'] = .65
mpl.rcParams['boxplot.whiskerprops.linestyle'] = '--'
# mpl.rcParams['boxplot.whiskerprops.color'] = 'black' # (check)
# box cap line
mpl.rcParams['boxplot.capprops.linewidth'] = .75
# mpl.rcParams['boxplot.capprops.color'] = 'black' # (check)
# box median line
mpl.rcParams['boxplot.medianprops.linewidth'] = 1
mpl.rcParams['boxplot.medianprops.linestyle'] = '-'
# mpl.rcParams['boxplot.medianprops.color'] = 'black' # (check)
mpl.rcParams['boxplot.meanprops.linewidth'] = 1
mpl.rcParams['boxplot.meanprops.linestyle'] = '-'
# mpl.rcParams['boxplot.meanprops.color'] = 'black' # (check)
# box scatter
mpl.rcParams['boxplot.flierprops.markerfacecolor'] = 'none'
mpl.rcParams['boxplot.flierprops.markeredgewidth'] = .65
mpl.rcParams['boxplot.flierprops.marker'] = 'o'
# mpl.rcParams['boxplot.flierprops.markersize'] = 6 # (check)
# mpl.rcParams['boxplot.flierprops.linewidth'] = 0 # (check)
# mpl.rcParams['boxplot.flierprops.markeredgecolor'] = 'black' # (check)
# mpl.rcParams['boxplot.flierprops.color'] = 'black' # (check)
# -- Figure padding ------------------------------------------------------
# Figure layout
# auto-make plot elements fit on figure
mpl.rcParams['figure.autolayout'] = True
mpl.rcParams['figure.constrained_layout.use'] = True # apply tight layout
# Subplot padding (all dims are a fraction of the fig width and height)/
# NOTE: not compatible with constrained_layout.
#
# mpl.rcParams['figure.subplot.left'] = .125 # left side
# mpl.rcParams['figure.subplot.right'] = 0.9 # right side of subplots
# mpl.rcParams['figure.subplot.bottom'] = 0.11 # bottom of subplots
# mpl.rcParams['figure.subplot.top'] = 0.88 # top of subplots
# Reserved space between subplots
# mpl.rcParams['figure.subplot.wspace'] = 0.2 # width
# mpl.rcParams['figure.subplot.hspace'] = 0.2 # height
# Constrained layout padding (not compatible with autolayout)
# mpl.rcParams['figure.constrained_layout.h_pad'] = 0.04167
# mpl.rcParams['figure.constrained_layout.w_pad'] = 0.04167
# Constrained layout spacing between subplots, relative to subplot sizes.
# Is much smaller than tight_layout (figure.subplot.{hspace, wspace)
# as constrained_layout already takes surrounding text
# (titles, labels, # ticklabels) into account.
# NOTE: not compatible with autolayout.
#
# mpl.rcParams['figure.constrained_layout.hspace'] = 0.02
# mpl.rcParams['figure.constrained_layout.wspace'] = 0.02
# -- Other ---------------------------------------------------------------
# Figure size and quality
mpl.rcParams['figure.dpi'] = 100 # NOTE: Alters figure size
mpl.rcParams['figure.figsize'] = (5, 5) # (6, 4), (6.4, 4.8)
# Figure saving settings
mpl.rcParams['savefig.transparent'] = False
mpl.rcParams['savefig.format'] = 'svg' # {png, ps, pdf, svg}
mpl.rcParams['savefig.dpi'] = 330
# Set format/quality of inline figures in Jupyter notebooks
# %config InlineBackend.figure_format = 'svg'
[docs]
class customstyle:
"""
Define plotting context given by :func:`datopy.stylesheet.customize_plots`.
Examples
--------
.. code-block:: python plot
.. plot::
:context: close-figs
:width: 100%
:align: left
..
# >>> with customstyle():
# ... plt.plot([1,2,3], [4,5,6])
# ... plt.show() # /doctest: +SKIP
# >>> plt.plot([1,2,3], [4,5,6]) # doctest: +SKIP
# >>> plt.show() # /doctest: +SKIP
"""
def __enter__(self):
"""
Setup.
"""
# TODO: ?package customize_plots with context manager
# to avoid circular dependency and double execution.
from datopy.stylesheet import customize_plots
customize_plots()
return self
def __exit__(self, *exc):
"""
Teardown.
"""
pass
def main():
import doctest
from datopy.workflow import doctest_function
# Comment out (2) to run all tests in script; (1) to run specific tests
doctest.testmod(verbose=True)
# doctest_function(customize_plots, globs=globals())
numpydoc_validate_module(sys.modules['__main__'])
if __name__ == "__main__":
main()