How to plot networks with bipartite

Tobias Bauer

Animal Network Ecology, University of Hamburg
tobias.bauer-2@uni-hamburg.de

August 21, 2025

Abstract

This vignette provides an overview of the plotting capabilities of the bipartite package. It includes multiple examples illustrating the intention behind various function arguments.

Contents

Preparing a web for plotting

First, we have to install the bipartite-package.

install.packages("bipartite")

After the installation we can load the package.

library(bipartite)
set.seed(123)

To be able to demonstrate the different ways to plot a web, we first need … a web. And to get one we might also want to understand how a web is defined in the bipartite-package.

Note: in this document we will only describe the necessary structure of a web. For a more in depth guide on how to load your data into bipartite have a look at the Intro2bipartite vignette.

To do so, we will use the function genweb, which generates a random bipartite web. The arguments N1 and N2 define the number of species in the lower and higher trophic levels, respectively.

web <- genweb(N1 = 5, N2 = 6)

After creating the web we might want to have a look at its structure to be able to understand it better.

str(web)
##  int [1:5, 1:6] 0 2 0 0 1 0 2 1 0 0 ...
web
##      [,1] [,2] [,3] [,4] [,5] [,6]
## [1,]    0    0    1    1    1    1
## [2,]    2    2    1   14   16    0
## [3,]    0    1    0    9    6    0
## [4,]    0    0    0    0    1    1
## [5,]    1    0    0    1    1    0
class(web)
## [1] "matrix" "array"

As we can see, a web in the bipartite package is simply an \(n \times m\) integer matrix (more generally, a numeric matrix). The rows and columns represent the nodes of the two independent sets in the graph. And the values indicate the weight of the edges between two nodes.

As bipartite is mainly developed with ecological applications in mind, nodes are often equated with individual species in this context. Ecologists also often think in trophic levels, thus the two independent sets will be referred to as such. The columns thus represent the higher trophic level, the rows the lower. To demonstrate this, we will change the row and column names within the web.

colnames(web) <- paste("Higher", 1:6)
rownames(web) <- paste("Lower", 1:5)
web
##         Higher 1 Higher 2 Higher 3 Higher 4 Higher 5 Higher 6
## Lower 1        0        0        1        1        1        1
## Lower 2        2        2        1       14       16        0
## Lower 3        0        1        0        9        6        0
## Lower 4        0        0        0        0        1        1
## Lower 5        1        0        0        1        1        0

To demonstrate the plotting capabilities of bipartite, we will use the included plant-pollinator network Safariland.

?Safariland
data(Safariland)
dim(Safariland)
## [1]  9 27
str(Safariland)
##  int [1:9, 1:27] 673 0 0 0 0 0 0 0 0 0 ...
##  - attr(*, "dimnames")=List of 2
##   ..$ : chr [1:9] "Aristotelia chilensis" "Alstroemeria aurea" "Schinus patagonicus" "Berberis darwinii" ...
##   ..$ : chr [1:27] "Policana albopilosa" "Bombus dahlbomii" "Ruizantheda mutabilis" "Trichophthalma amoena" ...

As we can see its a 9x27 integer matrix. The help reveals that the 9 rows represent plant species and the 27 columns represent different pollinators.

Plotting the old way

Up to version 2.20 of bipartite the standard way to visualize networks was using the plotweb function.

NOTE: In this vignette the plotting device is square with a size of 7x7 inches. On devices with other sizes the results may differ.

?plotweb
plotweb_deprecated(web)
plotweb_deprecated(Safariland)

However, as you can see, for larger networks the placement of the labels quickly becomes somewhat confusing.

plotweb_deprecated(Safariland, text.rot = 90)

Plotting the new way

Since version 2.22 bipartite contains a refined version of the plotweb function.

Among other things the stacking of labels is replaced in that version. And instead by default the labels are scaled so that they will always fit in the plot. The default function call stays the same, so for our randomly generated web the results is.

plotweb(web)

However, when we have a look at the plot produced for the Safariland web, …

plotweb(Safariland)

it becomes clear that this approach may not be ideal for crowded webs. So for networks with many species—especially those with long names—we recommend rotating the axis labels (e.g., by 90°) using the srt argument.

plotweb(Safariland, srt = 90)

Now the graph is way more readable, even without a magnifying glass.

Text size and spacing

The “magic” behind these well-scaled, out-of-the-box plots lies in the text_size and spacing arguments. By default, text_size is set to "auto" and spacing to 0.3. This means that, unless specified otherwise, the spacing between the boxes on each side will occupy 30% of the total available space in that axis, and the size of the text labels is automatically scaled so that they cannot overlap.

