Recently, I’ve been helping the plotting library plotnine—a port of ggplot2 to Python. plotnine normally is used to make plots for data analysis. But what if I told you there is another option: cobbling up generative art.
In this post I’ll walk through the basics of using plotnine to create generative art. I’ll look at three pieces:
theme()
elements (like axis ticks)If you’re curious about plotnine and generative art, this is a great opportunity to submit something artsy to the 2024 Plotnine Contest (deadline is 26 July 2024).
import polars as pl
import numpy as np
from plotnine import *
from mizani.palettes import brewer_pal, gradient_n_pal
np.random.seed(345678)
# generate random areas for each group to fill per year ---------
# Note that in the data the x-axis is called Year, and the
# filled bands are called Group(s)
opts = [0] * 100 + list(range(1, 31))
values = []
for ii in range(30):
values.extend(np.random.choice(opts, 30, replace=False))
# Put all the data together -------------------------------------
years = pl.DataFrame({"Year": list(range(30))})
groups = pl.DataFrame({"Group": [f"grp_{ii}" for ii in range(30)]})
df = (
years.join(groups, how="cross")
.with_columns(Values=pl.Series(values))
.with_columns(prop=pl.col("Values") / pl.col("Values").sum().over("Year"))
)
df.write_csv("plot-data.csv")
# Generate color palette ----------------------------------------
# this uses 12 colors interpolated to all 30 Groups
pal = brewer_pal("qual", "Paired")
colors = pal(12)
np.random.shuffle(colors)
all_colors = gradient_n_pal(colors)(np.linspace(0, 1, 30))
# Plot ---------------------------------------------------------
(
df
>> ggplot(aes("Year", "prop", fill="Group"))
+ geom_area()
+ scale_fill_manual(values=all_colors)
+ theme(
axis_text=element_blank(),
line=element_blank(),
title=element_blank(),
legend_position="none",
plot_margin=0,
panel_border=element_blank(),
panel_background=element_blank(),
)
)
(Note that this plot is based on this r-graph-gallery post)
In order to focus on plotting, I’ll use polars
to import the data used to create the plot above.
import polars as pl
df = pl.read_csv("https://mchow.com/posts/plotnine-art/plot-data.csv")
The data is 30 Groups (filled bands) across 30 Years (x-axis), making 900 rows in total:
df.head()
shape: (5, 4)
┌──────┬───────┬────────┬──────────┐
│ Year ┆ Group ┆ Values ┆ prop │
│ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ i64 ┆ f64 │
╞══════╪═══════╪════════╪══════════╡
│ 0 ┆ grp_0 ┆ 0 ┆ 0.0 │
│ 0 ┆ grp_1 ┆ 0 ┆ 0.0 │
│ 0 ┆ grp_2 ┆ 0 ┆ 0.0 │
│ 0 ┆ grp_3 ┆ 16 ┆ 0.153846 │
│ 0 ┆ grp_4 ┆ 0 ┆ 0.0 │
└──────┴───────┴────────┴──────────┘
The height of the band is decided by the prop
column, which is more likely than not to be 0. This makes the bands often cinch down to nothing and then expand again.
In order to plot the data, we’ll import plotnine
, and use the geom_area()
function, with Year
on the x-axis, and prop
on the y-axis.
from plotnine import *
(df >> ggplot(aes("Year", "prop", fill="Group")) + geom_area())
Notice that this produced roughly the original art plot, but with two differences:
In order to customize the fill colors, we can use some helper functions from the mizani
library. mizani
is used behind the scenes by plotnine
. In the code below, we’ll create a 12-color brewer palette, and then interpolate between those colors to be able to fill in all 30 groups.
import numpy as np
from mizani.palettes import brewer_pal, gradient_n_pal
pal = brewer_pal("qual", "Paired")
colors = pal(12)
np.random.shuffle(colors)
all_colors = gradient_n_pal(colors)(np.linspace(0, 1, 30))
Notice that there are two big steps for defining colors:
pal = brewer_pal("qual", "Paired")
gets the full color brewer palette, and pal(12)
gets 12 colors from that palette.gradient_n_pal()
takes those colors, and let’s us interpolate between them.With custom colors in hand, we can manually set the fill values in plotnine
:
(
df
>> ggplot(aes("Year", "prop", fill="Group"))
+ geom_area()
+ scale_fill_manual(values=all_colors)
)
This is looking much closer, but still has extra plotting elements (like a legend). In the next section, we’ll remove these to create a beautiful blank canvas.
In order to remove unecessary elements, we’ll use the theme()
function.
This function allows you to customize various elements (like increasing the size of x- or y-axis ticks). For our purposes, we’ll use it to hide most elements.
(
df
>> ggplot(aes("Year", "prop", fill="Group"))
+ geom_area()
+ scale_fill_manual(values=all_colors)
+ theme(
axis_text=element_blank(),
line=element_blank(),
title=element_blank(),
legend_position="none",
plot_margin=0,
panel_border=element_blank(),
panel_background=element_blank(),
)
)
Notice everything outside the geom_area()
fills is now hidden. We also set plot_margin=0
, so that there wouldn’t be an extra band around the plot.
Here’s all the code in one place, so it’s easier to copy and paste (or see this notebook, which also generates the data.).
import polars as pl
import numpy as np
from mizani.palettes import brewer_pal, gradient_n_pal
df = pl.read_csv("https://mchow.com/posts/plotnine-art/plot-data.csv")
pal = brewer_pal("qual", "Paired")
colors = pal(12)
np.random.shuffle(colors)
all_colors = gradient_n_pal(colors)(np.linspace(0, 1, 30))
(
df
>> ggplot(aes("Year", "prop", fill="Group"))
+ geom_area()
+ scale_fill_manual(values=all_colors)
+ theme(
axis_text=element_blank(),
line=element_blank(),
title=element_blank(),
legend_position="none",
plot_margin=0,
panel_border=element_blank(),
panel_background=element_blank(),
)
)
Generative art is a neat area, and we have just scratched the surface—by looking at how to represent data art using plotnine
. If you want to try your hand at generating art in plotnine
, please consider submitting to the 2024 Plotnine Contest (deadline 26 July 2024)!
Follow on Twitter | Hucore theme & Hugo ♥