Layout: using layout to arrange a set of plots

The simple addition of a cow.patch(...) object and a cow.layout(...) object allows for a range of different arrangements of plots. The below document enumerates a range of these approaches.

Connection to our R cousins

These approaches reflect similar arrangements that are seen in R packages like gridExtra’s arrange.grid, cowplot’s plot_grid and patchwork’s plot_layout.

Outline of examples

The below subsections lay out arrange of plots using just cow.patch and cow.layout in two distinct ways. The first captures a more straight forward, single step approach to defining the layout, whereas the later presents a way to define the arrangement of plots in a nested way which may align with work flows that are more interative in nature. This first way aligns with gridExtra philosophical approach more, whereas the second aligns with cowplot development philosophy.

A note on image sizes

When we call the .show() function, the documentation representation of the slides scales the width of the svg object to be 100% width of the document in it exceeds the size. This scaling won’t happen if you run the code in a standard jupyter notebook2.

Initial definition of plots

import numpy as np
import cowpatch as cow

import plotnine as p9
import plotnine.data as p9_data
mtcars = p9_data.mpg

g0 = p9.ggplot(p9_data.mpg) +\
    p9.geom_bar(p9.aes(x="hwy")) +\
    p9.labs(title = 'Plot 0')

g1 = p9.ggplot(p9_data.mpg) +\
    p9.geom_point(p9.aes(x="hwy", y = "displ")) +\
    p9.labs(title = 'Plot 1')

g2 = p9.ggplot(p9_data.mpg) +\
    p9.geom_point(p9.aes(x="hwy", y = "displ", color="class")) +\
    p9.labs(title = 'Plot 2')

g3 = p9.ggplot(p9_data.mpg[p9_data.mpg["class"].isin(["compact",
                                                     "suv",
                                                     "pickup"])]) +\
    p9.geom_histogram(p9.aes(x="hwy"),bins=10) +\
    p9.facet_wrap("class")

Layout of multiple plots

The examples below utilize a single cow.patch(...) and cow.layout(...) object to create complex arangements of figures.

Design matrix approach

One way to define the layout of different plots on the same level is thorugh a design matrix. Without specification of the relative heights and widths of each row or column they are assumed to be equivalent1.

Note that the values in the design matrix must be from 0 to the number of objects in the arrangement.

patch_obj = cow.patch(g0,g1,g2)
layout_obj = cow.layout(design = np.array([[0,0,0,1,1,1],
                                           [0,0,0,2,2,2],
                                           [0,0,0,2,2,2]]))

The above patch_obj could also have been defined using a list of plots, which could be useful if the number of plots you plan to use might depend on certain decisions or you wish to store the plots you’d like to arrange in a list.

patch_obj = cow.patch(grobs = [g0,g1,g2])

We can then visual the arrangement or patchwork of these plots with the above layout in the following manner:

vis = patch_obj + layout_obj
vis.show(width = 12, height = 7)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 12 x 7 inch image.
../_images/Layout_8_1.svg

Direct expression of relative matrix row heights and column widths

A similar arrangement as seen above can be described with the rel_heights structure in combination of a smaller design matrix.

layout_obj_rh = cow.layout(design = np.array([[0,1],
                                              [0,2]]),
                                 rel_heights = [1,2])
vis_rh = patch_obj + layout_obj_rh
vis_rh.show(width = 12, height = 7)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 12 x 7 inch image.
../_images/Layout_10_1.svg

Design string approach

Very similar to the design matrix approach, the user is also welcome to describe the design with a string.

Note that in the string format, one can use “\n” characters instead of a block string, and the values need to be capital letters (starting with “A”).

The following example captures the same structure of the previous example using a design string:

layout_obj_str = cow.layout(design = """
                                    AAABBB
                                    AAACCC
                                    AAACCC
                                    """)
vis_str = patch_obj + layout_obj_str
vis_str.show(width = 12, height = 7)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 12 x 7 inch image.
../_images/Layout_12_1.svg

Direct expression of relative matrix row heights and column widths

In a similar manner as with the design matrix, a design string’s row heights and column heights and be altered with rel_widths and rel_heights. This is demonstrated again in the example below:

