Utilities#
In addition to the highly customizable plotters with complex grid layout, the nested_grid_plotter library provides a set of utilities, gathered in a subpackage named utils. These utilities do not require to use a plotter and can usually be used on classic matplotlib Axes.
Note that the purpose of this library is simply to simplify some operations and to avoid code duplication. All the figures can be produced without this library, with matplotlib alone.
Tutorial configuration#
Let’s start by importing the modules that we need
[1]:
from datetime import datetime
from typing import List
import matplotlib as mpl
import matplotlib.pyplot as plt
import nested_grid_plotter as ngp
import numpy as np
from dateutil.relativedelta import relativedelta
from matplotlib.axes import Axes
To display the figure inline in this jupyter notebook, let’s run:
[2]:
%matplotlib inline
Check the versions being used:
[3]:
print(f"matplotlib version = {mpl.__version__}")
print(f"nested_grid_plotter version = {ngp.__version__}")
print(f"numpy version = {np.__version__}")
matplotlib version = 3.10.1
nested_grid_plotter version = 2.0.0
numpy version = 2.3.3
Let’s also apply some basic parameters for our figures so it looks nice.
[4]:
new_rc_params = {
"font.size": 16,
"figure.figsize": (8, 8),
"figure.facecolor": "w",
"savefig.facecolor": "w",
"savefig.edgecolor": "k",
"savefig.dpi": 300,
}
plt.rcParams.update(new_rc_params)
For the following tutorials, let’s create a general function to generate such complex plot.
[5]:
def gen_complex_example_fig():
return ngp.NestedGridPlotter(
ngp.Figure(
constrained_layout=True, # Always use this to prevent overlappings
figsize=(15, 6),
),
builder=ngp.SubfigsBuilder(
nrows=1,
ncols=2,
sub_builders={
"the_left_sub_figure": ngp.SubplotsMosaicBuilder(
mosaic=[["lt1", "lt1"], ["lb1", "rb1"]],
gridspec_kw=dict(height_ratios=[2, 1], width_ratios=[2, 1]),
sharey=False,
),
"the_right_sub_figure": ngp.SubfigsBuilder(
nrows=1,
ncols=2,
width_ratios=[2, 1],
sub_builders={
"the_right_left_sub_figure": ngp.SubplotsMosaicBuilder(
mosaic=[["l2"]],
),
"the_right_right_sub_figure": ngp.SubplotsMosaicBuilder(
mosaic=[["rt2"], ["rb2"]],
gridspec_kw=dict(height_ratios=[2, 1]),
sharey=False,
),
},
),
},
),
)
Replace non accepted parameter in paths#
Make filename compatible with path (to save the figure) by replacaing anything that isn’t alphanumeric, -, _, a space, a period, and brackets or parenthesis. Note that it combines multiple escaped characters into a single replacement.
[6]:
badfilename = r"#6262%%this?is#\\my=plot*///name(km/h) "
print(ngp.replace_bad_path_characters(badfilename, repchar=" "))
print(ngp.replace_bad_path_characters(badfilename, repchar="_"))
print(ngp.replace_bad_path_characters(badfilename, repchar="-"))
6262 this is my plot name(km h)
6262_this_is_my_plot_name(km_h)
6262-this-is-my-plot-name(km-h)
Add grids and configure thickparams#
This adds a grid to a specific axis and offers the possibility to configure thick params. It takes the same input as Axes.thick_params with some default parameters.
[7]:
plotter = gen_complex_example_fig()
ngp.add_grid_and_tick_prams_to_axis(plotter.ax_dict["lt1"])
ngp.add_grid_and_tick_prams_to_axis(
plotter.ax_dict["lb1"], colors="red", grid_color="red"
)
plotter.fig
[7]:
Also, a same grid can be applied to all subplots of the figure or of one of the subfigures.
[8]:
# Create a plot
plotter = gen_complex_example_fig()
plotter.identify_axes() # Helper to add the name of the axis on the plot
# Add the grids
plotter.add_grid_and_tick_prams_to_all_axes(
subfigure_name="the_left_sub_figure", colors="red", grid_color="red"
)
plotter.fig
[8]:
[9]:
# Add the grids to all subfigures' plots
plotter.add_grid_and_tick_prams_to_all_axes(colors="blue", grid_color="blue", length=5)
plotter.fig
[9]:
Note that because the tick are red and thick, it has a quite weird effect when overlapping the frame of the plot. It can be easily removed by using a method remove_axis_ticks_overlapping_axis_frame. The labels are preserved, the ticks are just made invisible.
[10]:
ngp.make_ticks_overlapping_axis_frame_invisible(plotter.ax_dict["lt1"])
plotter.fig # display again
[10]:
However, it needs to be performed only when the scale is fixed (i.e., no latter plot or manual adjustment modifies it) because if the scale of the axis is changed, the ticks are not updated and it might look weird. I haven’t find a way to overcome this.
[11]:
plotter.ax_dict["lt1"].set_xlim(2.0, 4.0)
plotter.fig
[11]:
Hide ticks, ticklabels and spines#
It is also possible to hide the ticks, ticklabels and spines with wrapping methods.
hide_axis_ticklabelsto hide the labels.Nothing special to hide the tick, the efault interface being already straightforward.
hide_axis_spineto hide the spines.
[12]:
# Create a plot
plotter = gen_complex_example_fig()
plotter.identify_axes() # Helper to add the name of the axis on the plot
# Remove the ticklabels in plot "l2" = We can also use 'both' to combine these two lines
ngp.hide_axis_ticklabels(plotter.ax_dict["l2"], "x")
ngp.hide_axis_ticklabels(plotter.ax_dict["l2"], "y")
# Remove the right and top ticks in plot 'rt2'
plotter.ax_dict["rt2"].tick_params(right=False, top=False, bottom=False, left=False)
# Remove the spines in plot 'bt2'
ngp.hide_axis_spine(plotter.ax_dict["rb2"], loc="top")
ngp.hide_axis_spine(plotter.ax_dict["rb2"], loc="right")
# plotter.hide_axis_spine("rb2", loc="left")
# plotter.hide_axis_spine("rb2", loc="bottom")
# plotter.hide_axis_spine("rb2", loc="all") -> combines the four previous lines
plotter.fig
[12]:
Align gridlines for two y-axis#
Aligning gridlines for multiple y-axis is sometimes very useful. The NestedGridPlotter offers two herlper methods:
align_y_axeswhich simply aligns ticks adding the minimum number of ticks as possible.align_y_axis_on_valueswhich taken from jason answer on this stackoverflow thread and works a bit differently. It allows to align one specific values for each axes.
Both support multiple axes.
Let’s see two examples:
[13]:
# craft some data to plot
x = np.arange(20)
y1 = np.sin(x)
y2 = x / 1000 + np.exp(x)
y3 = x + x**2 / 3.14
def plotLines(
x: np.ndarray, y1: np.ndarray, y2: np.ndarray, y3: np.ndarray, ax: Axes
) -> List[Axes]:
ax.plot(x, y1, "b-")
ax.tick_params("y", colors="b")
tax1 = ax.twinx()
tax1.plot(x, y2, "r-")
tax1.tick_params("y", colors="r")
tax2 = ax.twinx()
tax2.spines["right"].set_position(("axes", 1.2))
# make_patch_spines_invisible(tax2)
tax2.spines["right"].set_visible(True)
tax2.plot(x, y3, "g-")
tax2.tick_params("y", colors="g")
ax.grid(True, axis="both")
return [ax, tax1, tax2]
plt.rcParams.update({"font.size": 10})
plotter = ngp.NestedGridPlotter(
ngp.Figure(
constrained_layout=True,
figsize=(16, 4),
),
builder=ngp.SubfigsBuilder(ncols=3),
)
# Left plot: No alignment.
ax1 = plotter.ax_dict["subfig_1_ax1-1"]
axes1 = plotLines(x, y1, y2, y3, ax1)
ax1.set_title("No alignment")
# Mid plot: Aligned at (approximately) the lower bound of each y axis.
ax2 = plotter.ax_dict["subfig_2_ax1-1"]
axes2 = plotLines(x, y1, y2, y3, ax2)
ngp.align_y_axes(axes2)
ax2.set_title("Default alignment")
# Right plot: Aligned at specified values: 0 for blue, 2.2*1e8 for red, and 44 for
# green. Those are chosen arbitrarily for the example.
ax3 = plotter.ax_dict["subfig_3_ax1-1"]
axes3 = plotLines(x, y1, y2, y3, ax3)
ngp.align_y_axes_on_values(axes3, [0, 2.2 * 1e8, 44])
ax3.set_title("Specified alignment")
plotter.fig
[13]:
Note that if no args are given, then the default alignment might give undisired results.
[14]:
plotter = ngp.NestedGridPlotter(
ngp.Figure(
constrained_layout=True,
figsize=(15, 5),
),
builder=ngp.SubfigsBuilder(ncols=3),
)
# Make some data
np.random.seed(1)
data = (np.random.rand(20) + 5) ** 2
data_cum = data.cumsum()
# On the left figure, if we plot the data and its cumulative, the ticks on the y axes
# are not aligned
ax1 = plotter.ax_dict["subfig_1_ax1-1"]
ax1.plot(data)
twin_ax_1 = ax1.twinx()
twin_ax_1.plot(data_cum)
ax1.grid()
twin_ax_1.grid()
ax1.set_title("No alignment")
# This gives the optimal alignment
ax2 = plotter.ax_dict["subfig_2_ax1-1"]
ax2.plot(data)
ax2_twin = ax2.twinx()
ax2_twin.plot(data_cum)
ax2.grid()
ax2_twin.grid()
ngp.align_y_axes([ax2, ax2_twin])
ax2.set_title("Default alignment")
# This is not optimal
ax3 = plotter.ax_dict["subfig_3_ax1-1"]
ax3.plot(data)
ax3_twin = ax3.twinx()
ax3_twin.plot(data_cum)
ax3.grid()
ax3_twin.grid()
ngp.align_y_axes_on_values([ax3, ax3_twin])
ax3.set_title("Specific alignment with no values")
plotter.fig
[14]:
Note the same can be achieved for the x axis with
ngp.align_x_axes_on_values.
Make x and y axes symmetric and centered to zero#
With the y axis (most frequent case)
[15]:
plotter = ngp.NestedGridPlotter(
ngp.Figure(
constrained_layout=True,
figsize=(16, 4),
),
builder=ngp.SubfigsBuilder(ncols=4),
)
# Make some data
np.random.seed(1)
# data1 = np.random.normal(0,1,20)
data1 = np.random.rand(20) - 3
data2 = np.random.rand(20)
xy = np.zeros(20)
ax1 = plotter.ax_dict["subfig_1_ax1-1"]
ax1_twin = ax1.twinx()
ax1.plot(data1, label="data1")
ax1_twin.plot(data2, c="r", label="data2")
ax1.set_title("No alignment")
ax2 = plotter.ax_dict["subfig_2_ax1-1"]
ax2_twin = ax2.twinx()
ax2.plot(data1, label="data1")
ax2.plot(xy, linestyle="--")
ax2_twin.plot(data2, c="r", label="data2")
ngp.align_y_axes_on_values([ax2, ax2_twin], [0.0, 0.0]) # Works for more than 2 axes
ax2.set_title("Specific alignment at y=0.0,\n no symmetry")
ax3 = plotter.ax_dict["subfig_3_ax1-1"]
ax3_twin = ax3.twinx()
ax3.plot(data1, label="data1")
ax3.plot(xy, linestyle="--")
ax3_twin.plot(data2, c="r", label="data2")
ngp.make_y_axes_symmetric_zero_centered([ax3, ax3_twin]) # Works for more than 2 axes
ax3.set_title("Specific alignment at y=0.0,\n with symmetry")
ax4 = plotter.ax_dict["subfig_4_ax1-1"]
ax4_twin = ax4.twinx()
ax4.plot(data1, label="data1")
ax4.plot(xy, linestyle="--")
ax4_twin.plot(data2, c="r", label="data2")
# Here we impose a range of at least [-0.5, 0.5] for the blue curve,
# and of [-6, 6] for the red curve
ngp.make_y_axes_symmetric_zero_centered([ax4, ax4_twin], min_ylims=[0.5, 6.0])
ax4.set_title("Specific alignment at y=0.0,\n with symmetry and min limit")
plotter.add_fig_legend(fontsize=10, ncol=2)
plotter.fig
[15]:
With the x axis (more exotic)
[16]:
plotter = ngp.NestedGridPlotter(
ngp.Figure(
constrained_layout=True,
figsize=(16, 4),
),
builder=ngp.SubfigsBuilder(ncols=4),
)
# Make some data
np.random.seed(1)
# data1 = np.random.normal(0,1,20)
x_data1 = np.random.rand(20) - 3
x_data2 = np.random.rand(20)
y_data = np.arange(x_data1.size)
xy = np.zeros(20)
ax1 = plotter.ax_dict["subfig_1_ax1-1"]
ax1_twin = ax1.twiny()
ax1.plot(x_data1, y_data, label="data1")
ax1_twin.plot(x_data2, y_data, c="r", label="data2")
ax1.set_title("No alignment")
ax2 = plotter.ax_dict["subfig_2_ax1-1"]
ax2_twin = ax2.twiny()
ax2.plot(x_data1, y_data, label="data1")
ax2.plot(xy, y_data, linestyle="--")
ax2_twin.plot(x_data2, y_data, c="r", label="data2")
ngp.align_x_axes_on_values([ax2, ax2_twin], [0.0, 0.0]) # Works for more than 2 axes
ax2.set_title("Specific alignment at x=0.0, no symmetry")
ax3 = plotter.ax_dict["subfig_3_ax1-1"]
ax3_twin = ax3.twiny()
ax3.plot(x_data1, y_data, label="data1")
ax3.plot(xy, y_data, linestyle="--")
ax3_twin.plot(x_data2, y_data, c="r", label="data2")
ngp.make_x_axes_symmetric_zero_centered([ax3, ax3_twin]) # Works for more than 2 axes
ax3.set_title("Specific alignment at x=0.0, with symmetry")
ax4 = plotter.ax_dict["subfig_4_ax1-1"]
ax4_twin = ax4.twiny()
ax4.plot(x_data1, y_data, label="data1")
ax4.plot(xy, y_data, linestyle="--")
ax4_twin.plot(x_data2, y_data, c="r", label="data2")
# Here we impose a range of at least [-0.5, 0.5] for the blue curve,
# and of [-6, 6] for the red curve
ngp.make_x_axes_symmetric_zero_centered([ax4, ax4_twin], min_xlims=[0.5, 6.0])
ax4.set_title("Specific alignment at y=0.0,\n with symmetry and min limit")
plotter.add_fig_legend(fontsize=10, ncol=2)
plotter.fig
[16]:
Add a date axis#
It is sometimes painful to combine dates and number of days. I frequently used it o I wanted to included it in the library. Not that the time_unit and the format are totally independent, the first beinginvolve in computing the date, the secondonly for display. Even if this is not necessary, it is possible to add multiple date axes. Note that add_xaxis_twin_as_date is deprecated since v1.2.
[17]:
plotter = ngp.NestedGridPlotter(
ngp.Figure(
constrained_layout=True,
figsize=(15, 5),
),
builder=ngp.SubplotsMosaicBuilder(mosaic=[["ax1-1", "ax1-2", "ax1-3"]]),
)
# Plot the data
ax1 = plotter.ax_dict["ax1-1"]
ax1.plot(np.cumsum(0.004 + 0.04 * np.random.randn(100, 5)), label="label1")
ax1.set_title("A timeseries with daily data")
# You can either transform the existing axis
ngp.ticklabels_to_datetime(
ax1,
initial_datetime=datetime(2022, 1, 6),
is_y_axis=False,
step=relativedelta(days=1),
format="%d-%m-%Y",
rotation_degrees=15,
)
# Or add a second one
ngp.add_twin_axis_as_datetime(
ax1,
initial_datetime=datetime(2022, 1, 6),
step=relativedelta(days=1),
format="%d-%m-%Y",
rotation_degrees=-15, # rotation the other way around
spine_outward_position=50, # relative to ax11
)
# And even a third one with a rotation
ax113 = ngp.add_twin_axis_as_datetime(
ax1,
initial_datetime=datetime(2022, 1, 6),
step=relativedelta(days=1),
format="%d-%m",
spine_outward_position=90, # relative to ax11
rotation_degrees=0,
)
# Plot the data
ax2 = plotter.ax_dict["ax1-2"]
ax2.plot(np.cumsum(0.004 + 0.04 * np.random.randn(10, 5)), label="label2", c="r")
ax2.set_title("A timeseries with monthly data")
ax2.set_xlabel("Number of months")
# And on the third graphic, same with years and above the graphic
ax22 = ngp.add_twin_axis_as_datetime(
ax2,
initial_datetime=datetime(2022, 1, 6),
step=relativedelta(months=1),
format="%m-%Y",
spine_outward_position=38,
)
# Plot the data
ax3 = plotter.ax_dict["ax1-3"]
ax3.plot(np.cumsum(0.004 + 0.04 * np.random.randn(5, 5)), label="label3")
ax3.set_title("A timeseries with yealy data")
ax3.set_xlabel("Number of years")
# Add a date x axis
ax3 = ngp.add_twin_axis_as_datetime(
ax3,
initial_datetime=datetime(2022, 1, 6),
step=relativedelta(years=1),
format="%Y",
spine_outward_position=0,
position="top",
is_hide_opposed_tick_labels=False,
)
# no overlapping with the legend
plotter.add_fig_legend(ncol=3)
plotter.fig
[17]:
Add letters to frames#
[18]:
plotter = ngp.NestedGridPlotter(
ngp.Figure(
constrained_layout=True,
figsize=(10, 10),
),
builder=ngp.SubfigsBuilder(ncols=3, nrows=3),
)
ngp.add_letter_to_frames(plotter.axes)
plotter.fig
[18]:
If the number of frames (axes) is above 26 (the number of letter in the latin alphabet) than a suffix number is added.
[19]:
plotter = ngp.NestedGridPlotter(
ngp.Figure(
constrained_layout=True,
figsize=(10, 10),
),
builder=ngp.SubfigsBuilder(ncols=7, nrows=7),
)
ngp.add_letter_to_frames(plotter.axes)
plotter.fig
[19]: