2  Examples from the ggplot2 documentation

The following examples are adapted from the ggplot2 book’s coord chapter.

Caution

All of the examples on this page require the coord-polar branch of iangow/plotnine. The standard version of plotnine does not include the coord_polar() and coord_radial() functions used here.

The source code for this repository can be found here and includes a pyproject.toml file that can be used with uv to install everything needed to run all the examples here.

import numpy as np
import polars as pl

from plotnine import (
    ggplot, aes,
    geom_bar, geom_path, geom_point,
    coord_polar,
    scale_x_discrete, scale_y_continuous,
    theme,
)

from plotnine.data import mtcars

2.1 Bar → Pie → Bullseye (mtcars)

A stacked bar chart becomes a pie chart when theta = "y" maps height (count) to angle, and a bullseye chart when the default theta = "x" maps the discrete x position to angle instead.

mtcars_pl = (
    pl.from_pandas(mtcars)
    .with_columns(
        pl.lit("1").alias("x"),
        pl.col("cyl").cast(pl.Utf8),
    )
)

The code below shows how we can turn a bar into a pie chart or a bullseye chart by changing the coordinate system.

base = (
    ggplot(mtcars_pl, aes(x="x", fill="cyl"))
    + geom_bar(width=1)
    + scale_x_discrete(expand=(0, 0))
    + scale_y_continuous(expand=(0, 0))
    + theme(legend_position="none", figure_size=(4, 4))
)

base.show()
Figure 2.1: Stacked bar chart

In the original, the following plot has the \(y\)-axis labels from Figure 2.1 as labels around the circle.

base + coord_polar(theta="y")
Figure 2.2: Pie chart: coord_polar(theta=‘y’)

Unlike the ggplot2 original, Figure 2.2 is missing count labels around the circumference. The reason is that coord_polar.setup_panel_params explicitly suppresses the theta-axis breaks before handing the panel view to Matplotlib:

new_x = replace(pv_exp.x, ..., breaks=[], labels=[])

When theta = "y" the \(y\)-scale breaks (e.g. \(0, 5, 10, \dots\)) are the values that should appear at their corresponding angles around the circle, but they are discarded here before rendering begins.

Because the underlying axes object is already a Matplotlib PolarAxes — the facet creates it with projection='polar' whenever coord_polar is in use — the angular label machinery already exists.
PolarAxes.set_thetagrids(angles_in_degrees, labels) places labelled tick marks at arbitrary angles around the circumference, which is exactly what is needed.

A targeted fix in coord_polar.py would:

  1. Stash the theta-axis breaks (in data units) inside self.params during setup_panel_params, before zeroing them out.
  2. In draw(), convert those saved breaks to degrees via _to_radians (then multiply by \(180/\pi\)) and call polar_ax.set_thetagrids(degrees, labels).

No geometry or data-transformation code would need to change; the fix is entirely within the axis-decoration layer.

In the original, the following plot has the \(y\)-axis labels from Figure 2.1 as labels along the vertical axis.

base + coord_polar()
Figure 2.3: Bullseye chart: coord_polar()

2.2 Polar path (spiral)

In ggplot2 a straight line in Cartesian space becomes a spiral in polar coordinates. theta = "x" maps the x-column (here: angle in radians) to the angular axis; y maps to radius.

n = 100
spiral = pl.DataFrame({
    "r":     np.linspace(0, 1, n),
    "theta": np.linspace(0, 3 / 2 * np.pi, n),
})

(
    ggplot(spiral, aes(x="theta", y="r"))
    + geom_path()
    + geom_point(size=2, color="red")
    + coord_polar()
    + theme(figure_size=(5, 5))
)
Figure 2.4: A Cartesian line becomes a spiral under coord_polar()