5  Poster design

5.1 Introduction

In other situations, we may be more creative with our table design. A well-done table can be as catchy as a figure and we can use it to get attention of our readers. In the course of this tutorial, we can think for example of a table for a scientific poster. The vegetation data contains details about countries where the δ13C measurements were done - highlighting the country’s positions in continent shapes should be the eye catcher element of our table.

While working on Table 3.1, we realized that there was a different number of measurements in different families. In this section, we will group our data to even smaller numbers of groups and we do not want to loose the detail about the number of underlying data points. We will use ggplot_image() to include a scatter plot (combined with half-violin plots to visualize the distribution of measured values even better).

Besides combining table layout with {ggplot2} figures, we will pay attention to colours used in the various components of the table. In a real project, it may be for example organisation colours. Here, we will use a few colours from the sPBIYlGn palette from the {Redmonder} package.

We will start be loading the {rnaturalearth} package which - among many other details about countries - contains assignment of countries to continents and polygon shapes for countries’ borders.

library(rnaturalearth)

5.2 Pre-processing

Next, we will proceed with some pre-processing:

  1. extract labels (used as plot titles) of countries of interest (those with measurement data) as a named vector - the names are critical to match country names from dat_vegetation (and data frames derived from it) with country polygons from {rnaturalearth} (here, loaded as world_data)
  2. obtain countries data
  3. and subset them only to countries from the vegetation data set
# 1. Store country labels as a named vector
country_labels <- sort(unique(dat_vegetation$country_label))
names(country_labels) <- sort(unique(dat_vegetation$country))

# 2. Get countries data
world_data <- ne_countries(scale = "medium", returnclass = "sf")
# glimpse(world_data)

# 3. Subset countries data only for countries of interest

# Countries in the data
vegetation_countries <- unique(dat_vegetation$country)
vegetation_continents <- world_data[world_data$name_en %in% vegetation_countries, ]$continent

# Get their continents
vegetation_sf_continents <- world_data[world_data$continent %in% vegetation_continents, ]

# Subset the sf world data for countries of interest
vegetation_sf_countries <- world_data[world_data$name_en %in% vegetation_countries, ]
  1. define a custom font using {showtext} package
library(showtext)

# Font of choice
font_add_google(name = "Open Sans", family = "open sans")
# showtext_auto is needed to display the font
showtext_auto()
Custom font for table and embeded figures

It is a good idea to define the custom font before proceeding with generation of the tables and embedded figures. This will allow us to reference to it when we create these building blocks.

5.3 Including {ggplot2} plots in a gt table

We will actually produce two figures for each country and plant type (either C3 or c 4) and as a next step, we will therefore prepare the code for both figures. Because we will use the {gt}’s text_transform() function to embed ggplot2 figures into a table, we will wrap the plotting code into functions.

Country plots

This function takes a country name is argument and uses it to generate a map based on the vegetation_sf_continents object prepared earlier (it basically needs the country and continent polygons).

# Functions inputs are:
# country - character vector with name of the country to be highlighted

# Function relies on:
# vegetation_sf_countries - a combined sf + data.frame object with country coordinates and details; based on data from the {rnaturalearth} package

PlotCountry <- function(target_country) {
  # Get coordinates of the continent where target country is located
  target_continent <- vegetation_sf_countries[vegetation_sf_countries$name_en == target_country, ]$continent
  # Country label will be used a plot's title
  country_label <- country_labels[names(country_labels) == target_country]

  p_cont <- ggplot() +
    # entire continent
    geom_sf(data = vegetation_sf_continents[vegetation_sf_continents$continent == target_continent, ], fill = "white") +
    # highlight target country
    geom_sf(data = vegetation_sf_countries[vegetation_sf_countries$name_en == target_country, ], fill = "#1F6E5EFF") +
    labs(title = country_label) +
    # removes all grids, axes, etc.
    theme_void() +
    theme(plot.margin = margin(0, 0, 0, 0),
          plot.title = element_text(size = 32, hjust = 0.5),
          text = element_text(family = "open sans"))
  
  return(p_cont)
}

Let’s give the function try with Kenya as an example:

PlotCountry("Kenya")

Observations and density

The second figure should summarize observations for each country and type of plant. We will include all measurements as individual data points to give an impression about the size of each group. We will use a devoted geom_jitter() to give a little bit of random variation to the points, which is solely for visual purposes. (Note: geom_point(position = "jitter") would do the job as well.)

We will combine the scatter plot with half violin plot for an impression about distribution of the measure values. We will include y-axis to help our readers with interpretation of the plots. Nonetheless, we want to ensure that all figures will share the same axis limits to avoid any false interpretations.

### y-axis limits
# "nice" number are preferred and thus the use of modulo
# keep in mind that the measure values are negative!
y_min <- (round(min(dat_vegetation$delta) %% 10, 0) * 10) * -1
y_max <- (round(max(dat_vegetation$delta) %% 10, 0) * 10) * -1

### Function
PlotHalfViolins <- function(data_to_plot) {
  p_halfv <- ggplot(data_to_plot, aes(x = country, y = delta)) +
    geom_violinhalf(colour = "#005C55FF", fill = "#005C55FF", position = position_nudge(x = 0.15, y = 0)) +
    geom_jitter(aes(fill = type), colour = "#005C55FF", alpha = 0.8, width = 0.05) +
    scale_y_continuous(breaks = c(-10, -20, -30, -40)) +
    # set limits of the y axis
    coord_cartesian(ylim = c(y_min, y_max)) +
    theme_classic() +
    theme(
      text = element_text(family = "open sans"),
      plot.margin = margin(0, 0, 0, 0),
      plot.background = element_blank(),
      panel.background = element_blank(),
      axis.line.x = element_blank(),
      axis.line.y = element_line(colour = "black", linewidth = 1),
      axis.ticks.y = element_line(colour = "black", linewidth = 1),
      axis.ticks.x = element_blank(),
      axis.text.x = element_blank(),
      axis.text.y = element_text(colour = "black", size = 16),
      axis.title = element_blank(),
      aspect.ratio = 2,
      legend.position = "none"
    )

  return(p_halfv)
}

Again, let’s see how the output of this function looks like:

PlotHalfViolins(dat_vegetation[dat_vegetation$country == "Kenya", ])

5.4 Putting it all together

Let’s start with a very basic table - with all required building blocks but not much of customized design.

First of all, we will create a little summary table. It will be the basic scaffold to add the {ggplot2} components to. We will also calculate mean δ13C for each group of plants by their type AND country.

summarized_tbl_input <- dat_vegetation |>
  group_by(country, type) |>
  summarize(mean_d = mean(delta)) |>
  arrange(country)

Next, we will replace the country names with respective plots. We will use the ggplot_image() function in combination with text_transform() to replace a text (here, country name) with a ggplot2 graphic created with our custom function.

summarized_tbl_input |> 
  gt() |>
  # country maps
  text_transform(
    locations = cells_row_groups(),
    fn = function(x) {
      lapply(x, function(y) {
        html(PlotCountry(y) |> ggplot_image(height = px(150), aspect_ratio = 1))
      })
    }
  )
Table 5.1
type mean_d
C<sub>3</sub> -27.61667
C<sub>3</sub> -34.11429
C<sub>3</sub> -27.36429
C<sub>4</sub> -12.10545
C<sub>3</sub> -25.28333

text_transform() has done its job but the resulting table is not visually quite appealing. As we will work on improving it, we will first of all handle the “row groups”. For most of the countries, there is only a single group of observations and we included country names in plot titles already. We can move row group labels to columns for a much nicer table.

Luckily, the gt() has a process_md argument which makes handling of subscript a super easy task:

summarized_tbl_input |> 
  gt(
    rowname_col = "type",
    row_group_as_column = T,
    process_md = T
  ) |> 
  # country maps
  text_transform(
    locations = cells_row_groups(),
    fn = function(x) {
      lapply(x, function(y) {
        html(PlotCountry(y) |> ggplot_image(height = px(150), aspect_ratio = 1))
      })
    }
  ) |> 
  tab_options(table.width = px(800))
Table 5.2
mean_d
C3 -27.61667
C3 -34.11429
C3 -27.36429
C4 -12.10545
C3 -25.28333

Now, it’s time to include the other figure, a combined jitter and half violin plot. Again, we can use text_transform() alongside with ggplot_image(). However, we need to “provide a new column” to place the graphic into. One option is the mutate() function from {dplyr} package. We can apply it just before passing our summarized_tbl_input into the gt() function without any need to really modify this input data.

tbl_with_plots <- summarized_tbl_input |> 
  # new column for violin + scatter plot assembly
  mutate(violin_plot = "violin") |> 
  gt(
    rowname_col = "type",
    row_group_as_column = T,
    process_md = T
  ) |>
  # country maps
  text_transform(
    locations = cells_row_groups(),
    fn = function(x) {
      lapply(x, function(y) {
        html(PlotCountry(y) |> ggplot_image(height = px(150), aspect_ratio = 1))
      })
    }
  ) |>
  # half violin + dot plots
  text_transform(
    locations = cells_body(columns = "violin_plot"),
    fn = function(x) {
      # Use `map2` to iterate over the column and the row index simultaneously
      map2(x, 1:length(x), function(col_value, i) {
        # Get the row-specific country and type from the summarized data
        target_country <- summarized_tbl_input$country[i]
        target_type <- summarized_tbl_input$type[i]

        # Filter the original data and create the plot
        data_to_plot <- dat_vegetation |>
          filter(country == !!target_country, type == !!target_type)

        PlotHalfViolins(data_to_plot) |>
          ggplot_image(height = px(200)) |>
          html()
      })
    }
  )
Table 5.3
mean_d violin_plot
C3 -27.61667
C3 -34.11429
C3 -27.36429
C4 -12.10545
C3 -25.28333

With that, our table’s content is complete and we can try to improve it’s overall design. There is a lot to do: nicer column labels (cols_label()) and formatting of numerical values (fmt_number()), alignment of column headers (cols_align()) and cell content (tab_style() targeting cell_text()). We also make sure that the table text and plot title use the same font family - to apply a selected font to an entire table, we will use the opt_table_font() function.

tbl_with_plots |> 
  fmt_number(columns = "mean_d", decimals = 2) |>
  tab_spanner(columns = contains(c("mean", "violin")), label = md("&delta;^13^C")) |>
  cols_label(
    type = "Type",
    mean_d = "Mean",
    violin_plot = "Measurements"
  ) |>
  cols_align(align = "center", columns = everything()) |> 
  tab_style(
    style = list(
      cell_text(align = "center")
    ),
    locations = cells_body(columns = everything())
  ) |> 
  opt_table_font(font = list(google_font(name = "Open Sans"))) |> 
  tab_options(table.width = px(800))
Table 5.4
δ13C
Mean Measurements
C3 −27.62
C3 −34.11
C3 −27.36
C4 −12.11
C3 −25.28

5.5 Advanced modifications with CSS

This is not too bad - but what about setting a nice background colour and adjusting cell borders accordingly? With predefined arguments of tab_options() and a little bit of CSS, we can do a lot with relatively little of a code. Even a gradient background! Albert Rapp has done a great job in explaining the CSS part in his tutorial and I will thus refer you to this source. Here, I will only highlight the importance of setting an id to our gt table like this:

summarized_tbl_input |> 
  gt(
    id = "countries_tbl")

With a valid ID, we can later refer to a particular table in our opt_css() call.

