Bar Plot in R Base Graphics

Draw and customize a bar chart with base R barplot()

Data Visualization

Draw a bar plot in base R with barplot(). Chart counts from a table or values from a named vector, rename and colour the bars, go horizontal, add value labels, and build stacked and grouped bar charts from a matrix with a legend.

Published

July 8, 2026

Modified

July 9, 2026

TipKey takeaways
  • Draw a bar plot with barplot() — no package needed. Pass a named vector of values, or a table() of counts.
  • Rename bars with names.arg, add a heading and axis titles with main, xlab, ylab.
  • Colour the fill with col and the outline with border; pass a vector to colour each bar differently.
  • Flip to horizontal bars with horiz = TRUE; add value labels by capturing the bar midpoints bp <- barplot(...) and calling text(bp, …).
  • Give barplot() a matrix to get stacked bars, or add beside = TRUE for grouped bars — then label them with legend().

Introduction

A bar plot turns a set of category totals into a row of bars whose heights you can compare at a glance — counts per group, a value per category, a survey tally. In base R you draw one with barplot(), and it takes two kinds of input: a named vector of values (one bar per name) or a table() of counts (one bar per level). Neither needs a package.

This lesson covers both inputs, then the styling you actually reach for — renaming bars, titles, fill and border colours, horizontal layout, value labels on top of the bars — and finishes with the two multi-group forms: stacked and grouped bars from a matrix, tied together with a legend().

We use two built-in datasets: mtcars, where table(mtcars$cyl) counts cars by cylinder number, and VADeaths, a matrix of 1940 Virginia death rates (per 1000) cross-classified by age band and population group.

# Counts: how many cars have 4, 6, or 8 cylinders?
table(mtcars$cyl)

 4  6  8 
11  7 14 
# Values: a matrix of death rates (age band x population group)
VADeaths
      Rural Male Rural Female Urban Male Urban Female
50-54       11.7          8.7       15.4          8.4
55-59       18.1         11.7       24.3         13.6
60-64       26.9         20.3       37.0         19.3
65-69       41.0         30.9       54.6         35.1
70-74       66.0         54.3       71.1         50.0

Prefer the grammar of graphics? ggplot2’s geom_bar() and geom_col() build the legend and theming for you — see the ggplot2 series.

A basic bar plot of counts

The most common bar plot is a count per category. table() tallies a factor, and barplot() draws one bar per level. Here is the number of cars at each cylinder count, filled in brand azure:

counts <- table(mtcars$cyl)

barplot(counts,
        col = "#3a86d4", border = "white",
        main = "Cars by cylinder count",
        xlab = "Cylinders", ylab = "Number of cars")

A base R bar plot of car counts by cylinder number, three azure bars for 4, 6 and 8 cylinders, the 8-cylinder bar the tallest.

table(mtcars$cyl) supplies both the heights and the bar labels (4, 6, 8). col fills the bars and border = "white" gives them a clean separating edge; main, xlab and ylab set the title and axis labels — barplot() draws no axis titles by default.

A bar plot of values

When you already have one value per category — not raw data to count — pass a named vector. Each element becomes a bar and its name becomes the label. We pull one column out of VADeaths, which gives a named vector of death rates by age band:

deaths <- VADeaths[, "Rural Male"]
deaths
50-54 55-59 60-64 65-69 70-74 
 11.7  18.1  26.9  41.0  66.0 
barplot(deaths,
        col = "#3a86d4", border = "white",
        main = "Rural male death rates",
        xlab = "Age band", ylab = "Deaths per 1000")

A base R bar plot of rural male death rates across five age bands, five azure bars rising steadily with age.

deaths is a named numeric vector, so barplot() uses the values as heights and the names (50-54, 55-59, …) as labels. The rates climb steadily with age, exactly as you’d expect.

Renaming the bars

Override the labels with names.arg — a vector with one label per bar, in order. This is handy when the names are long, coded, or you want cleaner text than the data carries:

deaths <- VADeaths[, "Rural Male"]

barplot(deaths,
        names.arg = c("A", "B", "C", "D", "E"),
        col = "#3a86d4", border = "white",
        main = "Rural male death rates",
        xlab = "Age group", ylab = "Deaths per 1000")

The same rural male death-rate bar plot with the age bands relabelled from A to E along the x-axis.

names.arg must list one label per bar, in the same order as the values. Everything else — heights, colour, titles — is unchanged.

Horizontal bars

Set horiz = TRUE to lay the bars on their side. Horizontal bars read well when the category labels are long, or when you simply want the value axis along the bottom:

deaths <- VADeaths[, "Rural Male"]

barplot(deaths,
        horiz = TRUE, las = 1,
        col = "#3a86d4", border = "white",
        main = "Rural male death rates",
        xlab = "Deaths per 1000")

A horizontal base R bar plot of rural male death rates, five azure bars stacked with the rate axis running along the bottom.

With horiz = TRUE the value axis moves to the bottom and the categories stack up the side. las = 1 turns the axis labels upright so the age bands stay readable.

Colours: fill and border

col fills the bars; border colours their outlines. Give either a single colour for every bar, or a vector to colour each bar differently. A vector is useful when the colour itself carries meaning — here a colourblind-safe viridis set from base R’s hcl.colors():

op <- par(mfrow = c(1, 2))
deaths <- VADeaths[, "Rural Male"]
bar_cols <- hcl.colors(5, "viridis")   # colourblind-safe, base R

# Left: a single fill colour
barplot(deaths, col = "#3a86d4", border = "white",
        main = "Single fill")

# Right: one colour per bar
barplot(deaths, col = bar_cols, border = "white",
        main = "Colour per bar")

Two side-by-side base R bar plots of death rates: the left fills every bar one azure colour, the right gives each bar a different viridis colour.

par(op)

Use one colour when the bars are just categories you compare by height; use a vector — a colourblind-safe palette like hcl.colors(n, "viridis") — when the colour is meant to distinguish the categories.

Adding value labels on the bars

barplot() returns the x-positions of the bar centres — capture them in a variable and you can place the exact value on top of each bar with text(). This is the base-R idiom for value labels:

counts <- table(mtcars$cyl)

bp <- barplot(counts,
              col = "#3a86d4", border = "white",
              ylim = c(0, max(counts) * 1.15),   # headroom for the labels
              main = "Cars by cylinder count",
              xlab = "Cylinders", ylab = "Number of cars")

text(x = bp, y = counts, labels = counts,
     pos = 3, font = 2, col = "#1d4e7a")

A base R bar plot of car counts by cylinder with the count printed in bold just above each azure bar.

bp holds the bar midpoints, so text(x = bp, y = counts, …) writes each count at the top of its bar; pos = 3 nudges the label just above the bar and ylim adds headroom so it isn’t clipped. The labels sit on the white background, so they stay easy to read.

Stacked bar plots

Hand barplot() a matrix and it stacks the rows within each column. VADeaths is already a matrix — each column is a population group, each row an age band — so one call gives four stacked bars. Pass legend = rownames(...) to label the stacked segments:

bar_cols <- hcl.colors(5, "viridis")

barplot(VADeaths,
        col = bar_cols, border = "white",
        legend = rownames(VADeaths),
        main = "Death rates by population group",
        ylab = "Deaths per 1000")

A stacked base R bar plot of Virginia death rates for four population groups, each bar built from five viridis-coloured age-band segments with a legend.

Each bar is one column of VADeaths, and the five age-band rows stack from bottom to top. The colour vector maps to the rows, and legend = rownames(VADeaths) keys the age bands to their colours. Stacking shows the total per group and the composition inside it at the same time.

Grouped bar plots

Add beside = TRUE and the same matrix is drawn as grouped bars instead — one cluster per column, one bar per row. This is the form to reach for when you want to compare the individual age bands across groups, rather than read totals:

bar_cols <- hcl.colors(5, "viridis")

barplot(VADeaths,
        beside = TRUE,
        col = bar_cols, border = "white",
        legend = rownames(VADeaths),
        main = "Death rates by age band",
        ylab = "Deaths per 1000")

A grouped base R bar plot of Virginia death rates across four population groups, five viridis-coloured bars per group rising with age, with a legend for the age bands.

beside = TRUE places the five age-band bars side by side within each group. Now you can see at a glance that the rate rises with age in every group and that the urban and male groups run higher — a comparison the stacked version hides.

Placing the legend yourself

The built-in legend = argument sometimes overlaps the tallest bars. For full control, drop it and add a separate legend() call, which lets you choose the position and styling:

bar_cols <- hcl.colors(5, "viridis")

barplot(VADeaths,
        beside = TRUE,
        col = bar_cols, border = "white",
        main = "Death rates by age band",
        ylab = "Deaths per 1000")

legend("topleft",
       legend = rownames(VADeaths),
       fill = bar_cols,
       box.lty = 0, cex = 0.8)

