Skip to content

ggplot2 extensions

ggplot2 has a large family of extension packages in R. ggrust builds many of the most popular ones straight into the library, with the R package's function and parameter names, so there is nothing extra to install. lets-plot has none of these, so this is where ggrust goes beyond a lets-plot replacement.

Each feature below credits the R package it comes from. ggrust reimplements the behavior in Rust; the R packages and their authors designed it.

Feature ggrust From the R package
Beeswarm and quasirandom points geom_beeswarm, geom_quasirandom ggbeeswarm
Journal color palettes scale_color_npg, _aaas, _nejm, _lancet, _jama, _jco, _d3, _igv (and scale_fill_*) ggsci
Significance tests and labels stat_compare_means, stat_cor, stat_poly_eq ggpubr, ggpmisc
Cluster marks geom_mark_hull, geom_mark_ellipse, geom_mark_rect ggforce
Highlighting gghighlight gghighlight
Point density geom_pointdensity ggpointdensity
Side panels geom_xside*, geom_yside* (tile, bar, col, density, histogram, boxplot, violin, point, line, text), ggmarginal ggside
Several color scales in one plot new_scale_fill, new_scale_color, new_scale ggnewscale
Dendrograms hclust, scale_y_dendro, scale_x_dendro, guide_axis_dendro, geom_dendrogram legendry, ggh4x, ggdendro
Raincloud plots stat_halfeye, stat_slab, stat_interval, stat_pointinterval, geom_half_violin, geom_half_boxplot, geom_half_point ggdist, gghalves
Figure layouts gggrid(design=, tag_levels=), inset_element patchwork
Rasterized layers rasterize ggrastr, scattermore

Two more, geom_text_repel / geom_label_repel (ggrepel) and geom_area_ridges (ggridges), come with the lets-plot API and are covered in Layers. ggrust also adds core ggplot2 features lets-plot lacks: geom_col, geom_rug, stat_ellipse, and secondary axes with sec_axis / dup_axis.

Beeswarm points

geom_beeswarm places points side by side so that none overlap, which shows every observation and the shape of the distribution at once. geom_quasirandom spreads them by the local density instead. Both follow ggbeeswarm by Erik Clarke, Scott Sherrill-Mix and Charlotte Dawson.

import polars as pl
from ggrust import *

DATA = "https://raw.githubusercontent.com/datavil/ggrust/main/docs/data/"
penguins = pl.read_csv(DATA + "penguins.csv").drop_nulls("body_mass_g")

plot = (
    ggplot(penguins, aes("species", "body_mass_g", color="species"))
    + geom_beeswarm(size=2, cex=2.5, show_legend=False)
    + scale_color_npg()
    + labs(x="", y="body mass (g)")
    + theme_light()
    + ggsize(800, 500)
)
plot

Journal palettes

The scale_color_* / scale_fill_* palettes of ggsci by Nan Xiao take their colors from scientific journals and data tools: npg (Nature Publishing Group), aaas (Science), nejm, lancet, jama, jco, d3 and igv. igv has 51 colors, enough for many clusters. The examples on this page use several of them.

Significance tests

stat_compare_means runs the test (t.test, wilcox.test, anova, kruskal.test), adjusts the p-values if asked (p_adjust_method) and draws the brackets and labels, as in ggpubr by Alboukadel Kassambara. The statistics are computed in Rust and match R's own tests. stat_cor labels a correlation and stat_poly_eq a fitted polynomial, as in ggpubr and ggpmisc by Pedro J. Aphalo.

import polars as pl
from ggrust import *

DATA = "https://raw.githubusercontent.com/datavil/ggrust/main/docs/data/"
penguins = pl.read_csv(DATA + "penguins.csv").drop_nulls("flipper_length_mm")

