Lab 12: Data Visualization — Histograms, Bar Charts, Box Plots & Scatter Plots

BI 255 · Bethune-Cookman University · Fall 2026

Author

Dr. Rosie Stanbrook-Buyer

Published

September 21, 2026


Seeing Data Clearly

A well-constructed graph communicates in seconds what a table of numbers takes minutes to reveal. In biology, the ability to visualise data is not a cosmetic skill — it is fundamental to understanding your results, spotting errors, and communicating findings to others.

This lab covers the four workhorses of scientific data visualisation using R’s built-in graphics system: histograms for distributions, bar charts for categorical summaries, box plots for group comparisons, and scatter plots for relationships between two variables.

NoteLearning Objectives

By the end of this lab you will be able to:

  1. Choose the appropriate plot type for a given data structure
  2. Create histograms with hist() and adjust bin width
  3. Create bar charts with barplot() for categorical data
  4. Create box plots with boxplot() to compare groups
  5. Create scatter plots with plot() to explore relationships
  6. Customise plots: axes, colours, labels, titles, and legends
  7. Place multiple plots on one figure with par(mfrow = ...)

Part 1: Choosing the Right Plot

Before writing any code, the most important question is: what kind of data do I have?

ImportantPlot Type Decision Guide
Question Data type Best plot
What does the distribution of one variable look like? Continuous Histogram
How do counts or totals compare across categories? Categorical Bar chart
How do distributions compare across groups? Continuous + categorical Box plot
Is there a relationship between two continuous variables? Two continuous Scatter plot

The wrong plot type does not just look bad — it can actively mislead.

We will use three biological datasets throughout this lab. Let us create them now:

set.seed(42)

# Dataset 1: Enzyme activity (nmol/min/mg protein) across 4 temperature treatments
enzyme <- data.frame(
  temperature = rep(c("25C", "30C", "35C", "40C"), each = 20),
  activity    = c(rnorm(20, 18, 3),
                  rnorm(20, 26, 4),
                  rnorm(20, 31, 5),
                  rnorm(20, 22, 4))
)

# Dataset 2: Photosynthesis rate (umol CO2/m2/s) across 50 plant measurements
photo_rate <- c(rnorm(50, mean = 12.5, sd = 3.2))

# Dataset 3: Protein concentration vs absorbance (Bradford assay standard curve)
protein_conc <- seq(0, 2000, by = 200)     # ug/mL
absorbance   <- 0.00035 * protein_conc + rnorm(length(protein_conc), 0, 0.015)

Part 2: Histograms — Visualising Distributions

A histogram divides a continuous variable into bins and counts observations in each bin. It shows the shape of a distribution.

# Basic histogram of photosynthesis rates
hist(photo_rate,
     xlab  = "Photosynthesis Rate (umol CO2/m2/s)",
     ylab  = "Frequency",
     main  = "Distribution of Photosynthesis Rates\n50 Plant Measurements",
     col   = "#2C5F8A",
     border = "white")

Adjusting Bin Width

The number of bins dramatically affects what you see:

# Compare different bin counts using par(mfrow)
par(mfrow = c(1, 3))   # 1 row, 3 columns of plots

hist(photo_rate, breaks = 5,  col = "#A8C4D9", border = "white",
     main = "5 bins",  xlab = "Rate")
hist(photo_rate, breaks = 15, col = "#2C5F8A", border = "white",
     main = "15 bins", xlab = "Rate")
hist(photo_rate, breaks = 30, col = "#1A3D5C", border = "white",
     main = "30 bins", xlab = "Rate")

par(mfrow = c(1, 1))   # Reset to single plot
TipHow Many Bins?

Too few bins hide the shape; too many create noise. A good rule of thumb: √n bins, where n is your sample size. For n = 50, try ~7–8 bins.

R’s default (breaks not specified) usually makes a reasonable choice, but always inspect it.

Adding a Normal Curve

# Histogram with a superimposed normal distribution curve
hist(photo_rate,
     freq   = FALSE,      # Switch y-axis to density (required for overlaying a curve)
     col    = "#A8C4D9",
     border = "white",
     xlab   = "Photosynthesis Rate (umol CO2/m2/s)",
     main   = "Photosynthesis Rate with Normal Curve")

# Overlay normal curve using estimated mean and SD
curve(dnorm(x, mean = mean(photo_rate), sd = sd(photo_rate)),
      add = TRUE, col = "#C8102E", lwd = 2)


