ggplot2 geom_density2d() in R: 2D Density Contour Lines

The geom_density2d() function in ggplot2 estimates 2D kernel density from raw points and draws contour LINES of equal density. It is the contour-only sister of geom_density_2d_filled() and complements scatter plots.

⚡ Quick Answer
ggplot(df, aes(x, y)) + geom_density_2d()
geom_density_2d(bins = 10)               # control contour density
geom_density_2d_filled()                  # filled bands instead
geom_hex()                                 # rectangular alternative
geom_point() + geom_density_2d()          # raw points + density

Need explanation? Read on for examples and pitfalls.

📊 Is geom_density2d() the right tool?
START2D density contour LINES from raw pointsgeom_density_2d()2D density FILLED bandsgeom_density_2d_filled()count-based binninggeom_hex(), geom_bin2d()per-row density (z given)geom_contour()raw scattergeom_point()

What geom_density2d() does in one sentence

geom_density_2d() estimates a 2D kernel density from raw (x, y) points and draws contour lines of equal density. It overlays scatter plots to show density patterns hidden by overplotting.

Syntax

geom_density_2d(bins = NULL, contour_var = "density", h = NULL, ...). Requires aes(x, y); raw points (no z).

Run live
Run live, no install needed. Every R block on this page runs in your browser. Click Run, edit the code, re-run instantly. No setup.
RDensity contour over scatter
library(ggplot2) ggplot(faithful, aes(eruptions, waiting)) + geom_point(alpha = 0.4) + geom_density_2d()

  
Tip
Use geom_density_2d_filled() for filled bands instead of lines. The two-mode old-faithful dataset shows clear contours either way.

Five common patterns

1. Density on top of scatter

RReveal hidden density
ggplot(faithful, aes(eruptions, waiting)) + geom_point(alpha = 0.5) + geom_density_2d(color = "steelblue")

  

2. Filled density bands

RMore dramatic
ggplot(faithful, aes(eruptions, waiting)) + geom_density_2d_filled() + scale_fill_viridis_d()

  

3. Custom number of contours

Rbins controls level count
ggplot(faithful, aes(eruptions, waiting)) + geom_density_2d(bins = 5)

  

4. With facets

RPer-group density
ggplot(iris, aes(Sepal.Length, Sepal.Width)) + geom_point() + geom_density_2d() + facet_wrap(~ Species)

  

5. Adjust bandwidth

RSmoother / sharper
ggplot(faithful, aes(eruptions, waiting)) + geom_density_2d(h = c(0.5, 5))

  
Key Insight
geom_density_2d is the kernel-density VERSION of geom_contour. geom_contour needs pre-computed z; geom_density_2d estimates density from raw 2D points first, then draws contours.

geom_density2d() vs geom_hex() vs geom_contour()

Function Input Output
geom_density_2d() Raw 2D points Smooth contour lines
geom_density_2d_filled() Raw 2D points Filled bands
geom_hex() Raw 2D points Hex-bin counts
geom_contour() (x, y, z) grid Contours from given z

When to use which:

  • density_2d for smooth density lines.
  • hex / bin2d for count-based bins.
  • contour when z is already a grid.

A practical workflow

Use density contours to reveal cluster structure in dense scatter.

RInteractive R
ggplot(diamonds, aes(carat, price)) + geom_point(alpha = 0.1) + geom_density_2d(color = "white") + scale_y_log10()

  

Two clear density modes (small vs large diamonds) hidden in the overplot become obvious with contours.

Common pitfalls

Pitfall 1: too few points. Density estimation needs many points. With <30, contours look jagged or empty. Use geom_point alone for sparse data.

Pitfall 2: bandwidth choice. Default bandwidth is auto-estimated. For multimodal data, the auto choice may oversmooth. Set h = c(bw_x, bw_y) manually if needed.

Warning
Note the underscore: geom_density_2d, not geom_density2d. Both work as aliases in current ggplot2, but the underscore form is canonical.

Try it yourself

Try it: Plot density contours over a scatter of mtcars wt vs mpg. Save to ex_plot.

RYour turn: density contours
ex_plot <- mtcars |> ggplot(aes(wt, mpg)) + # your code here

  
Click to reveal solution
RSolution
ex_plot <- ggplot(mtcars, aes(wt, mpg)) + geom_point(alpha = 0.6) + geom_density_2d(color = "steelblue")

  

Explanation: Scatter plus density contour shows clusters in the wt-mpg plane.

After mastering geom_density_2d, look at:

  • geom_density_2d_filled(): filled bands
  • geom_hex() / geom_bin2d(): bin-based density
  • geom_contour(): contours from given z grid
  • stat_density_2d(): same stat, different geom
  • MASS::kde2d(): underlying density estimation

FAQ

What does geom_density_2d do in ggplot2?

geom_density_2d() estimates 2D kernel density from raw points and draws contour lines of equal density.

What is the difference between geom_density_2d and geom_contour?

geom_density_2d estimates density from raw points. geom_contour needs a pre-computed (x, y, z) grid.

How do I get filled density bands?

Use geom_density_2d_filled(). It uses the same density estimate but fills between contours.

How do I control the number of contour lines?

Pass bins = N. Default is 10. Smaller for fewer levels.

Is there a "geom_density2d" without underscore?

Both work. geom_density_2d (with underscore) is canonical; geom_density2d is an alias.