plot = (
    ggplot(penguins, aes("species", "flipper_length_mm"))
    + geom_boxplot(aes(fill="species"), alpha=0.6, outlier_alpha=0, show_legend=False)
    + geom_quasirandom(aes(color="species"), size=1.2, width=0.25, show_legend=False)
    + stat_compare_means(
        comparisons=[("Adelie", "Chinstrap"), ("Chinstrap", "Gentoo"), ("Adelie", "Gentoo")],
        label="p.signif",
    )
    + scale_fill_jco()
    + scale_color_jco()
    + labs(x="", y="flipper length (mm)")
    + theme_classic()
    + ggsize(800, 550)
)
plot

Cluster marks

geom_mark_hull, geom_mark_ellipse and geom_mark_rect outline each group and can label it outside the data, as in ggforce by Thomas Lin Pedersen.

import polars as pl
from ggrust import *

DATA = "https://raw.githubusercontent.com/datavil/ggrust/main/docs/data/"
penguins = pl.read_csv(DATA + "penguins.csv").drop_nulls("bill_length_mm")

plot = (
    ggplot(penguins, aes("bill_length_mm", "bill_depth_mm"))
    + geom_mark_hull(
        aes(fill="species", label="species"), concavity=8, alpha=0.15, show_legend=False
    )
    + geom_point(aes(color="species"), size=2, show_legend=False)
    + scale_color_aaas()
    + scale_fill_aaas()
    + expand_limits(x=[28, 64], y=[12, 23])
    + labs(x="bill length (mm)", y="bill depth (mm)")
    + theme_minimal()
    + ggsize(800, 600)
)
plot

Highlighting

gghighlight keeps the groups that match a condition in color and draws the rest in grey behind them, with optional direct labels. The condition is a Polars expression, evaluated per group by default. It follows gghighlight by Hiroaki Yutani.

import polars as pl
from ggrust import *

DATA = "https://raw.githubusercontent.com/datavil/ggrust/main/docs/data/"
cities = pl.read_csv(DATA + "cities.csv")

plot = (
    ggplot(cities, aes("month", "temperature", color="city"))
    + geom_line(size=1.5)
    + gghighlight(pl.col("temperature").min() < 0, label_key="city")
    + scale_x_continuous(breaks=list(range(1, 13)))
    + labs(y="mean temperature (°C)")
    + theme_minimal()
    + ggsize(800, 500)
)
plot

Point density

geom_pointdensity colors each point by the number of neighbours around it, so dense regions stand out in a scatter plot that would otherwise overplot. It follows ggpointdensity by Lukas P. M. Kremer, and stays fast on millions of points.

import polars as pl
from ggrust import *

DATA = "https://raw.githubusercontent.com/datavil/ggrust/main/docs/data/"
homes = pl.read_csv(DATA + "homes.csv")

plot = (
    ggplot(homes, aes("area_m2", "price_k"))
    + geom_pointdensity(size=2, adjust=4)
    + scale_color_viridis()
    + labs(x="floor area (m²)", y="price (k)", color="neighbours")
    + theme_light()
    + ggsize(800, 550)
)
plot

Side panels

geom_xside* and geom_yside* layers draw into a panel beside the main one that shares its x or y axis, after ggside by Justin Landis. They stay aligned with the main panel, also when you zoom. ggmarginal is lets-plot's shortcut for the same idea.

Unlike ggside, an axis can have more than one band: side_pos="left" (or "right", "top", "bottom") puts a layer's band on that side, side_band="name" gives it a band of its own (bands on one side stack outward in plot order) and side_size=0.2 sets its thickness. scale_ysidex_discrete(side_pos="left", ...) sets that band's own axis.

import polars as pl
from ggrust import *

DATA = "https://raw.githubusercontent.com/datavil/ggrust/main/docs/data/"
penguins = pl.read_csv(DATA + "penguins.csv").drop_nulls("bill_length_mm")

plot = (
    ggplot(penguins, aes("flipper_length_mm", "bill_length_mm", color="species"))
    + geom_point(size=1.8)
    + geom_xsidedensity(aes(fill="species"), alpha=0.4)
    + geom_ysideboxplot(aes(x="species", fill="species"), alpha=0.4, show_legend=False)
    + scale_color_nejm()
    + scale_fill_nejm()
    + labs(x="flipper length (mm)", y="bill length (mm)")
    + theme_light()
    + ggsize(800, 600)
)
plot

Clustered heatmaps

A clustered heatmap combines three extensions:

  • hclust clusters the rows (or columns) and scale_y_dendro orders the axis by the tree and draws it beside the labels, after legendry and ggh4x by Teun van den Brand. geom_dendrogram draws a tree as a layer, as ggdendro by Andrie de Vries does. A tree computed elsewhere, for example by scipy or scanpy, can be passed in as its linkage matrix.
  • new_scale_fill starts a second fill scale, so each annotation track gets its own colors and legend, as in ggnewscale by Elio Campitelli.
  • geom_ysidetile draws the annotation track beside the heatmap (see Side panels).
import polars as pl
from ggrust import *

DATA = "https://raw.githubusercontent.com/datavil/ggrust/main/docs/data/"
fruit = pl.read_csv(DATA + "fruit.csv")
nutrients = ["calories", "sugar_g", "fibre_g", "vitamin_c_mg"]
scaled = fruit.with_columns((pl.col(n) - pl.col(n).mean()) / pl.col(n).std() for n in nutrients)

rows = hclust(scaled.select("fruit", *nutrients), labels="fruit", method="average")
long = scaled.unpivot(index="fruit", on=nutrients, variable_name="nutrient", value_name="z")

plot = (
    ggplot(long, aes("nutrient", "fruit"))
    + geom_tile(aes(fill="z"), color="white")
    + scale_fill_gradient2(low="#2166ac", mid="white", high="#b2182b", name="z-score")
    + scale_y_dendro(rows)
    + new_scale_fill()
    + geom_ysidetile(
        aes(x="calorie", fill="calories"), data=fruit.with_columns(calorie=pl.lit("kcal"))
    )
    + scale_fill_viridis(option="cividis", name="kcal")
    + labs(x="", y="")
    + theme_minimal()
    + ggsize(800, 650)
)
plot

Raincloud plots

stat_halfeye draws a density slab with the median and quantile intervals, after ggdist by Matthew Kay. geom_half_violin, geom_half_boxplot and geom_half_point draw half of each shape, after gghalves by Frederik Tiedemann. Together they make raincloud plots; panel B of the next example is a stat_halfeye.

Figure layouts

gggrid accepts a design string that names a cell per letter, and tag_levels tags the panels A, B, C (or a, 1, i). inset_element places a plot inside another. These follow patchwork by Thomas Lin Pedersen.

import polars as pl
from ggrust import *

DATA = "https://raw.githubusercontent.com/datavil/ggrust/main/docs/data/"
penguins = pl.read_csv(DATA + "penguins.csv").drop_nulls("bill_length_mm")

scatter = (
    ggplot(penguins, aes("bill_length_mm", "bill_depth_mm", color="species"))
    + geom_point(show_legend=False)
    + scale_color_lancet()
    + labs(x="bill length (mm)", y="bill depth (mm)")
)
mass = (
    ggplot(penguins, aes("species", "body_mass_g", fill="species"))
    + stat_halfeye(show_legend=False)
    + scale_fill_lancet()
    + labs(x="", y="body mass (g)")
)
islands = (
    ggplot(penguins, aes("island", fill="species"))
    + geom_bar()
    + scale_fill_lancet()
    + labs(x="", y="penguins")
)

plot = gggrid(
    [scatter, mass, islands],
    design="AAB\nCCB",
    tag_levels="A",
    guides="collect",
) + ggsize(900, 600)
plot

Rasterized layers

rasterize(layer, dpi=300) draws one layer as an image inside SVG and PDF output while axes and text stay vector, after ggrastr by Viktor Petukhov, Teun van den Brand and Evan Biederstedt, and scattermore by Tomas Sieger and Mirek Kratochvil. A scatter plot of a million points then saves as a small PDF that still has sharp, selectable text.

plot = ggplot(cells, aes("umap_1", "umap_2")) + rasterize(geom_point(size=0.5), dpi=300)
ggsave(plot, "umap.pdf")