Menu

Bar Chart in R: barplot() from Counts and Tables

How to make a bar chart in R with barplot() - from a named vector or a table(), grouped and stacked bars from a matrix, styling, and geom_col vs geom_bar.

This page includes runnable editors - edit, run, and see output instantly.

Bar Charts Compare Categories

A bar chart answers "how much of each?" - one bar per category, with bar length showing the category's value. In R the base function is barplot(), and its input is simply a vector of heights. Give the vector names and the names become the labels under the bars:

visits <- c(Mon = 42, Tue = 51, Wed = 47, Thu = 60, Fri = 78)

barplot(visits,
        main = "Store visits by day",
        ylab = "Visits")

Five gray bars, labeled by day, climbing from a modest Monday to a Friday peak nearly twice as tall. Nothing to install, nothing to configure - if you can build a named vector, you can chart it.

The runnable editors on this page show text output only, so the barplot() calls here are static snippets to paste into a local R session - but the counting step before them runs fine, and that's the step that matters most.

The Real Workflow: table() First, barplot() Second

In practice you rarely have tidy pre-counted heights. You have a raw column of category labels - one row per survey response, per order, per user - and the counting is the actual work. table() does it in one call, and its output is a named vector of counts, which is exactly what barplot() wants:

Then the chart is one more line:

barplot(sort(t, decreasing = TRUE),
        main = "Survey responses",
        ylab = "Count")

That two-step - table() then barplot() - is the single most common bar-chart flow in R. Sorting the table first is a cheap upgrade: bars ordered by height read faster than bars ordered alphabetically, unless the categories have a natural order (days, months, dose levels) - in which case a factor with properly ordered levels keeps table() from alphabetizing them. For bars that need real aggregation - sums or means per group rather than row counts - do the group-by-summarize step first and hand barplot() the result.

Labels and Style

The labels normally come from the vector's names, but names.arg overrides them - useful when the data's codes aren't fit for an audience:

barplot(visits,
        names.arg = c("Monday", "Tuesday", "Wednesday",
                      "Thursday", "Friday"),
        col    = "steelblue",
        border = NA,
        main   = "Store visits by day",
        las    = 2)
  • col fills the bars; pass a vector to color each bar individually, but resist rainbow-by-default - color should mean something, like flagging the one bar you're talking about.
  • border = NA removes the bar outlines for a cleaner look.
  • las = 2 rotates the axis labels perpendicular to the axis - the fix for long category names overlapping each other. Combine it with a bigger bottom margin (par(mar = c(8, 4, 4, 2))) if the names are truly long.

Grouped and Stacked Bars from a Matrix

To show two categorical dimensions at once - say, sales per channel per quarter - hand barplot() a matrix. Each column becomes one position on the category axis, and the rows become the sub-bars:

sales <- matrix(c(12, 18,   15, 22,   20, 19,   24, 27),
                nrow = 2,
                dimnames = list(c("Online", "Retail"),
                                c("Q1", "Q2", "Q3", "Q4")))

# Grouped: rows stand next to each other within each quarter
barplot(sales, beside = TRUE,
        col = c("steelblue", "orange"),
        legend.text = rownames(sales),
        main = "Sales by channel and quarter")

# Stacked: rows pile up into one total bar per quarter (the default)
barplot(sales, beside = FALSE,
        col = c("steelblue", "orange"),
        legend.text = rownames(sales))

beside = TRUE puts the two channels shoulder to shoulder within each quarter - best when you want to compare the channels directly. The stacked default emphasizes the total per quarter, with the split shown inside each bar; it's honest for totals but makes the inner segments hard to compare across bars, since their baselines shift. legend.text = rownames(sales) adds the legend that makes either version readable.

One more switch: horiz = TRUE lays the bars along the horizontal axis. It's the kindest layout for many categories with long names - every label sits on its own line next to its bar, no rotation needed.

Bar Chart or Histogram?

Searchers mix these up constantly, and the distinction is worth thirty seconds: a bar chart compares distinct categories - the axis is a set of labels, the bars are separated, and reordering them is legal. A histogram bins one numeric variable - the axis is a continuous scale, the bars touch because the bins are consecutive intervals, and reordering them would be nonsense. If your column contains words ("Yes", "Chrome", "Q3"), you want barplot(table(x)). If it contains measurements (heights, prices, response times), you want the histogram and hist(x).

ggplot2: geom_col vs geom_bar

The ggplot2 version comes in two geoms, split by whether the counting is already done:

library(ggplot2)

# geom_bar(): raw observations in, counting done for you
ggplot(survey, aes(x = response)) +
    geom_bar(fill = "steelblue")

# geom_col(): heights already computed, one row per bar
totals <- data.frame(day = c("Mon", "Tue", "Wed", "Thu", "Fri"),
                     visits = c(42, 51, 47, 60, 78))

ggplot(totals, aes(x = day, y = visits)) +
    geom_col(fill = "steelblue")

geom_bar() is table() built into the plot: it takes one row per observation and tallies. geom_col() takes pre-aggregated data and maps a y column to bar height - the equivalent of classic barplot(). Feeding already-counted data to geom_bar() is the standard beginner trap: it counts the rows (one each) and every bar comes out the same height.

What You Take Away

  • barplot(heights) charts a named vector - and table(x) produces exactly that shape from raw category data, so barplot(table(x)) is the core idiom.
  • Sort the table (or set factor levels) before plotting; bar order is part of the message.
  • A matrix gives grouped (beside = TRUE) or stacked (default) bars; add legend.text = rownames(m).
  • names.arg, col, las = 2, and horiz = TRUE handle labeling; horizontal bars are best for long names.
  • Categories → bar chart; numeric bins → histogram. In ggplot2: raw rows → geom_bar(), computed heights → geom_col().

Next up: ggplot2 itself - the grammar that builds all of these charts, and many more, from the same few composable pieces.

Frequently Asked Questions

How do you make a bar chart in R?

Call barplot() on a vector of heights. Give the vector names - or build it with table(), whose output is already named - and each element becomes one labeled bar: barplot(table(df$category)).

How do you make a grouped or stacked barplot in R?

Pass a matrix. Each column becomes one position on the category axis; beside = TRUE draws the rows as bars standing next to each other (grouped), while the default beside = FALSE stacks them. Add legend.text = rownames(m) so readers can tell the rows apart.

What is the difference between barplot() and hist() in R?

barplot() compares distinct categories - one bar per category, bars separated. hist() bins one numeric variable into consecutive intervals - bars touch because the axis is a continuous scale. If your variable is words, you want barplot(table(x)); if it's numbers, you want hist(x).

Should I use geom_bar or geom_col in ggplot2?

geom_bar() counts for you - give it raw observations and it tallies each category. geom_col() plots values you've already computed - give it one row per bar plus a height column. Pre-counted data with geom_bar() is the classic mistake; it needs geom_col().

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED