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", ...)
| Argument | What it does |
|---|---|
mapping | Aesthetic mappings from aes() |
data | Data frame for this layer |
stat | "count" (default), "identity", or "fill" |
position | "stack" (default), "dodge", "fill", or "identity" |
width | Bar width as proportion of available space |
na.rm | Remove 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
| Parameter | Type | Default | Description |
|---|---|---|---|
mapping | aesthetic | NULL | Aesthetic mappings |
data | data.frame | NULL | Layer data |
stat | string | "count" | "count", "identity", or "fill" |
position | string | "stack" | "stack", "dodge", "fill", "identity" |
width | numeric | 0.9 | Bar width as proportion |
na.rm | logical | FALSE | Skip missing values silently |
show.legend | logical/NA | NA | Show in legend |
inherit.aes | logical | TRUE | Inherit global aesthetics |
See also
- /reference/tidyverse/ggplot2-geom-point/, scatter plots for continuous vs continuous data
- /reference/tidyverse/ggplot2-geom-line/, line charts for sequential data
- /tutorials/r-data-visualization/introduction-to-ggplot2/, layered grammar concept behind all geoms
- /reference/tidyverse/ggplot2-aes/ — aesthetic mapping system both geoms share