The grouped death-rate bar plot with a manually placed legend in the top-left corner, no box around it.

legend("topleft", …) puts the key where you want it; fill = bar_cols draws the matching colour swatches. box.lty = 0 removes the box around the legend and cex = 0.8 shrinks the text so it doesn’t crowd the bars. See Add a legend for the full legend() toolkit.

Stacked vs grouped — which to use

Both charts draw the same matrix; they answer different questions.

Form Argument Best for
Stacked default (a matrix) The total per group, and each segment’s share of it.
Grouped beside = TRUE Comparing the individual categories across groups.

Reach for stacked when the whole matters (a total split into parts); reach for grouped when you need to compare like-for-like bars side by side.

Try it live

Swap the dataset, switch beside = TRUE on or off, or change the colours and press Run — it executes in your browser via webR, no install needed.

🟢 With an AI agent

Ask Prova “how do I put the value on top of each bar in a base R barplot?” — it answers with base-R code you can run, so the explanation is reproducible. The runtime is the judge. Ask Prova →

Common issues

Only one bar appears when I pass a whole data frame or two columns. barplot() wants a vector or a matrix, not a data frame. For counts, tabulate first — barplot(table(mtcars$cyl)). For a values chart, pass a single named vector — barplot(VADeaths[, "Rural Male"]). For multiple series, pass a matrix (as.matrix(df)), which triggers the stacked/grouped behaviour.

My value labels get cut off at the top of the plot. The default y-axis stops at the tallest bar, so a label sitting above it is clipped. Add headroom with ylim = c(0, max(counts) * 1.15) before writing the labels with text(..., pos = 3).

Frequently asked questions

Call barplot() on a named numeric vector (one bar per name) or a table() of counts (one bar per level) — for example barplot(table(mtcars$cyl)). No package is required; it ships with base R. Add col for the fill and main/xlab/ylab for the labels.

Tabulate the variable first, then plot the table: barplot(table(mtcars$cyl)). table() counts how many observations fall in each category and supplies both the bar heights and the labels, so barplot() draws one bar per level automatically.

Give barplot() a matrix. By default the rows are stacked within each column; add beside = TRUE for grouped (side-by-side) bars. Pass legend = rownames(m) and a col vector to colour and label the series.

Add horiz = TRUE to the barplot() call. The value axis moves to the bottom and the categories stack up the side; add las = 1 to keep the category labels upright and readable.

Capture the bar positions that barplot() returns — bp <- barplot(x) — then call text(x = bp, y = x, labels = x, pos = 3). Give the plot headroom with ylim = c(0, max(x) * 1.15) so the labels above the tallest bar aren’t clipped.

Test your understanding

Starting from the VADeaths matrix, draw a grouped bar plot (bars side by side, not stacked) using the five colourblind-safe colours below, and add a legend in the top-left corner labelling the age bands.

Pass beside = TRUE and col = bar_cols to barplot(). Then call legend("topleft", legend = rownames(VADeaths), fill = bar_cols).

bar_cols <- hcl.colors(5, "viridis")

barplot(VADeaths, beside = TRUE,
        col = bar_cols, border = "white",
        main = "Death rates by age band",
        ylab = "Deaths per 1000")

legend("topleft",
       legend = rownames(VADeaths),
       fill = bar_cols, box.lty = 0, cex = 0.8)

beside = TRUE clusters the bars; legend(fill = bar_cols) draws swatches that match the bar colours.

Quick check. You call barplot(VADeaths) with no other arguments. Are the bars stacked or grouped?

Stacked. For a matrix, barplot() stacks the rows within each column by default. You get grouped (side-by-side) bars only when you add beside = TRUE.

Conclusion

barplot() is the one function you need for bar charts in base R. Pass a named vector for a values chart, or a table() for counts. From there, names.arg relabels the bars, col/border set the colours, horiz = TRUE flips the layout, and capturing the returned midpoints lets text() add value labels. Give it a matrix for stacked bars, add beside = TRUE for grouped bars, and tie the series together with legend(). That covers the everyday bar plot end to end.

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {Bar {Plot} in {R} {Base} {Graphics}},
  date = {2026-07-08},
  url = {https://www.datanovia.com/learn/data-visualization/r-base/barplot},
  langid = {en}
}
For attribution, please cite this work as:
“Bar Plot in R Base Graphics.” 2026. July 8. https://www.datanovia.com/learn/data-visualization/r-base/barplot.