LESSON 1 Module 22 Phase 3 — Advanced & Specialised ⏱ ~50 min 🔵 Intermediate

Why Visualisation Matters
in Bioinformatics

Understand why plots are not optional in genomics, learn the grammar of graphics, and build your first publication-ready chart with ggplot2.

1 · Why Visualisation Matters

🔬 The core idea

Bioinformatics produces enormous tables of numbers — thousands of gene expression values, millions of variant calls, billions of aligned reads. The human brain cannot process raw tables at that scale. A well-chosen plot can reveal a pattern in seconds that would take hours to find in a spreadsheet. Visualisation is not decoration — it is the primary tool for biological discovery and the main way reviewers and readers judge your results.

Consider a typical RNA-seq experiment on sorghum under drought stress. DESeq2 outputs a table of 30,000 genes with log₂ fold-change values and adjusted p-values. Without a volcano plot, you cannot see at a glance which genes are both statistically significant and biologically meaningful. Without a heatmap, you cannot see whether the top 50 DE genes cluster samples correctly. Without a PCA plot, you cannot check whether your replicates group together as expected.

Every stage of a bioinformatics workflow has a characteristic plot type that serves as a quality check:

Workflow stage Key plot What it checks
Raw reads QCPer-base quality score plotSequencing quality, adapter contamination
AlignmentCoverage track (genome browser)Read depth distribution, mapping artefacts
Exploratory DEPCA / sample distance heatmapBatch effects, outliers, replicate consistency
Differential expressionVolcano plot, MA plotFold-change vs significance landscape
Gene setsHeatmap with clusteringExpression patterns across conditions
GWASManhattan plot, QQ plotGenomic inflation, significant loci
scRNA-seqUMAP / t-SNECell type clustering, trajectory
💡 A good plot does three things at once: it communicates your finding, validates that your analysis ran correctly, and generates new hypotheses. A bad plot does none of those — it just fills space in a paper.

2 · Plot Types in Bioinformatics

Before writing any code, you should be able to look at a biological question and immediately know which plot type answers it. This is a skill most people never learn explicitly — they just imitate what they have seen in papers. This module will make it deliberate.

Plot type Best for Primary R tool
Scatter plotTwo continuous variables, correlationggplot2 + geom_point()
Line plotTime series, ordered sequencesggplot2 + geom_line()
Bar / column chartComparing counts or means across groupsggplot2 + geom_col()
BoxplotDistribution summary, outlier detectionggplot2 + geom_boxplot()
Violin plotFull distribution shape by groupggplot2 + geom_violin()
HeatmapMatrix data — expression, correlationpheatmap, ComplexHeatmap
Volcano plotDE results: significance vs fold-changeEnhancedVolcano, ggplot2
MA plotDE results: mean vs fold-change (bias check)DESeq2::plotMA()
Manhattan plotGWAS: chromosomal position vs –log₁₀(p)qqman, CMplot
QQ plotGWAS: expected vs observed p-valuesqqman
PCA plotSample clustering, batch effectsggplot2 + prcomp()
UMAP / t-SNEscRNA-seq cell embeddingSeurat, Scanpy
Genome trackRead coverage, gene models, variantsGviz, ggbio
Network plotProtein interactions, pathway connectionsigraph, ggraph
⚠️ Choosing the wrong plot type is one of the most common mistakes in bioinformatics papers. Using a bar chart when you should use a boxplot hides the distribution. Using a pie chart for any comparison (almost always wrong) makes magnitudes hard to judge. This module covers when and why to use each type.

3 · The Grammar of Graphics

🧠 Why this concept matters

In the 1990s, statistician Leland Wilkinson wrote a book called The Grammar of Graphics. It argued that every statistical chart — no matter how complex — is built from the same small set of components, combined in different ways. Hadley Wickham implemented this idea as ggplot2 in 2005, and it became the most widely used scientific plotting library in the world. Once you understand the grammar, you stop memorising individual plot recipes and start composing any plot you can imagine.

The grammar has seven layers. Every ggplot2 call is just a combination of these:

Layer ggplot2 component Example
1. Dataggplot(data = ...)Your data frame
2. Aestheticsaes(x = , y = , colour = )Which columns map to which visual properties
3. Geometrygeom_point(), geom_line()The shape used to draw the data
4. Statisticsstat_smooth(), stat_summary()Transformations applied before drawing
5. Scalesscale_colour_manual()How data values map to visual values
6. Coordinatescoord_flip(), coord_polar()The coordinate system
7. Facetsfacet_wrap(), facet_grid()Split into multiple panels by a variable

You do not need all seven layers in every plot. The minimum is Data + Aesthetics + Geometry. Every other layer is optional and added with +. This is why ggplot2 code looks like stacking building blocks:

R — ggplot2 structure
# The minimum viable ggplot2 call
ggplot(data = my_data,                # Layer 1: data
       aes(x = gene_expression,           # Layer 2: aesthetics
           y = fold_change,
           colour = condition)) +
  geom_point()                          # Layer 3: geometry

# Add more layers with +
ggplot(data = my_data,
       aes(x = gene_expression, y = fold_change, colour = condition)) +
  geom_point(size = 2, alpha = 0.7) +  # geometry
  geom_smooth(method = "lm") +           # statistics
  scale_colour_manual(                   # scale
    values = c("drought" = "#c0392b",
               "control" = "#2980b9")) +
  facet_wrap(~chromosome) +              # facet
  theme_classic()                        # theme (bonus layer)
💡 The + operator in ggplot2 works like the pipe (|>) in data wrangling — it passes the plot object to the next layer. The difference is that + adds a layer rather than transforming data. Always put the + at the end of a line, never at the start of the next line, or R will throw an error.

4 · ggplot2 Layers in Depth

Let's go through each layer with bioinformatics examples so you know what each one does before you start combining them.

Layer 1 — Data

ggplot2 expects a tidy data frame: one row per observation, one column per variable. If your RNA-seq results are in a matrix (genes as rows, samples as columns), you need to convert them to tidy format first using pivot_longer() from the tidyr package.

R — tidy data for ggplot2
# Wide format (NOT what ggplot2 wants)
#   gene     sample1  sample2  sample3
#   Sobic1   12.4     14.1     11.8
#   Sobic2   3.2       2.8      4.1

# Tidy format (what ggplot2 wants)
#   gene     sample    count
#   Sobic1   sample1   12.4
#   Sobic1   sample2   14.1
#   Sobic1   sample3   11.8

library(tidyr)

tidy_counts <- wide_matrix |>
  as.data.frame() |>
  tibble::rownames_to_column("gene") |>
  pivot_longer(
    cols      = starts_with("sample"),
    names_to  = "sample",
    values_to = "count"
  )

Layer 2 — Aesthetics (aes)

Aesthetics are the mappings between columns in your data frame and visual properties of the plot. The most common aesthetics are:

AestheticControlsBioinformatics use
x, yPosition on axeslog₂FC vs –log₁₀(padj) in volcano plot
colourOutline / line colourUp-regulated vs down-regulated genes
fillInterior fill colourSample groups in boxplot
sizePoint or line sizeExpression level as bubble size
alphaTransparency (0–1)Reducing overplotting in large datasets
shapePoint shape (1–25)Different tissues or conditions
labelText labelGene names on significant points

🔑 aes() vs direct argument

Put a property inside aes() when it maps to a data column — it will vary per point. Put it outside aes() when you want a fixed value for all points. For example: geom_point(aes(colour = condition)) colours by the condition column, but geom_point(colour = "red") makes everything red. This is the single most common source of confusion for ggplot2 beginners.

Layer 3 — Geometry (geom_*)

