Composing plots with plotnine has just become possible. Well, not quite yet. As of the writing of this blog post, the latest development version of plotnine is plotnine==v0.15.0a1
. And there is a discussion issue open where the feature has been teased: https://github.com/has2k1/plotnine/discussions/929
Copying the example from that issue, we can reproduce the tiling mentioned in the post:
from plotnine import *
from plotnine.data import mtcars
p1 = ggplot(mtcars) + geom_point(aes("wt", "mpg")) + labs(tag="a)")
p2 = (
ggplot(mtcars)
+ geom_boxplot(aes("wt", "disp", group="gear"))
+ labs(tag="b)")
)
p3 = ggplot(mtcars) + geom_smooth(aes("disp", "qsec")) + labs(tag="c)")
p4 = ggplot(mtcars) + geom_bar(aes("carb"))
(p1 | p2 | p3) / p4
/home/paul/miniforge3/envs/post_plotnine/lib/python3.12/site-packages/plotnine/stats/smoothers.py:345: PlotnineWarning: Confidence intervals are not yet implemented for lowess smoothings.
Wonderful. What did we do there? We generated 4 plotnine objects of type plotnine.ggplot.ggplot
. And tiling seems to be done by using the operators |
and /
, with rows being grouped using ()
.
So in the example, we paste three plots in the first row and have a last plot spanning three columns in the second row. Nifty.
And using the tag
argument, the top left corner of each plot gets an identifying label, turning a plot into a panel of a multi-panel figure. Very easy. This feature was introduced in ggplot2 in 2020 with the release of version 3.3.0
and to plotnine in commit 765dc1c in 2025.
Now of course, I want to see if the tiling works well, even when using faceting or when using theming. So let’s try some things out.
import plotnine as p9
from plotnine.data import mtcars
p1 = (
p9.ggplot(mtcars)
+ p9.geom_point(mapping=p9.aes("wt", "mpg"))
+ p9.labs(tag="A")
)
p2 = (
p9.ggplot(mtcars)
+ p9.geom_point(mapping=p9.aes("wt", "mpg"))
+ p9.labs(tag="B")
+ p9.facet_grid("~gear")
)
p3 = (
p9.ggplot(mtcars)
+ p9.geom_point(mapping=p9.aes("wt", "mpg"))
+ p9.labs(tag="C")
+ p9.facet_grid("carb~")
)
p4 = (
p9.ggplot(mtcars)
+ p9.geom_point(mapping=p9.aes("wt", "mpg"))
+ p9.labs(tag="D")
+ p9.facet_grid("carb~gear")
)
(p1 | p2) / (p3 | p4)
Another feature of the current implementation seems to be that the figure size stays the same, no matter how we arrange the plots.
This leads to the grid lines somehow disappearing for longer plots. So, one must play with the figure size, which makes them reappear.
After playing around with + p9.theme(figure_size=(X,Y)
a bit, it seems to me that the last applied figure size (in this case applied to p4
) defines the output size of the plot in the Jupyter notebook. Making the plot larger will lead to the gridlines coming back. So ultimately, it’s a resolution question.
One feature that is currently missing, or at least seems to be missing, is setting the relative height of rows 3 and 4 to twice or 4 times the height of rows 1 and 2.
Learning plotnine a bit more and theming
After my last blog post, I thought it would be cool to have a theme I can use repeatedly for my plotting needs. Of course, this is unlikely to be set in stone, but maybe having a good starting point is all I want.
A few features I would like to have:
- Make “Shape 21” aka “o” the default in point plots.
- Font choosing
- Sane defaults for theming
- Default colors that are nice
I am curious how far I can mod plotnine to fit this bill.
# Set my defaults for geom point
p9.geoms.geom_point.DEFAULT_AES["shape"] = "o"
p9.geoms.geom_point.DEFAULT_AES["size"] = 2
p9.geoms.geom_point.DEFAULT_AES["fill"] = "none"
p9.geoms.geom_point.DEFAULT_AES["color"] = "black"
class custom_theme(p9.theme_bw):
def __init__(
self,
base_size: int = 12,
base_family: str | None = ["Roboto", "sans-serif"],
rotate_label: int = 0,
legend_position: str = "outside",
):
super().__init__(base_size, base_family)
self += p9.theme(
panel_grid=p9.element_blank(), # Remove any grid lines
panel_border=p9.element_blank(), # Remove the border around the plot
axis_line_x=p9.element_line(color="black"),
axis_line_y=p9.element_line(color="black"),
axis_text=p9.element_text(
linespacing=1.2, color="black", size=base_size
),
axis_ticks_pad_major_x=3,
axis_ticks_pad_major_y=1,
axis_ticks_length=7,
legend_key_size=12,
legend_key=p9.element_blank(),
legend_title=p9.element_blank(),
legend_text=p9.element_text(size=base_size, margin={"l": 5}),
plot_margin=0.005, # We dont need extra whitespace usually
figure_size=(4, 4),
dpi=100, # For display inspection, save with dpi=300 for PNG
plot_title=p9.element_text(size=base_size, ha="center"),
svg_usefonts=False,
)
if rotate_label > 0:
self += p9.theme(axis_text_x=p9.element_text(rotation=rotate_label))
if legend_position == "inside":
self += p9.theme(
legend_position="inside",
legend_position_inside=(1, 1), # Set to (0,1) for top left)
)
p9.theme_set(custom_theme())
point_plot = (
p9.ggplot(mtcars)
+ p9.geom_point(mapping=p9.aes("wt", "mpg"))
+ p9.labs(tag="A")
)
box_plot = (
p9.ggplot(mtcars, mapping=p9.aes(x="cyl", y="mpg", group="cyl"))
+ p9.geom_boxplot()
)
bar_plot = (
p9.ggplot(mtcars.head(4), mapping=p9.aes(x="name", y="cyl"))
+ p9.geom_col()
+ p9.scale_y_continuous(expand=(0, 0))
+ custom_theme(rotate_label=35)
+ p9.labs(x="")
)
point_plot | box_plot | bar_plot + p9.theme(figure_size=(4 * 3, 4))
That already looks really good. So I was interested in colors:
point_plot = (
p9.ggplot(mtcars)
+ p9.geom_point(mapping=p9.aes("wt", "mpg"))
+ p9.labs(tag="A")
)
box_plot = (
p9.ggplot(
mtcars,
mapping=p9.aes(x="cyl", y="mpg", group="cyl", fill="factor(cyl)"),
)
+ p9.geom_boxplot()
+ p9.scale_fill_hue(color_space="hsluv")
+ p9.theme(legend_position="inside", legend_position_inside=(1, 1))
)
bar_plot = (
p9.ggplot(mtcars.head(4), mapping=p9.aes(x="name", y="cyl", fill="mpg"))
+ p9.geom_col(color="black")
+ p9.scale_y_continuous(expand=(0, 0))
+ custom_theme(rotate_label=35)
+ p9.labs(x="")
)
point_plot | box_plot | bar_plot + p9.theme(figure_size=(4 * 3, 4))
Seeing as plotnine is becoming a very solid plotting engine, I decided to write a small Python package to enable some of the theming I showed here by simply importing a package. I am developing this under github.com/openpaul/p9customtheme, and usage is as simple as importing the package.
uv pip install p9customtheme
To show the differences here is a plot with the default plotnine theme and settings:
import plotnine as p9
from plotnine.data import penguins
p = (
p9.ggplot(penguins, p9.aes("species", "bill_length_mm", fill="island"))
+ p9.geom_boxplot()
+ p9.labs(
title="Penguin bill length by species and island".title(),
subtitle="A comparison based on example data",
x="species",
y="bill length [mm]",
fill="Island",
)
)
p
/home/paul/miniforge3/envs/post_plotnine/lib/python3.12/site-packages/plotnine/layer.py:284: PlotnineWarning: stat_boxplot : Removed 2 rows containing non-finite values.
And here is the same plot using the new theme plus some custom colors from that theme:
from p9customtheme import custom_discrete, custom_theme
p + custom_theme() + custom_discrete()
/home/paul/miniforge3/envs/post_plotnine/lib/python3.12/site-packages/plotnine/layer.py:284: PlotnineWarning: stat_boxplot : Removed 2 rows containing non-finite values.
After importing, the package sets itself as the new default theme of plotnine. It also provides an easy way of getting some nice non-default colors, which hopefully will make figure building a bit more entertaining. And overall, it simply makes the process of going from data to a presentation-ready plot a bit smoother.
I think that this is a pretty cool package and am excited to use it in this blog for my next posts.