Part 3: Bar Charts — Comparing Category Totals

Bar charts show a summary statistic (count, mean, total) for each category. They should not be used for raw continuous data — that is what box plots are for.

# Mean enzyme activity at each temperature
mean_activity <- tapply(enzyme$activity, enzyme$temperature, mean)
mean_activity
     25C      30C      35C      40C 
18.57576 24.91603 30.99545 22.64240 
# Bar chart of mean enzyme activity
barplot(mean_activity,
        col    = c("#A8C4D9", "#5B8DB8", "#2C5F8A", "#1A3D5C"),
        ylab   = "Mean Enzyme Activity (nmol/min/mg protein)",
        xlab   = "Temperature",
        main   = "Mean Enzyme Activity at Four Temperatures",
        ylim   = c(0, 40))

Adding Error Bars

A bar chart without error bars shows only the mean and conceals variability. Add standard error bars:

# Calculate means and standard errors
means <- tapply(enzyme$activity, enzyme$temperature, mean)
sds   <- tapply(enzyme$activity, enzyme$temperature, sd)
ns    <- tapply(enzyme$activity, enzyme$temperature, length)
se    <- sds / sqrt(ns)

# Draw the bar chart and capture bar midpoint positions
bp <- barplot(means,
              col    = c("#A8C4D9", "#5B8DB8", "#2C5F8A", "#1A3D5C"),
              ylab   = "Mean Enzyme Activity (nmol/min/mg protein)",
              xlab   = "Temperature",
              main   = "Mean Enzyme Activity ± SE",
              ylim   = c(0, 42))

# Add error bars using arrows()
arrows(bp, means - se, bp, means + se,
       angle  = 90,
       code   = 3,
       length = 0.05,
       lwd    = 1.5)

NoteStandard Deviation vs Standard Error

Standard deviation (SD): describes the spread of your data — how variable individual measurements are.

Standard error of the mean (SE = SD / √n): describes the precision of your estimate of the mean — how variable the mean would be if you repeated the experiment.

Use SD when describing your sample. Use SE (or a confidence interval) when making inferences about the population mean. Many published papers incorrectly use SE to make distributions look narrower than they are.


Part 4: Box Plots — Comparing Distributions Across Groups

A box plot shows the full distribution of a continuous variable across groups: the median, interquartile range, and outliers. It is far more informative than a bar chart for this purpose.

# Box plot of enzyme activity across all four temperature groups
boxplot(activity ~ temperature,
        data   = enzyme,
        col    = c("#A8C4D9", "#5B8DB8", "#2C5F8A", "#1A3D5C"),
        ylab   = "Enzyme Activity (nmol/min/mg protein)",
        xlab   = "Temperature",
        main   = "Enzyme Activity Distribution by Temperature",
        outline = TRUE)

NoteAnatomy of a Box Plot
Element What it represents
Bottom of box 25th percentile (Q1)
Line inside box Median (50th percentile)
Top of box 75th percentile (Q3)
Box height Interquartile range (IQR = Q3 − Q1)
Whiskers Extend to 1.5 × IQR from box edges
Points beyond whiskers Potential outliers

Adding Raw Data Points

Box plots can hide multi-modal distributions or small sample sizes. Always add the raw data when n is small:

boxplot(activity ~ temperature,
        data    = enzyme,
        col     = "#A8C4D960",   # Transparent fill
        ylab    = "Enzyme Activity (nmol/min/mg protein)",
        xlab    = "Temperature",
        main    = "Enzyme Activity: Box Plot + Raw Data",
        outline = FALSE)

# Overlay individual data points with jitter
stripchart(activity ~ temperature,
           data     = enzyme,
           method   = "jitter",
           vertical = TRUE,
           add      = TRUE,
           pch      = 16,
           cex      = 0.7,
           col      = "#1A3D5C80")


Part 5: Scatter Plots — Exploring Relationships

Scatter plots show whether two continuous variables are related. This is essential for calibration curves, dose-response relationships, and regression analysis.

# Bradford assay standard curve: absorbance vs protein concentration
plot(protein_conc, absorbance,
     pch  = 16,
     col  = "#2C5F8A",
     cex  = 1.2,
     xlab = "Protein Concentration (ug/mL)",
     ylab = "Absorbance (595 nm)",
     main = "Bradford Assay Standard Curve")

