rguides

ggplot2::geom_bar()

geom_bar(mapping = NULL, data = NULL, stat = "count", position = "stack", ...)

geom_bar() draws bars whose height is proportional to the count of observations in each category (by default) or to a value you supply. It’s the go-to for comparing frequencies across categories, and the default behaviour counts observations without you having to pre-summarise the data.

Syntax

geom_bar(mapping = NULL, data = NULL, stat = "count", position = "stack", ...)
ArgumentWhat it does
mappingAesthetic mappings from aes()
dataData frame for this layer
stat"count" (default), "identity", or "fill"
position"stack" (default), "dodge", "fill", or "identity"
widthBar width as proportion of available space
na.rmRemove missing values silently

Basic usage

library(ggplot2)

# Count of diamonds by cut
ggplot(diamonds, aes(x = cut)) +
  geom_bar()

geom_bar() counts how many diamonds fall into each cut category and draws a bar for each. You don’t need to call group_by() and summarise() first, geom_bar() does it for you with stat = "count".

Bar height with stat = “identity”

The default stat = "count" tallies occurrences of each x value. When your data already contains the bar heights you want to display, switch to stat = "identity" and map the y aesthetic to the precomputed value column. The bar height then directly reflects that column’s values without any counting step:

# Pre-summarised revenue by department
dept_revenue <- data.frame(
  dept = c("sales", "engineering", "design"),
  revenue = c(420000, 380000, 210000)
)

ggplot(dept_revenue, aes(x = dept, y = revenue)) +
  geom_bar(stat = "identity")

Stacked, dodged, and filled bars

The position argument controls how multiple groups (split by fill) are arranged within each bar. The default "stack" layers subgroups on top of each other, "dodge" places them side by side for direct height comparison, and "fill" normalizes every bar to height 1 so you can compare proportions independent of group size:

# Stacked (default) — layers on top of each other
ggplot(diamonds, aes(x = cut, fill = clarity)) +
  geom_bar(position = "stack")

# Dodged — bars side by side
ggplot(diamonds, aes(x = cut, fill = clarity)) +
  geom_bar(position = "dodge")

# Filled — all bars normalised to height 1
ggplot(diamonds, aes(x = cut, fill = clarity)) +
  geom_bar(position = "fill")

Filled bars are useful when you want to compare proportions across categories regardless of the total count in each group.

Colour and fill aesthetics

The fill aesthetic controls the interior colour of bars, while colour sets the border stroke. A solid fill colour applied outside aes() gives every bar the same appearance. Mapping fill to the same variable as x creates a categorical gradient across the bars; mapping it to a different variable splits each bar into coloured segments:

# Solid colour for all bars
ggplot(diamonds, aes(x = cut)) +
  geom_bar(fill = "steelblue", colour = "darkblue")

# Colour mapped to the same variable as x — creates a gradient
ggplot(diamonds, aes(x = cut, fill = cut)) +
  geom_bar()

Width control

Bars take up 90% of the available width by default, leaving a small gap between them. Reduce width for narrower bars with more white space, or increase it toward 1 for bars that nearly touch. A width of 1 removes all spacing between adjacent bars:

# Thinner bars
ggplot(diamonds, aes(x = cut)) +
  geom_bar(width = 0.6)

# Fat bars (almost touching)
ggplot(diamonds, aes(x = cut)) +
  geom_bar(width = 0.95)

Horizontal bars

When category names are long, mapping them to the y aesthetic and then calling coord_flip() produces horizontal bars with labels that read naturally from left to right. This avoids the need to rotate or abbreviate x-axis text, producing a cleaner plot that is easier to scan:

ggplot(diamonds, aes(y = cut)) +
  geom_bar() +
  coord_flip()

geom_bar vs geom_col

geom_col() is shorthand for geom_bar(stat = "identity"). It directly plots a precomputed value column without counting, which is cleaner when your data is already summarized. The two forms are functionally identical but geom_col() communicates the intent more directly to anyone reading the code:

# geom_col: y is the value
ggplot(dept_revenue, aes(x = dept, y = revenue)) +
  geom_col()

# Equivalent to geom_bar with stat = "identity"
ggplot(dept_revenue, aes(x = dept, y = revenue)) +
  geom_bar(stat = "identity")

Use geom_col() when your data already has the numbers you want to display. Use geom_bar() when you want ggplot2 to count observations for you.

Dodge position for side-by-Side bars

position = "dodge" places groups side by side within each category, making comparison across groups easier:

# Dodge by transmission type
ggplot(mtcars, aes(x = factor(cyl), fill = factor(gear))) +
  geom_bar(position = "dodge")

With position = "dodge", each group gets its own coloured bar within each category, spaced horizontally.

Parameters

ParameterTypeDefaultDescription
mappingaestheticNULLAesthetic mappings
datadata.frameNULLLayer data
statstring"count""count", "identity", or "fill"
positionstring"stack""stack", "dodge", "fill", "identity"
widthnumeric0.9Bar width as proportion
na.rmlogicalFALSESkip missing values silently
show.legendlogical/NANAShow in legend
inherit.aeslogicalTRUEInherit global aesthetics

See also