To better understand the spacing parameter, let us have a look at a simple layout consisting of 2 columns and 1 row. To help visualize the structure, we enable the x- and y-axes by setting plot_axes = TRUE. In the resulting plot, the row1 box spans exactly 0.7 units in width, while the col1 and col2 boxes are each 0.35 units wide. As described above, this leaves 0.3 units—or 30% of the total width—as spacing between the elements.

web2 <- matrix(c(50, 50), ncol = 2)
plotweb(web2, plot_axes = TRUE)

If we explicitly set the spacing value to 0.1, we can see that the width of the lower box increases to 0.9 units, and the size of the upper boxes increases to 0.45 units each.

plotweb(web2, spacing = 0.1, plot_axes = TRUE)

If you prefer larger labels, you can set text_size to a value greater than 1 to increase their size. Conversely, if you want smaller labels, set text_size to a value between 0 and 1.

plotweb(web2, spacing = 0.1, text_size = 2)

If you have found a text_size that you like and simply want to scale the plot automatically so that labels do not overlap, you can also set spacing = "auto".

plotweb(Safariland, spacing = "auto", srt = 90, text_size = 0.75)

The option text_size = "auto" is designed to automatically reduce the text size only when necessary. If all labels fit within the plot at the default size of 1, that size will be used. Otherwise, the text size is scaled down just enough to ensure everything fits without overlap.

Switching to a horizontal plot

If you prefer your labels to be horizontal, another option is to rotate the whole plot by 90° altogether. This can be accomplished by setting horizontal = TRUE.

plotweb(web, horizontal = TRUE)

Now we are able to clearly read every label in the natural reading direction and still interpret the plotted interactions.

Sorting the web

By default, the species in the plot are arranged according to the rows and columns of the given web matrix. The first row/column is therefore plotted at the left of the plot (or at the top for horizontal plots). To arrange the plot in a specific order, simply provide that order when calling plotweb. Like in the example below, in which we reverse the row order for the plotweb call.

plotweb(web[rev(rownames(web)), ], horizontal = TRUE)

Now the plant species are plotted in reverse order.

However a built-in call to sortweb is also possible with the argument sorting. The options are the same as for sortweb. So dec orders the species in decreasing row/column totals, inc orders by increasing totals, and ca performs a correspondence analysis which might reduce link crossings in the plot.

plotweb(Safariland, horizontal = TRUE, sorting = "dec")

plotweb(Safariland, horizontal = TRUE, sorting = "inc")

plotweb(Safariland, horizontal = TRUE, sorting = "ca")

Adding independent abundances

In some cases, in addition to the recorded interactions, data on the actual abundance of some or all species may also be available. In such cases, it can be useful to modify the plot so that the boxes reflect species abundances, while the link widths continue to represent interaction preferences. This can be done using the two arguments higher_abundances and lower_abundances.

These take a named vector each—for the higher trophic level species (columns) and the lower trophic level species (rows).

Since we don’t have actual recorded abundances, we will generate some randomly for demonstration purposes.

n_lower_species <- nrow(Safariland)
lower_abundances <- sample(0:100, n_lower_species, replace = TRUE)
names(lower_abundances) <- rownames(Safariland)
print(lower_abundances)
##    Aristotelia chilensis       Alstroemeria aurea      Schinus patagonicus 
##                       75                       92                       42 
##        Berberis darwinii          Rosa eglanteria         Cynanchum diemii 
##                       23                       71                       64 
##       Ribes magellanicum        Mutisia decurrens Calceolaria crenatiflora 
##                       86                       34                       61

Now we have a named vector with a random abundance for each row (lower-level species / plants) in the Safariland web, as required by the function. When we call the function with this argument, we get the following result.

plotweb(Safariland, sorting = "ca", horizontal = TRUE, 
        lower_abundances = lower_abundances)

We now have abundances for the pollinator species that were randomly drawn from a uniform distribution between 0 and 100. As we can see the box sizes on the right are more homogeneous.

We can also see that some of the links are increasing or decreasing in size from the left to the right.

Adding additional abundances

Sometimes, not all individuals of a species are involved in observed interactions—such as unvisited plants, or unparasitized hosts.

For these cases, specifying independent abundances is not sufficient. However, additional abundances can be defined using the arguments add_higher_abundances and add_lower_abundances. Simply set these to the number of individuals not observed in the interaction matrix. These individuals will then be drawn as an extension to the main box.

Both arguments, just as their independent counterparts, take a named vector of all species as input. Set add_lower_abundances To demonstrate we take the same abundances as above.