# Add a best-fit line
abline(lm(absorbance ~ protein_conc), col = "#C8102E", lwd = 2)

# Add a legend and correlation coefficient
r <- cor(protein_conc, absorbance)
legend("topleft",
       legend = paste0("r = ", round(r, 3)),
       bty    = "n")

TipInterpreting a Standard Curve

The Bradford assay (and most spectrophotometric assays) produces a linear relationship between concentration and absorbance. Once you fit the line with lm(), you can rearrange it to calculate unknown protein concentrations from their absorbance readings.

We will cover linear regression in detail in Lab 22.


Part 6: Combining Plots — par(mfrow)

# Create a 2x2 panel showing all four plot types with the enzyme dataset
par(mfrow = c(2, 2),
    mar   = c(4, 4, 3, 1))   # Tighten margins

# Panel 1: Histogram of 35C activity
hist(enzyme$activity[enzyme$temperature == "35C"],
     col   = "#2C5F8A", border = "white",
     main  = "35C Activity Distribution",
     xlab  = "Activity")

# Panel 2: Bar chart of means
barplot(means,
        col   = c("#A8C4D9", "#5B8DB8", "#2C5F8A", "#1A3D5C"),
        main  = "Mean Activity by Temp",
        ylab  = "Mean activity")

# Panel 3: Box plot
boxplot(activity ~ temperature, data = enzyme,
        col  = "#5B8DB860",
        main = "Activity by Temperature",
        xlab = "Temperature", ylab = "Activity")

# Panel 4: Scatter of all data with jitter
plot(jitter(as.numeric(factor(enzyme$temperature))),
     enzyme$activity,
     col  = "#2C5F8A80",
     pch  = 16,
     xaxt = "n",
     xlab = "Temperature",
     ylab = "Activity",
     main = "Strip Plot")
axis(1, at = 1:4, labels = c("25C", "30C", "35C", "40C"))

par(mfrow = c(1, 1))   # Reset

3-Minute Knowledge Check

Close your notes. Answer these on your own — you have 3 minutes. We’ll go through the answers together after.

CautionKnowledge Check Questions

1. A researcher measures serum glucose levels in 80 patients. Which plot is best for showing the distribution of these values?

a) Bar chart    b) Scatter plot    c) Histogram    d) Pie chart

2. In a box plot, the line inside the box represents:

a) The mean    b) The median    c) The mode    d) The standard deviation

3. You add error bars to a bar chart using arrows(). What should the bars represent if you are making inferences about the population mean?

a) Range (min to max)    b) Standard deviation    c) Standard error or confidence interval    d) Variance

4. What does the R argument freq = FALSE in hist() change about the y-axis?

5. True or False: A box plot is more informative than a bar chart (mean ± SE) for visualising how a variable is distributed across groups.

1. c) Histogram — A histogram is designed for showing the distribution (shape, spread, centre) of a single continuous variable. Bar charts are for categorical summaries.

2. b) The median — The centre line of a box plot always represents the median (50th percentile). The mean may be represented separately as a point. This is one reason box plots are robust to outliers — they are based on medians and percentiles.

3. c) Standard error or confidence interval — When making inferences about the true population mean, error bars should represent SE or CIs. SD describes sample variability, not estimation precision.

4. It switches the y-axis from frequency (counts) to density (proportion per unit). Density is required when overlaying a probability distribution curve, because the area under a density histogram equals 1, matching the area under a probability density function.

5. True — A bar chart (mean ± SE) shows only two numbers: the mean and a measure of precision. A box plot shows the median, IQR, whiskers, and outliers — giving a far richer picture of how data are distributed across groups. A bimodal distribution, for example, looks identical to a normal distribution in a mean ± SE bar chart.


Lab 12 Checklist

Before you leave, make sure you can:

TipBonus Challenge

Using the enzyme data frame, produce a publication-quality box plot comparing enzyme activity across all four temperature groups. Add: (1) individual data points, (2) a horizontal reference line at the grand mean, (3) a meaningful title, and (4) axis labels with units. Export it as a PNG using png("filename.png") before and dev.off() after your plotting code.


Before Next Class (Wednesday)

  • Wednesday (Lab 13): ggplot2 — the publication-standard graphics system for R
  • Read R for Data Science Ch. 7–8 (link on Canvas)
  • If you have not installed ggplot2 yet: open Posit Cloud, click on your project, and run install.packages("ggplot2") in the Console before class