The geometry determines the visual shape drawn for each observation. Here are the most important geoms for bioinformatics:

R — essential geoms
# Points — scatter, volcano, PCA
geom_point(size = 2, alpha = 0.6)

# Lines — time series, expression trends
geom_line(linewidth = 1)

# Horizontal/vertical reference lines
geom_hline(yintercept = 0, linetype = "dashed", colour = "grey50")
geom_vline(xintercept = c(-1, 1), linetype = "dashed", colour = "grey50")

# Boxplot — distribution by group
geom_boxplot(outlier.size = 1, fill = "white")

# Violin — full distribution shape
geom_violin(trim = FALSE)

# Bars — counts, means
geom_col()      # uses actual y values
geom_bar()      # counts rows (stat = "count")

# Text labels — gene names
geom_text(aes(label = gene_name), size = 3, vjust = -1)
geom_label(aes(label = gene_name))   # boxed label

# Smooth regression line
geom_smooth(method = "lm", se = FALSE)

5 · Your First Plot: Sorghum Gene Expression

📋 What we will build

We will create a simulated sorghum RNA-seq dataset with gene expression values under two conditions (drought stress vs control), then build a scatter plot that shows the relationship between the two conditions. This is a common first sanity check — genes with similar expression in both conditions should cluster around the diagonal.

Step 1 — Install and load packages

Run from: RStudio or any R session
R — setup
# Install once (skip if already installed)
install.packages(c("ggplot2", "dplyr", "tidyr", "patchwork"))

# Load every session
library(ggplot2)
library(dplyr)
library(tidyr)

Step 2 — Create simulated sorghum expression data

Run from: ~/bioinformatics-visualisation/
R — simulate data
set.seed(42)                         # reproducible random numbers

n_genes <- 500

sorghum_expr <- data.frame(
  gene_id  = paste0("Sobic.", sprintf("%04d", 1:n_genes)),
  control  = rnorm(n_genes, mean = 8, sd = 2),  # log2 CPM values
  drought  = rnorm(n_genes, mean = 8, sd = 2)
)

# Add a fold-change column (drought vs control)
sorghum_expr <- sorghum_expr |>
  mutate(
    log2FC     = drought - control,
    is_de      = abs(log2FC) > 1,     # simple DE flag
    direction  = case_when(
      log2FC >  1  ~ "Up in drought",
      log2FC < -1  ~ "Down in drought",
      TRUE         ~ "Not DE"
    )
  )

head(sorghum_expr)

Step 3 — Build the scatter plot

Run from: ~/bioinformatics-visualisation/
R — first ggplot2 scatter
# Build the plot layer by layer
p1 <- ggplot(
    data = sorghum_expr,
    aes(x     = control,       # x axis: control expression
        y     = drought,        # y axis: drought expression
        colour = direction)     # colour by DE status
  ) +
  geom_point(size = 1.8, alpha = 0.7) +            # draw points
  geom_abline(slope = 1, intercept = 0,             # diagonal = no change
              linetype = "dashed", colour = "grey40",
              linewidth = 0.7) +
  scale_colour_manual(                              # custom colours
    values = c(
      "Up in drought"   = "#c0392b",
      "Down in drought" = "#2980b9",
      "Not DE"          = "#bdc3c7"
    )
  ) +
  labs(
    title    = "Sorghum gene expression: drought vs control",
    subtitle = "500 simulated genes · values in log2 CPM",
    x        = "Control (log2 CPM)",
    y        = "Drought (log2 CPM)",
    colour   = "Expression change",
    caption  = "Points above diagonal: higher in drought"
  ) +
  theme_classic(base_size = 13) +                  # clean theme
  theme(
    plot.title    = element_text(face = "bold"),
    legend.position = "top"
  )

# Display the plot
p1

# Save to file (publication quality)
ggsave("plots/sorghum_scatter.png",
       plot   = p1,
       width  = 7,
       height = 6,
       dpi    = 300)
