Skip to the content

Topics Covered

plot() Histogram Boxplot Scatter Plot Bar Chart Pie Chart Multi-panel Residual Plots Q-Q Plot
On this page
  1. 1. Plotting in R — Overview
  2. 2. Histograms
  3. 3. Boxplots
  4. 4. Scatter Plots
  5. 5. Bar Charts
  6. 6. Residual Plots for Assumption Checking
  7. 7. Saving Plots
  8. 8. Best Practices for Statistical Visualization
  9. Key Take-aways

1. Plotting in R — Overview

R offers three major plotting systems:

The generic plot() function dispatches on the class of the first argument:

Common Graphical Parameters

ParameterMeaningExample
mainTitlemain = "Histogram of Marks"
xlab, ylabAxis labelsxlab = "Hours"
colColourcol = "skyblue" or col = 2
pchPlotting character (0–25)pch = 19 (solid dot)
ltyLine typelty = 2 (dashed)
lwdLine widthlwd = 2
xlim, ylimAxis rangesxlim = c(0, 100)
cexSymbol/text sizecex = 1.2
lasAxis labels orientationlas = 1 (horizontal)

2. Histograms

Use hist() to display the distribution of a continuous variable.

data(mtcars)
hist(mtcars$mpg,
     breaks = 10,
     col    = "skyblue",
     border = "white",
     main   = "Distribution of Fuel Economy",
     xlab   = "Miles per Gallon",
     ylab   = "Frequency")

# With density curve overlay
hist(mtcars$mpg,
     freq = FALSE,            # density instead of counts
     col  = "lightyellow",
     main = "MPG Density")
lines(density(mtcars$mpg), col = "red", lwd = 2)
rug(mtcars$mpg)               # tick marks under axis
Frequency density(mpg) rug() 02 46 101520 2530 Miles per Gallon
What the code above draws: hist(mtcars$mpg) with the actual bin counts, a density() overlay (red) and rug() tick marks showing each individual car under the axis.

Controlling Bins

EXAMPLE 1
# Compare two histograms side by side
par(mfrow = c(1, 2))
hist(iris$Sepal.Length, col = "lightblue", main = "Sepal Length")
hist(iris$Petal.Length, col = "lightgreen", main = "Petal Length")
par(mfrow = c(1, 1))           # reset
EXAMPLE 2 — Histogram with normal overlay
x <- rnorm(500, mean = 60, sd = 10)
hist(x, freq = FALSE, breaks = 25, col = "lavender",
     main = "Sample vs Theoretical Normal", xlab = "Value")
lines(density(x), lwd = 2)              # sample density (black)
curve(dnorm(x, mean = 60, sd = 10),
      add = TRUE, col = "red", lwd = 2) # theoretical curve (red)
legend("topright", c("Sample density","N(60, 10)"),
       col = c("black","red"), lty = 1, lwd = 2, bty = "n")

3. Boxplots

Display the median, quartiles and outliers. The box spans Q1–Q3; each whisker extends to the most extreme observation still within 1.5 × IQR of the box; anything beyond that is drawn as an individual outlier point.

boxplot(mtcars$mpg,
        col  = "lightgreen",
        main = "Boxplot of MPG",
        ylab = "MPG")

# By group (formula syntax)
boxplot(mpg ~ cyl, data = mtcars,
        col  = c("salmon","skyblue","palegreen"),
        main = "MPG by Number of Cylinders",
        xlab = "Cylinders", ylab = "MPG",
        notch = TRUE)              # adds notches around medians

Interpretation: circles beyond the whiskers are outliers — points below Q1 − 1.5 × IQR or above Q3 + 1.5 × IQR (the fences are measured from the quartiles, not the median). Non-overlapping notches suggest the medians differ significantly.

Q1 − 1.5×IQR Q3 + 1.5×IQR outliers outlier Median Q1 Q3 whisker whisker IQR = Q3 − Q1 Whiskers stop at the last observation inside the fence; points beyond are plotted individually.
Anatomy of a boxplot as drawn by boxplot(): the fences at Q1 − 1.5×IQR and Q3 + 1.5×IQR (dashed) decide which points are flagged as outliers.
EXAMPLE 1
# All four iris numeric variables on one plot
boxplot(iris[, 1:4],
        col  = c("pink","skyblue","lightgreen","plum"),
        main = "Iris Measurements",
        ylab = "cm")
EXAMPLE 2 — Group comparison
boxplot(Sepal.Length ~ Species, data = iris,
        col = "lightyellow",
        main = "Sepal Length by Species")

4. Scatter Plots

plot(mtcars$wt, mtcars$mpg,
     pch  = 19, col = "navy",
     main = "Fuel Economy vs Weight",
     xlab = "Weight (1000 lbs)",
     ylab = "MPG")

# Add a regression line
fit <- lm(mpg ~ wt, data = mtcars)
abline(fit, col = "red", lwd = 2)

# Add LOESS smooth
lines(lowess(mtcars$wt, mtcars$mpg), col = "blue", lty = 2, lwd = 2)

legend("topright", c("Linear fit","LOESS"),
       col = c("red","blue"), lty = c(1,2), lwd = 2, bty = "n")
MPG Linear fit LOESS 101520 2530 23 45 Weight (1000 lbs)
The scatter plot the code produces: each dot is one car; the red line is abline(fit) (mpg = 37.29 − 5.34 × wt) and the dashed blue curve is the lowess() smooth, which reveals the relationship flattening for heavy cars.

Multi-variable Scatter Matrix

pairs(iris[, 1:4], col = iris$Species, pch = 19,
      main = "Iris Scatter Plot Matrix")

Plotting Symbols (pch) Reference

plot(1:25, rep(1, 25), pch = 1:25, cex = 2,
     main = "Plotting characters 1–25", ylab = "", yaxt = "n")
text(1:25, rep(1.2, 25), labels = 1:25)
Most-used pch symbols 012 345 6 151617 181921 222324 25 0–6: outline (col) · 15–19: solid (col) · 21–25: border = col, fill = bg
Quick reference for the plotting characters you will actually use. pch = 19 (large solid dot) is the usual choice for scatter plots; 21–25 accept a separate fill colour via bg =.
EXAMPLE 1 — Colour by group
plot(Petal.Length ~ Petal.Width, data = iris,
     col  = iris$Species, pch = 19,
     main = "Iris: Petal length vs width")
legend("topleft", levels(iris$Species),
       col = 1:3, pch = 19, bty = "n")
EXAMPLE 2 — Time series line
data(airquality)
plot(airquality$Day[1:31],
     airquality$Temp[1:31],
     type = "o", col = "darkred",
     main = "May Temperatures (NY)",
     xlab = "Day", ylab = "Temp (°F)")
grid()

5. Bar Charts

counts <- table(mtcars$cyl)
barplot(counts,
        col  = c("salmon","skyblue","palegreen"),
        main = "Number of Cars by Cylinders",
        xlab = "Cylinders", ylab = "Count")

# Horizontal
barplot(counts, horiz = TRUE)

# Stacked / grouped (two-way table)
tbl <- table(mtcars$gear, mtcars$cyl)
barplot(tbl, beside = TRUE,        # set FALSE for stacked
        col = c("lightblue","lightgreen","lightyellow"),
        main = "Cylinders by Gears",
        legend.text = TRUE)
3 gears 4 gears 5 gears 04 812 1 8 2 2 4 1 12 0 2 4 cyl6 cyl8 cyl barplot(table(gear, cyl), beside = TRUE) — real mtcars counts
Grouped bars from the two-way table: nearly all 8-cylinder cars have 3 gears, while 4-cylinder cars mostly have 4. Set beside = FALSE to stack the bars instead.

Pie Chart

pie(counts,
    col    = c("salmon","skyblue","palegreen"),
    labels = paste(names(counts), counts),
    main   = "Cars by Cylinders")

Note: Bar charts are usually preferable to pie charts for comparison.

EXAMPLE 1
fruits  <- c("Apple","Banana","Cherry","Date")
prices  <- c(80, 40, 200, 150)
barplot(prices, names.arg = fruits,
        col = "lightcoral",
        main = "Fruit Prices (₹/kg)", ylab = "₹/kg")
EXAMPLE 2 — Stacked frequency
# Iris counts by species (one per row, but illustrative)
sp <- table(iris$Species)
barplot(sp, col = c("plum","skyblue","palegreen"),
        main = "Iris Sample by Species")

6. Residual Plots for Assumption Checking

After fitting a model with lm() or glm(), R provides four diagnostic plots through plot(model).

fit <- lm(mpg ~ wt + hp, data = mtcars)
par(mfrow = c(2, 2))
plot(fit)
par(mfrow = c(1, 1))

The four panels are:

  1. Residuals vs Fitted — checks linearity & homoscedasticity. A flat red line ⇒ ok.
  2. Normal Q-Q — residuals should lie close to the straight reference line if normally distributed.
  3. Scale–Location — checks equal variance; should show no clear pattern.
  4. Residuals vs Leverage — points outside Cook's distance contours are influential.
1 · Residuals vs Fitted random cloud + flat red line ⇒ linear, equal variance 2 · Normal Q-Q points near the line ⇒ residuals ≈ normal (tails stray first) 3 · Scale–Location flat trend ⇒ homoscedastic; a rising wedge ⇒ variance grows 4 · Residuals vs Leverage influential point outside the dashed Cook's-distance bands ⇒ investigate
What plot(fit) shows when the model assumptions hold. Trouble signs: a curved red line in panel 1 (non-linearity — see Example 2), S-shaped points in panel 2 (non-normality), a wedge in panel 3 (heteroscedasticity), or points past the Cook's bands in panel 4.

Other Useful Diagnostics

# Histogram of residuals
hist(resid(fit), breaks = 12, col = "lightgray",
     main = "Distribution of Residuals", xlab = "Residual")

# Q-Q plot for any data
qqnorm(resid(fit)); qqline(resid(fit), col = "red", lwd = 2)

# Shapiro-Wilk test for normality
shapiro.test(resid(fit))
EXAMPLE 1
fit <- lm(Sepal.Length ~ Petal.Length, data = iris)
par(mfrow = c(2, 2)); plot(fit); par(mfrow = c(1, 1))
shapiro.test(resid(fit))   # p > 0.05 ⇒ residuals plausibly normal
EXAMPLE 2 — Spotting non-linearity
x <- 1:50
y <- x + x^2/40 + rnorm(50, 0, 3)    # quadratic relation + noise
fit <- lm(y ~ x)
par(mfrow = c(1, 2))
plot(x, y, main = "Data + Linear Fit"); abline(fit, col = "red")
plot(fit, which = 1)                  # curved residual pattern signals non-linearity
par(mfrow = c(1, 1))

7. Saving Plots

# PDF
pdf("myplot.pdf", width = 8, height = 6)
hist(mtcars$mpg, col = "skyblue", main = "MPG")
dev.off()

# PNG
png("myplot.png", width = 800, height = 600, res = 100)
boxplot(mpg ~ cyl, data = mtcars, col = "lightyellow")
dev.off()

# JPEG, SVG, TIFF — similar
svg("myplot.svg"); plot(1:10); dev.off()

8. Best Practices for Statistical Visualization

Key Take-aways