Skip to the content.

Base Plotting

Last Updated: 20, November, 2025 at 09:27

Notes

The base plotting system in R is somewhat dated (we’ll run into some limits below). But it is still a way of creating simple plots programmatically. I think these this approach is good for quick plots while processing and exploring data. Or for very simple plots. This is an online article that shows some more advanced use of the base plotting system: here.

At the other end of the spectrum, is ggplot2 (part of the tidyverse) which is a very powerful plotting system that allows creating complex plots with relative ease. But it takes time to get used to it. Luckily, there are some intermediate solutions:

Package Description Examples/Tutorials
lattice Powerful multi-panel plots with simpler syntax than ggplot2. Lattice Gallery
plotly Interactive, web-based plots with hover tooltips and zooming. Plotly R Examples
easyGgplot2 [NO LONGER WORKS - USE GGPUBR]Simplifies ggplot2 syntax for common tasks. easyGgplot2 Vignette
esquisse RStudio add-in for drag-and-drop ggplot2 plot creation. esquisse Demo
patchwork Simplifies combining and arranging ggplot2 plots. patchwork Examples
ggpubr Extends ggplot2 for publication-ready plots with one-line functions. ggpubr Gallery

Read in some data

This is the data source.

library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.1     ✔ stringr   1.5.2
## ✔ ggplot2   4.0.0     ✔ tibble    3.3.0
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.1.0     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
body_data <- read_csv('data/body.csv')
## Rows: 507 Columns: 25
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (25): Biacromial, Biiliac, Bitrochanteric, ChestDepth, ChestDia, ElbowDi...
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
head(body_data)
## # A tibble: 6 × 25
##   Biacromial Biiliac Bitrochanteric ChestDepth ChestDia ElbowDia WristDia
##        <dbl>   <dbl>          <dbl>      <dbl>    <dbl>    <dbl>    <dbl>
## 1       42.9    26             31.5       17.7     28       13.1     10.4
## 2       43.7    28.5           33.5       16.9     30.8     14       11.8
## 3       40.1    28.2           33.3       20.9     31.7     13.9     10.9
## 4       44.3    29.9           34         18.4     28.2     13.9     11.2
## 5       42.5    29.9           34         21.5     29.4     15.2     11.6
## 6       43.3    27             31.5       19.6     31.3     14       11.5
## # ℹ 18 more variables: KneeDia <dbl>, AnkleDia <dbl>, Shoulder <dbl>,
## #   Chest <dbl>, Waist <dbl>, Navel <dbl>, Hip <dbl>, Thigh <dbl>, Bicep <dbl>,
## #   Forearm <dbl>, Knee <dbl>, Calf <dbl>, Ankle <dbl>, Wrist <dbl>, Age <dbl>,
## #   Weight <dbl>, Height <dbl>, Gender <dbl>
try(dev.off()) # Make sure all graphic parameters are reset
## null device 
##           1

Basic plot type: lines and scatter

Scatter plot

plot(body_data$KneeDia, body_data$Forearm)

Line plot

x <- 1:10
y <- runif(10) * x
plot(x, y, type= 'l')

plot(x, y, type= 'b')

Setting colors and markers

See here for a list of pch marker values. See here for a list of color names R knows out of the box.

x <- 1:10
y <- runif(10) * x
plot(x, y, type= 'b', pch=15, col ='red2')

R also knows about hex colors.

x <- 1:10
y <- runif(10) * x
plot(x, y, type= 'b', pch=15, col ='#47B04E')

Handy Dandy: adding an alpha channel to a color

my_red <- adjustcolor( "red2", alpha.f = 0.25) #This creates a red color with 25% opacity
plot(body_data$KneeDia, body_data$Forearm, pch=16, col=my_red)

Basic plot type: histogram

The hist() function has a number of interesting arguments:

hist(body_data$ChestDepth, freq = FALSE, main ='A normalized histogram')

Basic plot type: barchart

Some interesting arguments:

labels <- c('a', 'b', 'c', 'd', 'e', 'f')
values <- c(1, 2, 3, 1, 2, 3)
barplot(values, names.arg = labels)

Basic plot type: boxplot

body_data$AgeCat <- cut(body_data$Age, 10)
boxplot(body_data$Height ~ body_data$AgeCat)

Setting plot parameters

The function par() allows setting parameters for subsequent plots. Most importantly, you can set the subsequent plots’ margins and number of of subplots.

Setting the inner and the outer margins

You can find more information about inner and outer margins in R here.

par(oma=c(0,0,0,0))
x <- 1:10
y <- runif(10) * x
plot(x, y, type= 'l')

Plotting subplots

par(mfcol = c(1,2))
hist(body_data$ChestDepth, freq = FALSE, main ='A normalized histogram')
hist(body_data$Biiliac, freq = FALSE, main ='A normalized histogram')

try(dev.off())  # Make sure all graphic parameters are reset
## null device 
##           1

https://r-charts.com/base-r/combining-plots/

Changing text and font

Adding labels

  1. axis labels: xlab =, ylab =
  2. subtitle: sub =
  3. title: main =

Changing the font face

font face: font =

font family: family =

Scaling text sizes

  1. scaling all elements: cex =
  2. scaling axis labels: cex.lab =
  3. scaling subtitle: cex.sub =
  4. scaling tick mark labels: cex.axis =
  5. scaling title: cex.main =

Example

plot(x, y, type= 'b', family='serif', main='Some title', cex=1.25)

Adding stuff to existing plots

Adding points or lines

plot(body_data$KneeDia, body_data$Forearm)
points(c(18, 20, 22), c(22, 23, 24), type='b', col='red2')
points(c(18, 20, 22), c(23, 24, 25), col='blue2')

A limitation of R

This does cuts the range of the plot to the range of the first plotted data!

my_blue <- adjustcolor( "navyblue", alpha.f = 0.25)
my_red <- adjustcolor( "indianred4", alpha.f = 0.25)

males <- body_data[body_data$Gender==0,]
females <- body_data[body_data$Gender==1,]

plot(females$Height, females$Weight, pch=15, main='Some graph', col=my_blue)
points(males$Height, males$Weight, pch=15, main='Some graph', col=my_red)

This solves the problem. But it’s a bit dissapointing that R does not update the axes of the plots.

my_blue <- adjustcolor( "navyblue", alpha.f = 0.25)
my_red <- adjustcolor( "indianred4", alpha.f = 0.25)

xrange <- range(body_data$Height)
yrange <- range(body_data$Weight)

males <- body_data[body_data$Gender==0,]
females <- body_data[body_data$Gender==1,]

plot(females$Height, females$Weight, pch=15, main='Some graph', col=my_blue, xlim = xrange, ylim = yrange)
points(males$Height, males$Weight, pch=15, main='Some graph', col=my_red)

More bling

my_blue <- adjustcolor( "navyblue", alpha.f = 0.25)
my_red <- adjustcolor( "indianred4", alpha.f = 0.25)
my_orange <- adjustcolor( "orange", alpha.f = 0.5)

xrange <- range(body_data$Height)
yrange <- range(body_data$Weight)

males <- body_data[body_data$Gender==0,]
females <- body_data[body_data$Gender==1,]

plot(females$Height, females$Weight, pch=15, main='Some graph', col=my_blue, xlim = xrange, ylim = yrange, xlab='Height', ylab='Weight')
points(males$Height, males$Weight, pch=15, main='Some graph', col=my_red)

# Add a label to the graph
text(x=160, y=100, labels='A label', col="green2")
# Add a label to the graph
text(x=190, y=50, labels='A label', col="blue", font=3, family='serif')
# Add an arrow
arrows(x0=190, y0=100, x1=170,y1=60, length = 0.1, lwd=4, col=my_orange)

Adding legends

Adding legends can be done using the legend(). These are the main arguments to the function:

You can set the location using a keyword, i.e. x= “bottomright”, “bottom”, “bottomleft”, “left”, “topleft”, “top”, “topright”, “right” or “center”.

Apart from the location of the legend and the text to appear, you have to provide some parameters that set the legend’s markers.

my_blue <- adjustcolor( "navyblue", alpha.f = 0.25)
my_red <- adjustcolor( "indianred4", alpha.f = 0.25)
my_orange <- adjustcolor( "orange", alpha.f = 0.5)

xrange <- range(body_data$Height)
yrange <- range(body_data$Weight)

males <- body_data[body_data$Gender==0,]
females <- body_data[body_data$Gender==1,]

plot(females$Height, females$Weight, pch=15, main='Some graph', col=my_blue, xlim = xrange, ylim = yrange, xlab='Height', ylab='Weight')
points(males$Height, males$Weight, pch=15, main='Some graph', col=my_red)

# Add the legend - notice: we have to set the colors and markers for the legend manually
legend('bottomright', c('Men', 'Women'), col = c(my_red, my_blue), pch=15)

Exercise

Use these data:

body_data <- read_csv('data/body.csv')
## Rows: 507 Columns: 25
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (25): Biacromial, Biiliac, Bitrochanteric, ChestDepth, ChestDia, ElbowDi...
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Potential solution

# Preparation
male_color <- adjustcolor( "navyblue", alpha.f = 0.25)
female_color <- adjustcolor( "indianred4", alpha.f = 0.25)

#Create 10Y age bins
body_data <- mutate(body_data, AgeCat= round(Age/10)*10) #Truncate to tens

# This illustrates the transformation we've done
plot(body_data$Age, body_data$AgeCat)
points(25, 20, col='red', pch=16)
points(26, 30, col='green', pch=16)

points(35, 30, col='red', pch=16)
points(36, 40, col='green', pch=16)

#Split out the data
male_data <- filter(body_data, Gender==1)
female_data <- filter(body_data, Gender==0)

# We could use the histogram function here. 
# But sometimes it makes sense to think outside the box

library(janitor)
## 
## Attaching package: 'janitor'

## The following objects are masked from 'package:stats':
## 
##     chisq.test, fisher.test
male_counts <- tabyl(male_data$AgeCat)
female_counts <- tabyl(female_data$AgeCat)

# This just makes things more handy
male_counts <- rename(male_counts, AgeCat = 'male_data$AgeCat')
female_counts <- rename(female_counts, AgeCat = 'female_data$AgeCat')


# Plotting the females first makes things easier: 
# There are more age categories in the females
# The counts are higher
# Therefore, by plotting the females first, we make sure that all bars are labeled.

barplot(female_counts$n, names.arg = female_counts$AgeCat, col=female_color)
barplot(male_counts$n, names.arg = male_counts$AgeCat, col=male_color, xlab='Age Category', ylab='Count', main='Age distribution by gender', add=TRUE)

The previous exercise highlighted that the old plotting system in R is clunky. Therefore, people have designed a new plotting system: ggplot2 (part of the tidyverse). I have a section on this in the next file. But here is a simple example to show how we could recreate the previous plot using ggplot2.

The downside of ggplot2 is that it takes time to get used to it.

library(tidyverse)
body_data <- read_csv('data/body.csv')
## Rows: 507 Columns: 25
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (25): Biacromial, Biiliac, Bitrochanteric, ChestDepth, ChestDia, ElbowDi...
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
male_color <- adjustcolor( "navyblue", alpha.f = 0.25)
female_color <- adjustcolor( "indianred4", alpha.f = 0.25)

body_data <- mutate(body_data, AgeCat= round(Age/10)*10) #Truncate to tens
basic_plot <- ggplot(body_data)
basic_plot <- basic_plot + aes(x=AgeCat, fill=factor(Gender)) 
basic_plot <- basic_plot + geom_bar(position='identity', alpha=0.5)
basic_plot <- basic_plot + scale_fill_manual(values=c(female_color, male_color), labels=c('Female', 'Male'))

# Initialize a ggplot object using the 'body_data' dataset
basic_plot <- ggplot(body_data)

# Define the aesthetics for the plot:
# - x-axis: AgeCat (age groups in decades)
# - fill: Gender (converted to a factor for categorical coloring)
basic_plot <- basic_plot + aes(x = AgeCat, fill = factor(Gender))

# Add bar geometry to the plot:
# - position='identity' overlays bars for each gender (no stacking or dodging)
# - alpha=0.5 makes bars semi-transparent for better visibility of overlaps
basic_plot <- basic_plot + geom_bar(position = 'identity', alpha = 0.5)

# Customize the fill colors and legend labels:
# - values: Use predefined colors for females and males (e.g., female_color, male_color)
# - labels: Set legend labels to 'Female' and 'Male'
basic_plot <- basic_plot + scale_fill_manual(values = c(female_color, male_color), labels = c('Female', 'Male'))

Let’s do the same thing with the easy version of ggplot2, ggpubr.

library(ggpubr)
body_data$Gender <- factor(body_data$Gender)
gghistogram(
  data = body_data,
  x = "Age",
  color = "Gender",
  fill = "Gender",
  position = "identity",
  alpha = 0.4,
  legend = "top"
)
## Warning: Using `bins = 30` by default. Pick better value with the argument
## `bins`.

## More exercises

The internet is full of exercises on plotting in R (using the base plotting system). Here are two selected resources:

https://www.bioinformatics.babraham.ac.uk/training/Core_R_Plotting/Core%20R%20Plotting%20Exercises.pdf

This one uses the cars data we have already used in the course (I’ved added it to the data folder):

https://www.r-exercises.com/2016/09/23/advanced-base-graphics-exercises/