Skip to content

[ENH]: Include something equivalent to Matlab's tiledlayout and nexttile #24953

Closed as not planned
@benoitespinola

Description

@benoitespinola

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions