Description
Problem
Plotting data in subplots can sometimes be a pain. For instance, if you want to create one plot for each day of the year.
import numpy as np
import matplotlib.pyplot as plt
def days_in_year(year):
return (np.datetime64(f'{(year+1)}-01-01')-np.datetime64(f'{(year)}-01-01')).astype(int)
print(days_in_year(2013)) # 365 days
print(days_in_year(2012)) # 366 days
print(days_in_year(2014)) # 365 days
# 21*18 >= 366
n_cols = 18
n_rows = 21
fig, axs = plt.subplots(n_cols, n_rows, sharex = True, sharey = True)
for d in range(days_in_year(2013)):
x = np.random.rand(20)
y = np.random.rand(20)
# calculate the plot location
col = d % n_cols # prone to error
row = int(d/n_cols) # prone to error
# plot
axs[col,row].scatter(x,y) # again, prone to error row,col vs col,row
plt.show()
Note that the number of days can change according to the year, it is not easy to find a good grid that fits all the data. You also need to hide the tiles that remain unused. Additionally, if you have too many rows, this method will not display the tick labels for the bottom-most plots (because they are not the last row). Finally, there are many points where errors can be made, potentially leading to unexpected results.
Proposed solution
It would be far more convenient to have an easy way to sequentially plot.
One could start a subplot object that contains zero plots.
Then you add plots in a sequential manner. You get a row vector with plots and a linear index (for instance, 0 to len - 1).
Once you are done making the plots, you can call a function that will produce the layout. You may pass the number of columns and rows for this layout and how to fill (by column or row). You then can show or save the figure.
Example code:
import numpy as np
import matplotlib.pyplot as plt
def days_in_year(year):
return (np.datetime64(f'{(year+1)}-01-01')-np.datetime64(f'{(year)}-01-01')).astype(int)
print(days_in_year(2013)) # 365 days
print(days_in_year(2012)) # 366 days
print(days_in_year(2014)) # 365 days
tile = plt.tiled(sharex = True, sharey = True)
for d in range(days_in_year(2013)):
x = np.random.rand(20)
y = np.random.rand(20)
tile.nexttile()
# plots in the current tile
tile.scatter(x,y)
tile.tile[2].plot([0, 0], [0, 1]) # add a plot to an existing tile
tile.layout(height = 21, width= 18, fill_by = 'row')
plt.show()
Providing a too little width x height after creating the tiles throws an error.
The layout can be built in advance too, in this case there is only width x height tiles available.
If there are new tiles, the layout adds a row or column according to the fill_by argument.
By default fill_by fills by row:
0 1 2 3
4 5 6 7
...
Alternatively, one can fill by column:
0 4
1 5
2 6
3 7
...
One could give either height or width and the counterpart could be calculated automatically
For instance:
tile.layout(height = 21, fill_by = 'row')
Calculates the smallest number for width:
width = math.ceil(len(tiles)/height)
There could be a default height and width that could be calculated using sqrt of the length of tiles
width = math.ceil(sqrt(len(tiles)))
height = math.floor(sqrt(len(tiles)))
Only the last width tiles would have the x-axis displayed, the other tiles have it hidden by default
The missing tiles in the width x height layout are by default hidden.