plotweb(Safariland, sorting = "ca", horizontal = TRUE,
        add_lower_abundances = lower_abundances)

As you can see the additional abundances are now plotted as red extra boxes on top of the original boxes.

Switching to absolute scaling

In many cases having independent abundances will lead to the total abundances on one side being larger than on the other side. However by default the plotweb function will make both sides use the maximum available space.

If you are interested in the absolute relations between both sides you need to set the option scaling = "absolute". To demonstrate the difference we will generate uniformly random abundance values for the pollinator that are much larger than the actual values in the web.

n_higher_species <- ncol(Safariland)
higher_abundances <- sample(0:1000, n_higher_species, replace = TRUE)
names(higher_abundances) <- colnames(Safariland)
plotweb(Safariland, sorting = "ca", horizontal = TRUE,
        higher_abundances = higher_abundances, scaling = "relative")

plotweb(Safariland, sorting = "ca", horizontal = TRUE,
        higher_abundances = higher_abundances, scaling = "absolute")

In the same way as for independent abundances a plot with additional abundances is by default scaled in a relative manner. That means the boxes on both sides use as much space as possible. However,

plotweb(Safariland, sorting = "ca", horizontal = TRUE,
        add_lower_abundances = lower_abundances, scaling = "absolute")

Aesthetics

Now that we’ve covered the basics of plotting bipartite networks, let’s explore how to customize the plots to better match your desired aesthetics.

Cursive species labels

The options higher_italic and lower_italic make the higher trophic level and lower trophic level species labels respectively cursive.

plotweb(Safariland, sorting = "ca", horizontal = TRUE, curved_links = TRUE,
        higher_italic = TRUE, lower_italic = TRUE)

If you take a closer look at the labels on the left (the pollinators or higher trophic species), you will notice that not all of them are full species names. For instance, Phthiria is just a genus name. So, to make things a bit more interesting in the example below, we use the higher_labels option to italicize only those labels that contain two words separated by a space—usually the species names. Feel free to copy the code snippet below if you ever want to do the same.

higher_labels <- colnames(Safariland)
names(higher_labels) <- higher_labels
species_name_selector <- lengths(strsplit(higher_labels, " ")) == 2
higher_species_names <- higher_labels[species_name_selector]
higher_labels[higher_species_names] <- lapply(higher_species_names,
                                              function(x) bquote(italic(.(x))))

plotweb(Safariland, sorting = "ca", horizontal = TRUE, curved_links = TRUE,
        higher_labels = higher_labels, lower_italic = TRUE)

Coloring all species

With the arguments higher_color and lower_color the colors of the boxes can be changed. For example setting these to a single color value changes the color of all boxes on each side.

plotweb(Safariland, sorting = "ca", horizontal = TRUE, curved_links = TRUE, 
        higher_labels = higher_labels, lower_italic = TRUE,
        higher_color = "orange", lower_color = "darkgreen")

As you can see all boxes on the left (higher or columns) are orange and all boxes on the right (lower or rows) are dark green.

plotweb(Safariland, sorting = "ca", horizontal = TRUE, curved_links = TRUE,
        higher_labels = higher_labels, lower_italic = TRUE,
        lower_color = rainbow(nrow(Safariland)))

Coloring some species

However oftentimes we want to highlight just a few species. For the example below we are especially interested in the interaction of the plant species Alstroemeria aurea. So in the first step we generate a vector for all lower species and fill it with the value "black". In the next step we change the value only for the considered species to "orange".

lower_color <- rep("black", nrow(Safariland))
names(lower_color) <- rownames(Safariland)
lower_color["Alstroemeria aurea"] <- "orange"
plotweb(Safariland, sorting = "ca", horizontal = TRUE, curved_links = TRUE,
        higher_labels = higher_labels, lower_italic = TRUE,
        lower_color = lower_color)

As you can see now only the box of Alstroemeria aurea is orange.

Coloring additional abundances

Of course the color of the additional abundance boxes can be specified as well via the options higher_add_color and lower_add_color.

lower_add_color <- rep("black", nrow(Safariland))
lower_color <- rep("gray50", nrow(Safariland))
names(lower_add_color) <- rownames(Safariland)
names(lower_color) <- rownames(Safariland)
lower_add_color["Alstroemeria aurea"] <- "darkorange"
lower_color["Alstroemeria aurea"] <- "orange"
plotweb(Safariland, sorting = "ca", horizontal = TRUE, 
        add_lower_abundances = lower_abundances, curved_links = TRUE,
        higher_labels = higher_labels, higher_color = "grey50",
        lower_italic = TRUE, lower_color = lower_color,
        link_color = "lower", lower_add_color = lower_add_color)