Source code for pyleoclim.utils.plotting

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Plotting utilities, leveraging Matplotlib.
"""

__all__ = ['set_style','closefig', 'savefig']


import matplotlib.pyplot as plt
import pathlib
import matplotlib as mpl
import numpy as np
from matplotlib.patches import Rectangle
from matplotlib.collections import PatchCollection
from matplotlib.colors import ListedColormap


[docs]def scatter_xy(x,y,c=None, figsize=None, xlabel=None, ylabel=None, title=None, xlim=None, ylim=None, savefig_settings=None, ax=None, legend=True, plot_kwargs=None, lgd_kwargs=None): """ Make scatter plot. Parameters ---------- x : numpy.array x value y : numpy.array y value c : TYPE, optional DESCRIPTION. The default is None. figsize : list, optional A list of two integers indicating the dimension of the figure. The default is None. xlabel : str, optional x-axis label. The default is None. ylabel : str, optional y-axis label. The default is None. title : str, optional Title for the plot. The default is None. xlim : list, optional Limits for the x-axis. The default is None. ylim : list, optional Limits for the y-axis. The default is None. savefig_settings : dict, optional the dictionary of arguments for plt.savefig(); some notes below: - "path" must be specified; it can be any existing or non-existing path, with or without a suffix; if the suffix is not given in "path", it will follow "format" - "format" can be one of {"pdf", "eps", "png", "ps"} The default is None. ax : pyplot.axis, optional The axis object. The default is None. legend : bool, optional Whether to include a legend. The default is True. plot_kwargs : dict, optional the keyword arguments for ax.plot(). The default is None. lgd_kwargs : dict, optional the keyword arguments for ax.legend(). The default is None. Returns ------- ax : the pyplot.axis object """ savefig_settings = {} if savefig_settings is None else savefig_settings.copy() plot_kwargs = {} if plot_kwargs is None else plot_kwargs.copy() lgd_kwargs = {} if lgd_kwargs is None else lgd_kwargs.copy() if ax is None: fig, ax = plt.subplots(figsize=figsize) ax.scatter(x, y, c=c, **plot_kwargs) if xlabel is not None: ax.set_xlabel(xlabel) if ylabel is not None: ax.set_ylabel(ylabel) if title is not None: ax.set_title(title) if xlim is not None: ax.set_xlim(xlim) if ylim is not None: ax.set_ylim(ylim) if legend: ax.legend(**lgd_kwargs) else: ax.legend().remove() if 'fig' in locals(): if 'path' in savefig_settings: savefig(fig, settings=savefig_settings) return fig, ax else: return ax
[docs]def plot_scatter_xy(x1,y1,x2,y2, figsize=None, xlabel=None, ylabel=None, title=None, xlim=None, ylim=None, savefig_settings=None, ax=None, legend=True, plot_kwargs=None, lgd_kwargs=None): ''' Plot a scatter on top of a line plot. Parameters ---------- x1 : array x axis of timeseries1 - plotted as a line y1 : array values of timeseries1 - plotted as a line x2 : array x axis of scatter points y2 : array y of scatter points figsize : list a list of two integers indicating the figure size xlabel : str label for x-axis ylabel : str label for y-axis title : str the title for the figure xlim : list set the limits of the x axis ylim : list set the limits of the y axis ax : pyplot.axis the pyplot.axis object legend : bool plot legend or not lgd_kwargs : dict the keyword arguments for ax.legend() plot_kwargs : dict the keyword arguments for ax.plot() savefig_settings : dict the dictionary of arguments for plt.savefig(); some notes below: - "path" must be specified; it can be any existing or non-existing path, with or without a suffix; if the suffix is not given in "path", it will follow "format" - "format" can be one of {"pdf", "eps", "png", "ps"} Returns ------- ax : the pyplot.axis object See also -------- pyleoclim.utils.plotting.set_style : set different styles for the figures. Should be set before invoking the plotting functions pyleoclim.utils.plotting.savefig : save figures ''' # handle dict defaults savefig_settings = {} if savefig_settings is None else savefig_settings.copy() plot_kwargs = {} if plot_kwargs is None else plot_kwargs.copy() lgd_kwargs = {} if lgd_kwargs is None else lgd_kwargs.copy() if ax is None: fig, ax = plt.subplots(figsize=figsize) ax.plot(x1, y1, **plot_kwargs, color='green') ax.scatter(x2, y2, color='red') if xlabel is not None: ax.set_xlabel(xlabel) if ylabel is not None: ax.set_ylabel(ylabel) if title is not None: ax.set_title(title) if xlim is not None: ax.set_xlim(xlim) if ylim is not None: ax.set_ylim(ylim) if legend: ax.legend(**lgd_kwargs) else: ax.legend().remove() if 'fig' in locals(): if 'path' in savefig_settings: savefig(fig, settings=savefig_settings) return fig, ax else: return ax
[docs]def plot_xy(x, y, figsize=None, xlabel=None, ylabel=None, title=None, xlim=None, ylim=None,savefig_settings=None, ax=None, legend=True, plot_kwargs=None, lgd_kwargs=None, invert_xaxis=False, invert_yaxis=False): ''' Plot a timeseries Parameters ---------- x : array The time axis for the timeseries y : array The values of the timeseries figsize : list a list of two integers indicating the figure size xlabel : str label for x-axis ylabel : str label for y-axis title : str the title for the figure xlim : list set the limits of the x axis ylim : list set the limits of the y axis ax : pyplot.axis the pyplot.axis object legend : bool plot legend or not lgd_kwargs : dict the keyword arguments for ax.legend() plot_kwargs : dict the keyword arguments for ax.plot() mute : bool if True, the plot will not show; recommend to turn on when more modifications are going to be made on ax (going to be deprecated) savefig_settings : dict the dictionary of arguments for plt.savefig(); some notes below: - "path" must be specified; it can be any existing or non-existing path, with or without a suffix; if the suffix is not given in "path", it will follow "format" - "format" can be one of {"pdf", "eps", "png", "ps"} invert_xaxis : bool, optional if True, the x-axis of the plot will be inverted invert_yaxis : bool, optional if True, the y-axis of the plot will be inverted Returns ------- ax : the pyplot.axis object See Also -------- pyleoclim.utils.plotting.set_style : set different styles for the figures. Should be set before invoking the plotting functions pyleoclim.utils.plotting.savefig : save figures ''' # handle dict defaults savefig_settings = {} if savefig_settings is None else savefig_settings.copy() plot_kwargs = {} if plot_kwargs is None else plot_kwargs.copy() lgd_kwargs = {} if lgd_kwargs is None else lgd_kwargs.copy() if ax is None: fig, ax = plt.subplots(figsize=figsize) ax.plot(x, y, **plot_kwargs) if xlabel is not None: ax.set_xlabel(xlabel) if ylabel is not None: ax.set_ylabel(ylabel) if title is not None: ax.set_title(title) # TODO replace with ax.set_title(title, fontweight='bold') when all relevant plots use plot_xy if xlim is not None: ax.set_xlim(xlim) if ylim is not None: ax.set_ylim(ylim) if legend: ax.legend(**lgd_kwargs) else: ax.legend().remove() if invert_xaxis: ax.invert_xaxis() if invert_yaxis: ax.invert_yaxis() if 'fig' in locals(): if 'path' in savefig_settings: savefig(fig, settings=savefig_settings) return fig, ax else: return ax
[docs]def stripes_xy(x, y, ref_period, thickness = 1.0, LIM = 0.75, figsize=None, xlabel=None, ylabel=None, title=None, xlim=None, savefig_settings=None, ax=None, x_offset = 0.05, label_size = None, show_xaxis = False, invert_xaxis=False, top_label = None, bottom_label = None, label_color = None): ''' Represent y as an Ed Hawkins "warming stripes" pattern, as a function of x Credit: https://matplotlib.org/matplotblog/posts/warming-stripes/ Parameters ---------- x : array Independent variable y : array Dependent variable ref_period : 2-tuple or 2-vector indices of the reference period, in the form "(first, last)" thickness : float, optional vertical thickness of the stripe . The default is 1.0 LIM : float, optional size of the y-value range (default: 0.7) figsize : list a list of two integers indicating the figure size top_label : str the "title" label for the stripe bottom_label : str the "ylabel" explaining which variable is being plotted label_size : int size of the text in labels (in points). Default is the Matplotlib 'axes.labelsize'] rcParams xlim : list set the limits of the x axis x_offset : float value controlling the horizontal offset between stripes and labels (default = 0.05) show_xaxis : bool flag indicating whether or not the x-axis should be shown (default = False) ax : pyplot.axis the pyplot.axis object savefig_settings : dict the dictionary of arguments for plt.savefig(); some notes below: - "path" must be specified; it can be any existing or non-existing path, with or without a suffix; if the suffix is not given in "path", it will follow "format" - "format" can be one of {"pdf", "eps", "png", "ps"} invert_xaxis : bool, optional if True, the x-axis of the plot will be inverted Returns ------- ax, or fig, ax (if no axes were provided) ''' # handle dict defaults savefig_settings = {} if savefig_settings is None else savefig_settings.copy() if ax is None: fig, ax = plt.subplots(figsize=figsize) if label_size is None: label_size = mpl.rcParams['axes.labelsize'] if thickness is None: thickness = 5*label_size ax.get_yaxis().set_visible(False) # remove parasitic lines and labels ax.get_xaxis().set_visible(show_xaxis) # remove parasitic lines and labels ax.spines[:].set_visible(False) dx = np.diff(x).mean() xmin = x.min() xmax = x.max() + dx # inclusive # Reference period for the center of the color scale reference = y[ref_period[0]:ref_period[1]].mean() # colormap: the 8 more saturated colors from the 9 blues / 9 reds cmap = ListedColormap([ '#08306b', '#08519c', '#2171b5', '#4292c6', '#6baed6', '#9ecae1', '#c6dbef', '#deebf7', '#fee0d2', '#fcbba1', '#fc9272', '#fb6a4a', '#ef3b2c', '#cb181d', '#a50f15', '#67000d', ]) col = PatchCollection([ Rectangle((yl, 0), 1, 1) for yl in range(int(xmin), int(xmax)) ]) # set data, colormap and color limits col.set_array(y) col.set_cmap(cmap) col.set_clim(reference - LIM, reference + LIM) ax.add_collection(col) # adjust axes ax.set_ylim(0, thickness) ax.set_xlim(xmin, xmax); # add label to the right #offset = y_offsets[column] / 72 #trans = mtransforms.ScaledTranslation(0, offset, fig.dpi_scale_trans) #trans = ax.transData #+ trans ypos = 0.4*thickness ax.text(xmax+dx+x_offset, 0.6*thickness, top_label, color=label_color, fontsize=label_size, fontweight = 'bold') ax.text(xmax+dx+x_offset, 0.2*thickness, bottom_label, color=label_color, fontsize=label_size) if xlabel is not None: ax.set_xlabel(xlabel) if ylabel is not None: ax.set_ylabel(ylabel) if title is not None: ax.set_title(title) if xlim is not None: ax.set_xlim(xlim) if invert_xaxis: ax.invert_xaxis() if 'fig' in locals(): fig.tight_layout() if 'path' in savefig_settings: savefig(fig, settings=savefig_settings) return fig, ax else: return ax
[docs]def closefig(fig=None): '''Close the figure Parameters ---------- fig : matplotlib.pyplot.figure The matplotlib figure object ''' if fig is not None: plt.close(fig) else: plt.close()
[docs]def savefig(fig, path=None, dpi=300, settings={}, verbose=True): ''' Save a figure to a path Parameters ---------- fig : matplotlib.pyplot.figure the figure to save path : str the path to save the figure, can be ignored and specify in "settings" instead dpi : int resolution in dot (pixels) per inch. Default: 300. settings : dict the dictionary of arguments for plt.savefig(); some notes below: - "path" must be specified in settings if not assigned with the keyword argument; it can be any existing or non-existing path, with or without a suffix; if the suffix is not given in "path", it will follow "format" - "format" can be one of {"pdf", "eps", "png", "ps"} verbose : bool, {True,False} If True, print the path of the saved file. ''' if path is None and 'path' not in settings: raise ValueError('"path" must be specified, either with the keyword argument or be specified in `settings`!') savefig_args = {'bbox_inches': 'tight', 'path': path, 'dpi': dpi} savefig_args.update(settings) path = pathlib.Path(savefig_args['path']) savefig_args.pop('path') dirpath = path.parent if not dirpath.exists(): dirpath.mkdir(parents=True, exist_ok=True) if verbose: print(f'Directory created at: "{dirpath}"') path_str = str(path) if path.suffix not in ['.eps', '.pdf', '.png', '.ps']: path = pathlib.Path(f'{path_str}.pdf') fig.savefig(path_str, **savefig_args) plt.close(fig) if verbose: print(f'Figure saved at: "{str(path)}"')
[docs]def set_style(style='journal', font_scale=1.0, dpi=300): ''' Modify the visualization style This function is inspired by [Seaborn](https://github.com/mwaskom/seaborn). Parameters ---------- style : {journal,web,matplotlib,_spines, _nospines,_grid,_nogrid} set the styles for the figure: - journal (default): fonts appropriate for paper - web: web-like font (e.g. ggplot) - matplotlib: the original matplotlib style In addition, the following options are available: - _spines/_nospines: allow to show/hide spines - _grid/_nogrid: allow to show gridlines (default: _grid) font_scale : float Default is 1. Corresponding to 12 Font Size. ''' font_dict = { 'font.size': 10, 'axes.labelsize': 11, 'axes.titlesize': 12, 'xtick.labelsize': 10, 'ytick.labelsize': 10, 'legend.fontsize': 9, } style_dict = {} if 'journal' in style: style_dict.update({ 'axes.axisbelow': True, 'axes.facecolor': 'white', 'axes.edgecolor': 'black', 'axes.grid': True, 'grid.color': 'lightgrey', 'grid.linestyle': '--', 'xtick.direction': 'out', 'ytick.direction': 'out', 'font.sans-serif': ['Arial', 'DejaVu Sans', 'Liberation Sans', 'Bitstream Vera Sans', 'sans-serif'], 'axes.spines.left': True, 'axes.spines.bottom': True, 'axes.spines.right': False, 'axes.spines.top': False, 'legend.frameon': False, 'axes.linewidth': 1, 'grid.linewidth': 1, 'lines.linewidth': 2, 'lines.markersize': 6, 'patch.linewidth': 1, 'xtick.major.width': 1.25, 'ytick.major.width': 1.25, 'xtick.minor.width': 0, 'ytick.minor.width': 0, }) elif 'web' in style: style_dict.update({ 'figure.facecolor': 'white', 'axes.axisbelow': True, 'axes.facecolor': 'whitesmoke', 'axes.edgecolor': 'lightgrey', 'axes.grid': True, 'grid.color': 'white', 'grid.linestyle': '-', 'xtick.direction': 'out', 'ytick.direction': 'out', 'text.color': 'grey', 'axes.labelcolor': 'grey', 'xtick.color': 'grey', 'ytick.color': 'grey', 'font.sans-serif': ['Arial', 'DejaVu Sans', 'Liberation Sans', 'Bitstream Vera Sans', 'sans-serif'], 'axes.spines.left': False, 'axes.spines.bottom': False, 'axes.spines.right': False, 'axes.spines.top': False, 'legend.frameon': False, 'axes.linewidth': 1, 'grid.linewidth': 1, 'lines.linewidth': 2, 'lines.markersize': 6, 'patch.linewidth': 1, 'xtick.major.width': 1.25, 'ytick.major.width': 1.25, 'xtick.minor.width': 0, 'ytick.minor.width': 0, }) else: raise ValueError(f'Style [{style}] not availabel!') if '_spines' in style: style_dict.update({ 'axes.spines.left': True, 'axes.spines.bottom': True, 'axes.spines.right': True, 'axes.spines.top': True, }) elif '_nospines' in style: style_dict.update({ 'axes.spines.left': False, 'axes.spines.bottom': False, 'axes.spines.right': False, 'axes.spines.top': False, }) if '_grid' in style: style_dict.update({ 'axes.grid': True, }) elif '_nogrid' in style: style_dict.update({ 'axes.grid': False, }) figure_dict = { 'savefig.dpi': dpi, } # modify font size based on font scale font_dict.update({k: v * font_scale for k, v in font_dict.items()}) for d in [style_dict, font_dict, figure_dict]: mpl.rcParams.update(d)