"""
A helper function that updates the default matplotlib (and by
extension, Seaborn) style parameters for a more uniform and readable
look.
"""
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import seaborn as sns
from cycler import cycler
import os
import sys
[docs]
class outputoff:
"""
Context manager to suppress outputs.
Intended for use with pyplot to suppress intermittent printouts.
"""
def __enter__(self):
self.stdout = sys.stdout
# Redirect stdout to null device
sys.stdout = open(os.devnull, 'w')
def __exit__(self, exc_type, exc_value, traceback):
"""
Args:
exc_type : Exception type
exc_value : Exception value
traceback : Traceback
"""
sys.stdout.close()
sys.stdout = self.stdout
# TODO attempt to 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"""
Sets custom matplotlib rcParams to handle default styling for
all matplotlib plotting functions and functions built upon matplotlib
(e.g., Seaborn).
Notes
-----
- When run in isolation, will apply the new style parameters globally.
- When provided as context to a plotting routine, styles only that plot.
- 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
:width: 100%
:align: left
>>> 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():
... 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$')
... ax.set_ylabel('$y$')
... # ax.set_title("A minimal plot")
... ax.legend()
... ax.grid()
... ax.set_frame_on(False)
... # ax.tick_params(bottom=False, left=False)
... plt.show()
>>> def multi_panel_plot():
... 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.set_title(f"$a = {i}$")
... # if i > 2: ax.set_xlabel('$x$')
... # if i % 2 != 0: ax.set_ylabel('$y$')
... ax.grid()
... ax.text(
... 2 * np.pi - 0.1, -1 + 0.1,
... f"$a = {i}$",
... size=12,
... horizontalalignment="right",
... bbox={
... "boxstyle": "round",
... "alpha": 0.8,
... "facecolor": "white",
... }
... )
... fig.supylabel("$y$")
... fig.supxlabel("$x$", x=.57)
... # fig.suptitle("A multi-panel plot", x=.57)
... 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() # /doctest: +SKIP
>>> multi_panel_plot() # /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)
)
# 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:
"""Wrapper and context manager for `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):
# 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):
pass