• Skip to primary navigation
  • Skip to main content
  • Skip to primary sidebar

Data Viz with Python and R

Learn to Make Plots in Python and R

  • Home
  • Seaborn
  • Matplotlib
  • ggplot2
  • Altair
  • About
    • Privacy Policy
  • Visualizing Activation Functions in Neural Networks
  • Confusion Matrix Calculator
  • Visualizing Dropout Rate in Neural Network
  • Visualizing Loss Functions in Neural Networks
  • Show Search
Hide Search

How to Remove Space between Legend at Bottom and x-axis in ggplot2

datavizpyr · December 31, 2021 ·

Last updated on August 28, 2025

Goal: This tutorial shows how to control the spacing around the ggplot2 legend—especially when the legend is placed at the bottom—and how to fine-tune multi-row legends. We begin with a reproducible baseline that illustrates the problem, then move through five practical fixes. You’ll learn when to use each parameter (x = NULL, legend.margin, plot.margin, multi-row tuning, and in-panel legends), and how they affect layout. By the end, you’ll have a clean, publication-ready legend layout you can reuse.

📚 Browse the complete ggplot2 tutorial hub

Contents

  1. Baseline: legend at the bottom (the problem)
  2. Method 1 — Use x = NULL (avoid reserving label space)
  3. Method 2 — Pull legend closer with legend.margin
  4. Method 3 — Trim outer plot margin with plot.margin
  5. Method 4 — True multi-row legend tuning
  6. Method 5 — Place legend inside plot
  7. Before/After comparison
  8. Summary: which fix should I use?
  9. FAQs

Baseline: legend at the bottom (the problem)

When you move the legend to the bottom using theme(legend.position = "bottom"), ggplot2 may leave excess white space above the legend. This is especially common if you “remove” the x-axis title by setting x = "", which still reserves label space. The baseline example below reproduces the issue so you can see the starting point clearly. We’ll use a small subset of the TidyTuesday student debt dataset and create a simple side-by-side column chart with a bottom legend.

library(tidyverse)
theme_set(theme_bw(20))

student_debt <- readr::read_csv(
  "https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2021/2021-02-09/student_debt.csv")

debt_df <- student_debt |>
  filter(year %in% c(1989, 2016)) |>
  mutate(year = as.character(year))

debt_df |> head()
year race loan_debt loan_debt_pct
2016	White	11108.4100	0.3367511
2016	Black	14224.7700	0.4183588
2016	Hispanic	7493.9990	0.2189689
1989	White	1100.4070	0.1047123
1989	Black	1160.5680	0.1788198
1989	Hispanic	897.5826	0.1272523
6 rows
p_baseline <- debt_df |>
  ggplot(aes(x = forcats::fct_reorder(race, loan_debt_pct),
             y = loan_debt_pct,
             fill = year)) +
  geom_col(position = "dodge") +
  theme(legend.position = "bottom") +
  labs(x = "", y = "Loan Debt %") +  # x = "" still reserves empty label space
  scale_y_continuous(labels = scales::percent,
                     breaks = scales::pretty_breaks(n = 6))

p_baseline
Barplot with legend at bottom
Barplot with legend at bottom with excess white space between legend and x-axis

Method 1 — Use x = NULL (avoid reserving label space)

If you don’t need an x-axis title, set x = NULL rather than an empty string. With "", ggplot2 treats the label as present but empty and still reserves space, which pushes the legend farther away. NULL tells ggplot2 not to allocate label space at all. This single change often fixes the most obvious gap, and it’s the cleanest solution when your category names already function as the x-axis description.

p_xnull <- debt_df |>
  ggplot(aes(x = forcats::fct_reorder(race, loan_debt_pct),
             y = loan_debt_pct,
             fill = year)) +
  geom_col(position = "dodge") +
  theme(legend.position = "bottom") +
  labs(x = NULL,
       y = "Loan Debt %",
       title=stringr::str_wrap("Reduce Space Between
                               Bottom Legend and X-axis 
                               with x=NULL",
                               width=40)) +  # better than x = ""
  scale_y_continuous(labels = scales::percent,
                     breaks = scales::pretty_breaks(n = 6))

p_xnull
ggsave("reduce_space_between_bottom_legend_and_x_axis_with_xNULL.png")
Use `x = NULL` to avoid reserving space between x-axis and legend
Use `x = NULL` to avoid reserving space between x-axis and legend

Method 2 — Pull legend closer with legend.margin

To control the space between the plotting panel and the legend box, adjust legend.margin. A small negative top margin can “pull” the legend upward when it sits below the panel. Start conservatively (e.g., −8 to −12 points) and tweak until it looks balanced. This parameter affects the gap immediately around the legend box, not the spacing between legend rows or the overall outer plot margins.

p_margin <- debt_df |>
  ggplot(aes(x = forcats::fct_reorder(race, loan_debt_pct),
             y = loan_debt_pct,
             fill = year)) +
  geom_col(position = "dodge") +
  labs(x = NULL,
       y = "Loan Debt %",
       title=stringr::str_wrap("Reduce Space Between 
                               Bottom Legend and X-axis 
                               with legend.margin",
                               width=40)) +
  scale_y_continuous(labels = scales::percent,
                     breaks = scales::pretty_breaks(n = 6)) +
  theme(
    legend.position = "bottom",
    legend.margin   = margin(t = -12)   # try -8, -12, -16 as needed
  )

p_margin
ggsave("pull_legend_closer_to_xaxis_with_legend_margin.png")
Reduce Spacing Between X-axis and Legend using legend.margin
Reduce Spacing Between X-axis and Legend using legend.margin

Method 3 — Trim outer plot margin with plot.margin

Even with a tight legend margin, generous outer plot margins can make the legend appear far away. Use plot.margin to reduce the bottom outer padding, which shortens the overall canvas. This doesn’t change the panel-to-legend gap itself; rather, it brings the entire plot content closer to surrounding elements in your layout, helpful for multi-plot pages or reports with strict vertical space.

p_plotmarg <- debt_df |>
  ggplot(aes(x = forcats::fct_reorder(race, loan_debt_pct),
             y = loan_debt_pct,
             fill = year)) +
  geom_col(position = "dodge") +
  labs(x = NULL, 
       y = "Loan Debt %",
       title=stringr::str_wrap("Reduce Space Between 
                               Bottom Legend and X-axis 
                               with plot.margin",
                               width=40)) +
  scale_y_continuous(labels = scales::percent,
                     breaks = scales::pretty_breaks(n = 6)) +
  theme(
    legend.position = "bottom",
    legend.margin   = margin(t = -8),
    plot.margin     = margin(t = 10, r = 10, b = 5, l = 10) # tighten bottom
  )

p_plotmarg
ggsave("trim_plot_outer_margin_plot_margin.png")
Reduce Space Between Bottom Legend and X-axis with plot.margin
Reduce Space Between Bottom Legend and X-axis with plot.margin

Method 4: Place the legend **inside** the plot (when appropriate)

If your plot allows it, put the legend **inside the plotting area** to remove external spacing entirely. Add a semi-transparent background for readability.

p_inside <- debt_df |>
  ggplot(aes(x = forcats::fct_reorder(race, loan_debt_pct),
             y = loan_debt_pct,
             fill = year)) +
  geom_col(position = "dodge") +
  labs(x = NULL, 
       y = "Loan Debt %",
       title=stringr::str_wrap("Move the legend inside the plot",
                               width=40)) +
  scale_y_continuous(labels = scales::percent,
                     breaks = scales::pretty_breaks(n = 6)) +
  theme(
    legend.position   = c(0.5, 0.05), # centered, near bottom inside panel
    legend.direction  = "horizontal",
    legend.background = element_rect(fill = scales::alpha("white", 0.7), colour = NA),
    plot.margin       = margin(10, 10, 10, 10)
  )

p_inside
ggsave("move_legend_inside_plot_ggplot2.png")
Place the legend **inside** the plot (when appropriate)
Place the legend **inside** the plot (when appropriate)

Method 5 — True multi-row legend tuning

For legends with many keys, the bottom legend can wrap to multiple rows. To make row spacing changes visible, first guarantee multiple rows using guide_legend(nrow = …, byrow = TRUE).

Then control row-to-row distance with legend.spacing.y (requires grid::unit()), and the physical height of each key with legend.key.height.

Remember: legend.margin affects the gap between the legend box and the panel, not the spacing between legend rows.

library(tidyverse)
library(grid)   # for unit()

data(diamonds, package = "ggplot2")


# Force a multi-row legend at bottom
p <- diamonds |> 
  dplyr::sample_n(2500) |>
  ggplot(aes(carat, price,
             color = color)) +
  geom_point(alpha = 0.5, 
             size = 1.2) +
  scale_color_brewer(palette = "Dark2") +
  labs(x = NULL, y = "Price (USD)", 
       color = "Color grade",
       title=stringr::str_wrap("Multi-line legend spacing",
                               width=40)) +
  theme(
    legend.position  = "bottom",
    legend.direction = "horizontal"
  ) +
  guides(
    color = guide_legend(nrow = 2, byrow = TRUE)   # <- force two rows
  )

# BEFORE: default spacing
p
ggsave("multi_line_legend_default_spacing.png")
ggplot2 Multi Line Legend with default Spacing
Multi Line Legend with default excess Spacing
# AFTER A: tighter rows (reduce vertical gap between legend rows)
p + theme(
  legend.spacing.y  = unit(-4, "pt"),  # try -2, -4, -6 for stronger effect
  legend.key.height = unit(10, "pt")   # shrink key height for tighter rows
)
ggsave("reduce_vertical_gap_between_legend_rows.png")
ggplot2 reduce vertical gap between legend rows
How to reduce vertical gap between legend rows
# AFTER B: more outer padding around the whole legend box (not row spacing)
p + theme(
  legend.box = "vertical",
  legend.box.margin = margin(t = 0, r = 0, b = 0, l = 0),
  legend.margin     = margin(t = -15)   # distance between legend and plot
)
ggsave("reduce_distance_between_multi_legend_and_plot.png")

