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
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")
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:
barplot(deaths,col ="#3a86d4", border ="white",main ="Rural male death rates",xlab ="Age band", ylab ="Deaths per 1000")
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")
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")
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 colourbarplot(deaths, col ="#3a86d4", border ="white",main ="Single fill")# Right: one colour per barbarplot(deaths, col = bar_cols, border ="white",main ="Colour per bar")
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 labelsmain ="Cars by cylinder count",xlab ="Cylinders", ylab ="Number of cars")text(x = bp, y = counts, labels = counts,pos =3, font =2, col ="#1d4e7a")
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")
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")
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)
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
NoteHow do I make a bar plot in R?
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.
NoteHow do I make a bar chart of counts in R?
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.
NoteHow do I create a grouped or stacked bar plot in R?
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.
NoteHow do I draw a horizontal bar plot in R?
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.
NoteHow do I add value labels on top of the bars in R?
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
ImportantExercise: a grouped bar plot with a legend
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.
TipHint
Pass beside = TRUE and col = bar_cols to barplot(). Then call legend("topleft", legend = rownames(VADeaths), fill = bar_cols).
TipSolution
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?
TipShow answer
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.
Related lessons
Histogram & density — for the distribution of a continuous variable, where a histogram, not a bar plot, is the right tool. · Box plots — compare a numeric variable across groups. · Add a legend — the full legend() toolkit for keying colours to groups. · Colours in R — build colourblind-safe palettes.
This lesson is reproducible: every figure was produced by the code shown, executed at build time (if they render, the code works), and the sandbox re-runs live in your browser. Edit any block and Run. The runtime is the judge.