tbl_with_plots_gradient <- summarized_tbl_input |> 
  mutate(violin_plot = "Violin") |> 
  gt(
    id = "countries_tbl",
    rowname_col = "type",
    groupname_col = "country",
    row_group_as_column = T,
    process_md = T
  ) |> 
  # country maps
  text_transform(
    locations = cells_row_groups(),
    fn = function(x) {
      lapply(x, function(y) {
        html(PlotCountry(y) |> ggplot_image(height = px(150), aspect_ratio = 1))
      })
    }
  ) |> 
  # half violin + dot plots
  text_transform(
    locations = cells_body(columns = "violin_plot"),
    fn = function(x) {
      # Use `map2` to iterate over the column and the row index simultaneously
      map2(x, 1:length(x), function(col_value, i) {
        # Get the row-specific country and type from the summarized data
        target_country <- summarized_tbl_input$country[i]
        target_type <- summarized_tbl_input$type[i]

        # Filter the original data and create the plot
        data_to_plot <- dat_vegetation |>
          filter(country == !!target_country, type == !!target_type)

        PlotHalfViolins(data_to_plot) |>
          ggplot_image(height = px(200)) |>
          html()
      })
    }
  ) |> 
  fmt_number(columns = "mean_d", decimals = 2) |>
  tab_spanner(columns = contains(c("mean", "violin")), label = md("&delta;^13^C")) |>
  cols_label(
    type = "Type",
    mean_d = "Mean",
    violin_plot = "Measurements"
  ) |>
  cols_align(align = "center", columns = everything()) |> 
  tab_style(
    style = list(
      cell_text(align = "center")
    ),
    locations = cells_body(columns = everything())
  ) |> 
  opt_table_font(font = list(google_font(name = "Open Sans"))) |> 
  tab_options(
    table.width = px(600),
    table.font.names = "open sans",
    heading.align = "center",
    column_labels.border.top.color = "white",
    column_labels.border.bottom.color = "white",
    heading.border.bottom.color = "white",
    table_body.border.top.color = "white",
    table_body.border.bottom.color = "white",
    table_body.hlines.color = "white",
    row_group.border.top.color = "white",
    row_group.border.bottom.color = "white",
    footnotes.border.bottom.color = "white"
  ) |>
  cols_width(
    type ~ px(100),
    mean_d ~ px(100)
  ) |> 
  opt_css(css ="
    /*Background for the entire table*/
    #countries_tbl .gt_table {
      background: linear-gradient(180deg, #7DA37BFF, #DBD797);
    }
    
    /*All transparency control for the plots*/
    #countries_tbl .gt_col_heading, #countries_tbl .gt_column_spanner_outer, #countries_tbl .gt_row.gt_center.gt_stub, #countries_tbl .gt_row.gt_center.gt_stub_row_group, #countries_tbl .gt_row.gt_left.gt_stub_row_group {
      background: transparent; border-right-style: none; vertical-align: middle;
    }")
tbl_with_plots_gradient |> 
  tab_options(table.width = px(800))
Table 5.5
δ13C
Mean Measurements
C3 −27.62
C3 −34.11
C3 −27.36
C4 −12.11
C3 −25.28

In case we would use the table as a stand-alone graphic, it is a good idea to include the data source and a little bit of detail about the data. We can do so with tab_footnote() (our predefined footnote label will come handy here) and tab_header.

tbl_with_plots_gradient |> 
  tab_header(md("&delta;^13^C is a ratio between ^12^C and ^13^C isotopes; photosynthetically more efficient plant species show less negative values")) |> 
  tab_footnote(md(folio_footnote)) |> 
  tab_style(style = cell_text(align = "left"), locations = cells_footnotes()) |> 
  tab_options(heading.title.font.size = 12,
              footnotes.font.size = 12,
              table.width = px(800))
Table 5.6
δ13C is a ratio between 12C and 13C isotopes; photosynthetically more efficient plant species show less negative values
δ13C
Mean Measurements
C3 −27.62
C3 −34.11
C3 −27.36
C4 −12.11
C3 −25.28
Data source: vegetation data from the {folio} package