layout_obj_str_rh = cow.layout(design = """
                                        AB
                                        AC
                                        """,
                                 rel_heights = [1,2])
vis_str_rh = patch_obj + layout_obj_str_rh
vis_str_rh.show(width = 12, height = 7)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 12 x 7 inch image.
../_images/Layout_14_1.svg

Empty space in design layouts

In this design layout approach we can also define empty space. To do so, with a design matrix we include np.nan values to denote locations without associated plots and with a design string we denote these empty spaces with either “#” or “.”s.

Below we present examples of all these approaches.

Empty space with a design matrix

layout_obj_str_rh_nan = cow.layout(design = np.array([[0,np.nan,1,1],
                                                      [0,2,2,np.nan]]),
                                 rel_heights = [1,2],
                                 rel_widths = [3,1,1,1])
vis_str_rh_nan = patch_obj + layout_obj_str_rh_nan


print(np.array([[0,np.nan,1,1],
                [0,2,2,np.nan]]))
vis_str_rh_nan.show(width = 14, height = 9)
[[ 0. nan  1.  1.]
 [ 0.  2.  2. nan]]
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 14 x 9 inch image.
../_images/Layout_17_2.svg

Empty space with a design string

layout_obj_str_rh_hash = cow.layout(design = """
                                        A#BB
                                        ACC#
                                        """,
                                 rel_heights = [1,2],
                                 rel_widths = [3,1,1,1])
vis_str_rh_hash = patch_obj + layout_obj_str_rh_hash
vis_str_rh_hash.show(width = 14, height = 9)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 14 x 9 inch image.
../_images/Layout_19_1.svg
layout_obj_str_rh_dot = cow.layout(design = """
                                        A.BB
                                        ACC.
                                        """,
                                 rel_heights = [1,2],
                                 rel_widths = [3,1,1,1])
vis_str_rh_dot = patch_obj + layout_obj_str_rh_dot
vis_str_rh_dot.show(width = 14, height = 9)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 14 x 9 inch image.
../_images/Layout_20_1.svg

Layouts defined by number of columns and/or rows (ncol and nrow)

Sometimes design matrices are overkill, and in many other plot and arrangement tools, we often see arrangement descriptions define the number of rows and/or columns (nrow, ncol). Like these approaches, cow.layout’s nrow and ncol define the number of rows and columns of the layout. By default the plots in the cow.patch are then filled in by row (alterable with the byrow parameter). Below we present a sequence of examples that explore the potential layouts that can be defined with these parameters.

Using both ncol and nrow

vis_ncol_nrow = cow.patch(g0,g1,g2,g3) + cow.layout(ncol = 2, nrow = 2)
vis_ncol_nrow.show(width = 12, height = 7)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 12 x 7 inch image.
../_images/Layout_23_1.svg
vis_ncol_nrow_over = cow.patch(g0,g1,g2,g3) + cow.layout(ncol = 3, nrow = 2)
vis_ncol_nrow_over.show(width = 14, height = 7)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 14 x 7 inch image.
../_images/Layout_24_1.svg
vis_ncol_nrow_X_over = cow.patch(g0,g1,g2,g3) + cow.layout(ncol = 3, nrow = 3)
vis_ncol_nrow_X_over.show(width = 14, height = 11)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 14 x 11 inch image.
../_images/Layout_25_1.svg

Using only ncol

vis_ncol = cow.patch(g0,g1,g2) + cow.layout(ncol = 2)
vis_ncol.show(width = 12, height = 7)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 12 x 7 inch image.
../_images/Layout_27_1.svg

Using only ncol with byrow = False

vis_ncol_bycol = cow.patch(g0,g1,g2) + cow.layout(ncol = 2, byrow = False, rel_widths = [2,3])
vis_ncol_bycol.show(width = 12, height = 7)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 12 x 7 inch image.
../_images/Layout_29_1.svg

Using only nrow

vis_ncol = cow.patch(g0,g1,g2,g3) + cow.layout(nrow = 3)
vis_ncol.show(width = 12, height = 11)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 12 x 11 inch image.
../_images/Layout_31_1.svg

Using only nrow with byrow=False

vis_ncol = cow.patch(g0,g1,g2,g3) + cow.layout(nrow = 3, byrow=False, rel_heights = [1,1,2])
vis_ncol.show(width = 12, height = 11)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 12 x 11 inch image.
../_images/Layout_33_1.svg

Nesting arrangement of plots

As mentioned above, it maybe more intuitive in certain situations and also work better in certain workflows to define the arrangement of plots by nesting sub-arrangements of plots. In the following we show how one can include an arrangement as a single object in another larger arrangement and visualized as follows. We only demonstrate a single layout of nesting, but naturally more one can leverage nesting in with more levels.

Note that we decide to use the cow.layout’s nrow and ncol parameters for simplicity, but design approaches would naturally preform well here too.

vis_left = cow.patch(g1,g2) + cow.layout(ncol =1, rel_heights = [1,2])

vis_total = cow.patch(g0, vis_left) + cow.layout(nrow = 1)
vis_total.show(width = 12, height = 7)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 12 x 7 inch image.
../_images/Layout_35_1.svg

with grobs parameter

It seems important enough to highlight that this approach might be cleanly approach with a list approach in cow.patch. Remember this can also be done with the grobs parameter as introduced at the beginning of this document. For example:

vis_total_g = cow.patch(grobs = [g0, vis_left]) + cow.layout(nrow = 1)
vis_total_g.show(width = 12, height = 7)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 12 x 7 inch image.
../_images/Layout_38_1.svg

Defaults and error avoidance corrections

In the above examples you’ll see that we provided user defined width and height values for the .show call. And you’ll find that a similar approach is done with .save class method.

Underneath the hood of cowpatch we’ve provided a few useful additions in this area

  1. If you’re in a hurry and just want to see the plot you can call .show(). We’ll select a decent height and width. This approach can also help you as a starting point (so you can fine tune the parameters after you see the underlying plot without your guidance on the sizing).

  2. There are underlying complications with plotnine’s ability to actually create a plot any size you’d like. If your requested width and height parameters run into this error (for example requesting a overall graphic size too small), we’ll propose a different set of parameter values (with the same aspect ratio, and width and height values as close to your requested ones as possible).

Example of selection of height and width if not provided

vis_left = cow.patch(g1,g2) + cow.layout(ncol =1, rel_heights = [2,3])
vis_total = cow.patch(g0, vis_left) + cow.layout(nrow = 1)

vis_total.show()
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 12 x 9.3 inch image.
../_images/Layout_41_1.svg

Example of size correction if height and width don’t work with plotnine’s visualization ability

vis_left = cow.patch(g1,g2) + cow.layout(ncol =1, rel_heights = [2,3])
vis_total = cow.patch(g0, vis_left) + cow.layout(nrow = 1)

vis_total.show(width = 1.2, height = .9)
/home/runner/work/cowpatch/cowpatch/src/cowpatch/svg_utils.py:438: CowpatchWarning: Showing 5.5 x 4.1 inch image.
../_images/Layout_43_1.svg

Avoiding sizing correction

If you want to throw an error when you request a size that is not ability to be delivered (as opposed to the default approach of overriding your request), you can change the global rcParams’s num_attempts. Specifically, by setting cow.rcParams["num_attempts"] = 1 you’ll observe an error thrown if you define an unobtainable size.

import pytest
cow.rcParams["num_attempts"] = 1

with pytest.raises(Exception) as e_info:
    vis_total.show(width = 1.2, height = .9)
print(e_info)
    
# setting the parameter back
cow.rcParams["num_attempts"] = 2 # current default
<ExceptionInfo StopIteration('Attempts to find the correct sizing of innerplots failed with provided parameters') tblen=3>

2

On the flip side, if you want such things in your jupyter notebooks you can change the css of your notebooks by injecting them with something like this:

.output_svg > div {
    max-width:100% !important; 
}

One way to do this would be to create a code block in each notebook and put something like the following in it (note that we didn’t have the idea first, we just can’t find the reference…).

%%html
<style>
.output_svg > div {
    max-width:100% !important; 
}
</style>
1

The equal sizing for each block is within dimensions not across dimensions. That is, each row is not necessarily the same height as the width of each column.