ggplot2 reduce distance between multi line legend and x-axis of the plot
ggplot2 reduce distance between multi line legend and x-axis of the plot

Before/After comparison

Let us visually confirm improvements, by laying out the baseline next to one or more fixes. If you have the patchwork package installed, the code below will render a two-row comparison: baseline vs x = NULL on the first row, and legend/plot margin tweaks on the second row. This side-by-side view helps you choose a combination that balances readability, compactness, and your publication’s spacing constraints.

if (requireNamespace("patchwork", quietly = TRUE)) {
  (p_baseline + p_xnull) / (p_margin + p_plotmarg)
}
Comparison Before and After Reducing Legend Spacing
Comparison Before and After Reducing Legend Spacing

Summary: which fix should I use?

Use x = NULL to avoid accidental label space; it’s the simplest and most reliable first step. If the legend still feels detached, adjust legend.margin to close the gap to the panel. When the entire figure needs tightening, reduce plot.margin. For legends that wrap, tune row spacing with legend.spacing.y and legend.key.height. If layout allows, consider an in-panel legend for maximum compactness without compromising readability.

Method Use when… Key code
Use x = NULL You removed the x-axis label with x = "" and see extra space labs(x = NULL)
legend.margin Legend sits too far from the panel even with x = NULL theme(legend.margin = margin(t = -8))
plot.margin Outer plot margins make the figure look padded overall theme(plot.margin = margin(b = 5))
Multi-row tuning Your bottom legend wraps to multiple rows guide_legend(nrow = 2), legend.spacing.y, legend.key.height
Legend inside plot Compact layouts or dashboard-style visuals legend.position = c(0.5, 0.06)

FAQs

How do I move the legend to the top?

Use theme(legend.position = "top"). If the legend looks too far from the panel, adjust legend.margin. For overall compactness, reduce plot.margin. Remember that moving a long legend to the top may also require adjusting text size or using multiple columns via guide_legend(nrow = ...) to maintain a clean layout.

How do I remove the legend entirely?

Use theme(legend.position = "none") to hide all legends, or selectively suppress specific guides, e.g., guides(fill = "none", color = "none"). Removing the legend makes sense when encodings are self-evident (e.g., labeled facets) or when space is extremely limited. Always verify that the message remains clear for your intended audience.

Why didn’t my multi-row spacing change?

First ensure your legend truly wraps: set guides(... = guide_legend(nrow = 2, byrow = TRUE)). Then adjust legend.spacing.y using grid::unit(), and reduce legend.key.height to make rows physically tighter. Also check that you’re not confusing legend.margin (panel gap) with row spacing. Finally, long labels may require text size tweaks for noticeable results.

What’s the difference between legend.margin and legend.box.margin?

legend.margin controls the space between the legend box and the plotting panel—ideal for closing the “gap” when the legend is at the bottom. legend.box.margin controls the padding around the entire legend block (outside the keys and labels) and is more useful when combining multiple guides or packing legends tightly in multi-row configurations.

Want more layout recipes for publication-ready plots? Explore the ggplot2 tutorial hub for legends, axes, annotations, facets, and themes. Each article includes reproducible code and practical gotchas so you can go from draft to camera-ready quickly.

Explore the Complete ggplot2 Guide

35+ tutorials with code: scatterplots, boxplots, themes, annotations, facets, and more—tested and beginner-friendly.

Visit the ggplot2 Hub → No fluff—just code and visuals.

Related posts:

Customizing Mean mark to boxplot with ggplot2How To Show Mean Value in Boxplots with ggplot2? Scatterplot with marginal multi-histogram with ggExtraHow To Make Scatterplot with Marginal Histograms in R? ggforce geom_circle(): Annotate with a circleHow To Annotate a Plot with Circle in R Default ThumbnailHow to Make Axis Text Bold in ggplot2

Filed Under: ggplot2, R Tagged With: ggplot2 legend.margin, reduce space between legend and x-axis

Primary Sidebar

Python & R Viz Hubs

  • Seaborn Guide & Cookbook
  • ggplot2 Guide & Cookbook
  • Matplotlib Guide & Cookbook
  • Confusion Matrix Calculator
  • Visualizing Activation Functions
  • Visualizing Dropout
  • Visualizing Loss Functions

Buy Me a Coffee

Copyright © 2026 · Daily Dish Pro on Genesis Framework · WordPress · Log in

Go to mobile version