HOWTO · R

How to Change ggplot2 Axis Tick Labels in R

Change ggplot2 axis tick labels with discrete mappings, continuous breaks and formatters, and readable styling for crowded labels.

On this page

Use scale_x_discrete(labels = ...) to rename factor or character ticks, and use scale_x_continuous(breaks = ..., labels = ...) to position and format numeric ticks. Styling such as rotation belongs in theme(axis.text.x = element_text(...)). To change the axis title instead, use labs(x = ...); the title is not a tick label. Replace x with y in these functions to apply the same ideas to the vertical axis.

The examples require R and ggplot2. The continuous example also uses the scales package for a label formatter. They were run with R 4.1.2, ggplot2 3.3.5, and scales 1.1.1. The scale arguments used here also exist in current ggplot2, although a locally installed version may produce small theme differences.

Task ggplot2 control
Change the axis title labs(x = "Title") or labs(y = "Title")
Rename factor or character ticks scale_x_discrete(labels = ...)
Set numeric tick positions and text scale_x_continuous(breaks = ..., labels = ...)
Rotate or style displayed tick text theme(axis.text.x = element_text(...))
Format a Date axis scale_x_date(breaks = ..., labels = ...)

Create a Baseline ggplot2 Chart

Start with one small data frame so the effect of each change is easy to see. The plan column is a factor, so ggplot2 gives it a discrete x scale. The orders column is numeric and therefore uses a continuous y scale.

library(ggplot2)

plan_sales <- data.frame(
  plan = factor(
    c("starter", "standard", "premium", "enterprise"),
    levels = c("starter", "standard", "premium", "enterprise")
  ),
  orders = c(180, 250, 210, 140)
)

base_plot <- ggplot(plan_sales, aes(x = plan, y = orders)) +
  geom_col(fill = "#2c7fb8", width = 0.7) +
  labs(x = "Plan", y = "Orders") +
  theme_minimal(base_size = 16)

base_plot
ggsave(
  "ggplot-default-axis-tick-labels.png",
  plot = base_plot, width = 7.4, height = 4.82, dpi = 100, bg = "white"
)

Default ggplot2 chart with unmodified x- and y-axis tick labels.

The baseline shows the four stored factor values on the x-axis and automatically chosen numeric breaks on the y-axis. labs(x = "Plan") supplies the x-axis title; it does not rewrite starter, standard, or any other tick.

Before choosing a scale, inspect the mapped variable rather than guessing from how its values look in the chart. str(plan_sales$plan) reports a factor, while str(plan_sales$orders) reports a numeric vector. This matters when category codes happen to contain digits: a factor containing "1", "2", and "3" is still discrete. Conversely, a numeric column remains continuous even when the current data contains only a few unique values. Choose a different type only when that change represents what the values mean.

Rename and Order Discrete Tick Labels

For a discrete scale, pair breaks with labels. breaks identifies the underlying data values whose ticks should appear, while labels supplies the text printed for those values. Pairing them explicitly prevents a label from being attached to the wrong tick if the scale order later changes.

The following example also demonstrates safe category ordering. It changes the factor levels in a copy of the data, then passes the same sequence as breaks. A named label vector makes the mapping readable even before the plot is run.

plan_order <- c("premium", "standard", "starter", "enterprise")
plan_names <- c(
  premium = "Premium",
  standard = "Standard",
  starter = "Starter",
  enterprise = "Enterprise"
)

ordered_sales <- plan_sales
ordered_sales$plan <- factor(ordered_sales$plan, levels = plan_order)

discrete_plot <- ggplot(ordered_sales, aes(x = plan, y = orders)) +
  geom_col(fill = "#2c7fb8", width = 0.7) +
  scale_x_discrete(
    breaks = plan_order,
    labels = plan_names[plan_order]
  ) +
  labs(x = "Plan", y = "Orders") +
  theme_minimal(base_size = 16)