💡 Always save with dpi = 300 for print-quality output. Journals typically require 300–600 DPI for raster figures. For vector output (scalable to any size), use ggsave("plot.pdf") — PDF is always vector by default in ggplot2.

6 · Mapping Aesthetics to Data

Let's extend the plot to encode a third variable (gene length) as point size and a fourth variable (chromosome) as facets. This demonstrates how ggplot2 allows you to visualise four dimensions in a single 2D plot.

Run from: ~/bioinformatics-visualisation/
R — multi-aesthetic plot
# Add chromosome and gene length to simulated data
sorghum_expr <- sorghum_expr |>
  mutate(
    chromosome  = paste0("Chr", sample(1:10, n_genes, replace = TRUE)),
    gene_length = runif(n_genes, 500, 5000)    # bp
  )

# Select just chr 1-4 for a cleaner faceted plot
sub_data <- sorghum_expr |>
  filter(chromosome %in% paste0("Chr", 1:4))

p2 <- ggplot(sub_data,
       aes(x      = control,
           y      = drought,
           colour = direction,
           size   = gene_length)) +    # 4th aesthetic
  geom_point(alpha = 0.6) +
  geom_abline(slope = 1, intercept = 0,
              linetype = "dashed", colour = "grey50", linewidth = 0.5) +
  facet_wrap(~chromosome, nrow = 2) +               # split by chromosome
  scale_colour_manual(
    values = c("Up in drought"   = "#c0392b",
               "Down in drought" = "#2980b9",
               "Not DE"          = "#bdc3c7")) +
  scale_size_continuous(range = c(0.5, 4),
                        name  = "Gene length (bp)") +
  labs(title  = "Expression by chromosome — Sorghum drought vs control",
       x      = "Control (log2 CPM)",
       y      = "Drought (log2 CPM)",
       colour = "DE status") +
  theme_classic(base_size = 12) +
  theme(strip.background = element_rect(fill = "#e8f5ee"),
        strip.text        = element_text(face = "bold"),
        legend.position   = "bottom")

p2
ggsave("plots/sorghum_facet_scatter.png", p2,
       width = 10, height = 7, dpi = 300)

7 · Choosing Geoms: The Decision Framework

When you face a new plotting task, ask these three questions in order:

Question Answer → geom
How many variables am I visualising? 1 continuous → histogram/density; 1 categorical → bar; 2 continuous → scatter; 1 continuous + 1 categorical → boxplot/violin
What is my message? Comparison → bar/boxplot; Relationship → scatter; Distribution → histogram/violin; Change over time → line; Part of whole → bar (NOT pie)
How many data points? < 100: show all points; 100–10,000: semi-transparent points + summary; > 10,000: density plot or hexbin

🌱 Sorghum example

In a sorghum drought experiment with 3 replicates per condition (6 samples total), you want to compare library sizes. With only 6 points, a bar chart works fine — show each bar as one sample. If you had 60 samples across 10 conditions, switch to a boxplot per condition. If you had 600 samples from a meta-analysis, use a violin plot to show the full distribution.

Here is a practical comparison of the three distribution geoms:

R — distribution geom comparison
library(patchwork)   # for combining multiple plots

# Simulate library sizes for sorghum samples
lib_data <- data.frame(
  sample    = paste0("S", 1:30),
  condition = rep(c("Control", "Drought"), each = 15),
  lib_size  = c(rnorm(15, 20e6, 3e6),
                rnorm(15, 18e6, 4e6))
)

# Plot A: boxplot
pA <- ggplot(lib_data, aes(x = condition, y = lib_size / 1e6,
                               fill = condition)) +
  geom_boxplot(alpha = 0.7) +
  labs(title = "A: Boxplot", y = "Library size (M reads)", x = NULL) +
  scale_fill_manual(values = c("#2980b9", "#c0392b")) +
  theme_classic() + theme(legend.position = "none")

# Plot B: violin
pB <- ggplot(lib_data, aes(x = condition, y = lib_size / 1e6,
                               fill = condition)) +
  geom_violin(trim = FALSE, alpha = 0.7) +
  geom_boxplot(width = 0.1, fill = "white") +   # inner boxplot
  labs(title = "B: Violin + boxplot", y = NULL, x = NULL) +
  scale_fill_manual(values = c("#2980b9", "#c0392b")) +
  theme_classic() + theme(legend.position = "none")

# Plot C: jitter (best for small n)
pC <- ggplot(lib_data, aes(x = condition, y = lib_size / 1e6,
                               colour = condition)) +
  geom_jitter(width = 0.15, size = 3, alpha = 0.8) +
  stat_summary(fun = mean, geom = "crossbar",
               width = 0.4, colour = "black") +
  labs(title = "C: Jitter + mean bar", y = NULL, x = NULL) +
  scale_colour_manual(values = c("#2980b9", "#c0392b")) +
  theme_classic() + theme(legend.position = "none")

# Combine with patchwork
pA + pB + pC + plot_layout(ncol = 3)
ggsave("plots/distribution_comparison.png",
       width = 12, height = 5, dpi = 300)
💡 patchwork is the easiest way to combine multiple ggplot2 plots into a multi-panel figure for publication. Use + to place plots side by side, / to stack them vertically, and plot_annotation(tag_levels = "A") to add A, B, C panel labels automatically.
Advertisement AdSense in-feed slot reserved

8 · Exercises

1
Identify the correct geom

You have RNA-seq data from 4 sorghum tissues (root, leaf, seed, stem) with 20 samples per tissue. You want to compare the distribution of total read counts per sample across tissues. Which geom is most appropriate — bar chart, boxplot, violin, or scatter? Why?

Show answer
Boxplot or violin plot is most appropriate here. A bar chart would only show one value per tissue (e.g. the mean), hiding the spread. With 20 samples per group, a boxplot shows median, IQR, and outliers clearly. A violin plot would also work and shows the full distribution shape — useful if you suspect bimodality. A scatter/jitter plot would work for 20 points but gets cluttered. The violin + inner boxplot combination (Option B from the lesson code) is often the best choice for n = 20.
2
Fix the aes() mistake

This code is supposed to colour all points red, but instead it creates a legend with one category called "red". Find and fix the bug:

R — buggy code
ggplot(sorghum_expr, aes(x = control, y = drought,
                              colour = "red")) +
  geom_point()
Show answer
The fix is to move colour = "red" outside of aes() and into geom_point() directly. Inside aes(), R treats the string "red" as a column name / data value — it maps the constant "red" to all points and assigns a colour from the default palette. The corrected code:

ggplot(sorghum_expr, aes(x = control, y = drought)) +
geom_point(colour = "red")
3
Add a layer to the scatter plot

Take the p1 scatter plot from Section 5 and add a linear regression line for each DE direction group (Up, Down, Not DE) separately. The line should be drawn without a confidence interval ribbon.

Show answer
p1 + geom_smooth(method = "lm", se = FALSE, linewidth = 0.8)

Because colour = direction is set in the global aes(), geom_smooth() automatically fits a separate line per group (colour group). Setting se = FALSE removes the confidence interval ribbon. linewidth = 0.8 makes the line visible without dominating the points.
4
Create your own project folder

Create the directory structure for the bioinformatics-visualisation repo, then run the simulated data code and save both plots to a plots/ subfolder. Verify the files were created.

Show answer
In the terminal:
mkdir -p ~/bioinformatics-visualisation/plots
cd ~/bioinformatics-visualisation
Rscript lesson-01-visualisation-intro.R
ls -lh plots/
You should see sorghum_scatter.png, sorghum_facet_scatter.png, and distribution_comparison.png in the plots/ directory.
Advertisement AdSense rectangle slot reserved