discrete_plot
ggsave(
  "ggplot-discrete-axis-tick-labels.png",
  plot = discrete_plot, width = 7.4, height = 4.82, dpi = 100, bg = "white"
)

ggplot2 chart with discrete x-axis categories renamed and ordered.

The displayed sequence is Premium, Standard, Starter, Enterprise. The data values remain lowercase; only the presentation changes. A label function is useful when one rule applies to every tick. For example, scale_x_discrete(labels = abbreviate) passes all break values to base R’s abbreviate() function. A custom function(x) may return any character vector of the same length.

If you only need new names and want to retain the existing order, omit breaks and pass a named vector that covers every displayed value, such as c(starter = "Starter", standard = "Standard", premium = "Premium", enterprise = "Enterprise"). The explicit paired form above is preferable when both the displayed set and its order are requirements. After filtering data, confirm the result by reading the x-axis from left to right; the expected sequence is stated beside the image so the check does not depend on color or chart styling.

Do not treat breaks and limits as interchangeable. On a discrete scale, breaks selects displayed ticks without changing which categories the scale accepts. limits defines the allowed values and their order. Values outside those limits are converted to missing values, and a layer such as geom_col() can then remove the corresponding rows with a warning. Releveling the factor is clearer when ordering is a data decision; use limits only when excluding values is intentional.

Set and Format Continuous Axis Ticks

A numeric axis needs a continuous scale. Supply the exact positions through breaks, then supply either matching character labels or a formatter function through labels. A formatter is safer when values might change because ggplot2 calls it with the final break values.

This scatter plot sets horsepower ticks at 100, 200, and 300 and uses scales::label_number() to append a unit. Calling the formatter directly provides a text check of the labels before drawing the chart.

hp_breaks <- c(100, 200, 300)
hp_formatter <- scales::label_number(suffix = " hp", accuracy = 1)
hp_formatter(hp_breaks)

continuous_plot <- ggplot(mtcars, aes(x = hp, y = mpg)) +
  geom_point(size = 3, color = "#d95f0e") +
  scale_x_continuous(
    breaks = hp_breaks,
    labels = hp_formatter
  ) +
  labs(x = "Engine power", y = "Fuel economy (mpg)") +
  theme_minimal(base_size = 16)

continuous_plot
ggsave(
  "ggplot-continuous-axis-tick-labels.png",
  plot = continuous_plot, width = 7.4, height = 4.82, dpi = 100, bg = "white"
)
[1] "100 hp" "200 hp" "300 hp"

ggplot2 chart with custom continuous-axis breaks and formatted tick labels.

The chart should show exactly those three labeled x positions. Use the equivalent scale_y_continuous() call for a numeric y-axis. Other formatters in scales handle commas, percentages, currency, and similar display tasks; the data stays numeric because formatting affects only label text.

Exact breaks are appropriate when the positions themselves carry meaning, as these horsepower values do. If only formatting matters, omit breaks and let ggplot2 choose positions from the data range, then pass only the formatter to labels. Do not preconvert the numeric column to strings to add a suffix: doing so changes the scale type and can produce lexicographic ordering rather than numeric spacing. The formatter keeps calculations and point positions numeric while changing only what readers see.

Date values are a separate scale type. When a mapped column inherits from Date, use scale_x_date() or its y-axis equivalent rather than forcing it through a numeric or discrete scale. The same breaks-and-labels principle applies, with date-aware break specifications and formatters.

Rotate Crowded Tick Labels

Changing label content and styling label text solve different problems. Keep the scale when the text is correct but overlaps, and adjust axis.text.x in the theme. Here, angle = 35 rotates each label, while hjust = 1 aligns its right edge with its tick. A larger base text size keeps the result readable when the chart is narrowed.

channel_sales <- data.frame(
  channel = c(
    "North America Online",
    "Europe Retail Stores",
    "Asia Pacific Partners",
    "Latin America Direct"
  ),
  orders = c(260, 220, 195, 150)
)

rotated_plot <- ggplot(channel_sales, aes(x = channel, y = orders)) +
  geom_col(fill = "#31a354", width = 0.65) +
  labs(x = "Sales channel", y = "Orders") +
  theme_minimal(base_size = 18) +
  theme(
    axis.text.x = element_text(angle = 35, hjust = 1, vjust = 1)
  )

rotated_plot
ggsave(
  "ggplot-rotated-axis-tick-labels.png",
  plot = rotated_plot, width = 7.4, height = 4.82, dpi = 100, bg = "white"
)

Crowded ggplot2 x-axis labels rotated and aligned so they do not overlap.

Rotation preserves every category name. If the labels still collide, shortening the wording or making the chart wider usually communicates better than using a steeper angle. In the verified ggplot2 3.3.5 environment, scale_x_discrete(guide = guide_axis(n.dodge = 2)) staggers discrete labels over two rows; the function is also present in the current ggplot2 API. Dodging is useful when horizontal text matters, but it consumes more vertical space. See how to rotate axis labels in base R or ggplot2 for more rotation choices.

Hide Labels, Tick Marks, or Breaks Deliberately

Choose the object you want to remove instead of using these controls as if they were synonyms. The following three plot objects demonstrate the differences:

control_plot <- base_plot +
  theme_classic(base_size = 16)

labels_hidden <- control_plot +
  theme(axis.text.x = element_blank())

ticks_hidden <- control_plot +
  theme(axis.ticks.x = element_blank())

breaks_hidden <- control_plot +
  scale_x_discrete(breaks = NULL)

control_plot uses theme_classic(), which draws short tick marks, so each change has an observable baseline. labels_hidden removes only the printed x-axis tick text. ticks_hidden removes only the short tick marks, so the category names remain. breaks_hidden tells the scale to produce no major x breaks; the labels and tick marks anchored to those breaks disappear. None of these expressions filters rows from plan_sales.

Another scale-level option is labels = NULL, which suppresses labels while retaining the break positions. Prefer that when the labels should be absent by definition of the scale. Prefer element_blank() when the scale remains meaningful but a particular chart theme should not draw the text. Hiding categorical labels can make bars or points impossible to identify, so provide direct annotations or another key when the category still matters.

The three changed objects are intentionally not saved as images because their behavior is easier to compare from the selectable code and precise description. To verify one interactively, print it in the R console—for example, enter ticks_hidden—and compare it with control_plot. The bars and their heights should remain unchanged in all three cases; only axis guides differ. If the geometry changes or a removed-row warning appears, inspect any limits call elsewhere in the plot rather than the theme settings shown here.

Troubleshoot Incorrect or Missing Tick Labels

If ggplot2 reports that a continuous value was supplied to a discrete scale, or the reverse, inspect the mapped column with str() before changing the plot. Factor and character columns use discrete scales; numeric columns use continuous scales; Date and date-time objects use their corresponding date scales. Convert the data only when the converted type matches its real meaning, not merely to silence an error.

When using a character vector for labels, always pair it with explicit breaks and keep both vectors the same length. Otherwise labels may shift after filtering, reordering, or adding categories. Named mappings, as in plan_names, document which displayed name belongs to each stored value. A function avoids the length problem when one transformation applies to every break.

Missing bars or warnings about removed rows often point to limits, not labels. Check whether the scale limits omit a value that exists in the data. Use breaks if the goal is only to reduce the number of visible tick labels. For intentional zooming or range control, follow the separate guide to set ggplot2 axis limits without confusing them with tick breaks.

Finally, check whether the text being changed is actually the axis title. labs(x = "Plan") and the name argument of an x scale set the title. The labels argument controls tick text. Keeping those roles separate prevents a common situation in which the title changes correctly but the category or number labels appear untouched.

For most plots, choose the scale from the mapped data type, use paired breaks and labels when exact text matters, and reserve theme() for appearance. This separation makes axis changes predictable and keeps the underlying observations intact.