ggplot2 Axis Limits & Scales in R

Zoom, set min/max, log-transform, and reverse an axis — live in your browser

Data Visualization

Control ggplot2 axes in R — zoom with coord_cartesian(), set limits with xlim()/ylim() and scale_*_continuous(), force the origin with expand_limits(), apply log10, sqrt, or reversed scales, and add log tick marks with annotation_logticks(). Edit and run the code live, no install needed.

Published

June 20, 2026

Modified

July 12, 2026

TipKey takeaways
  • Zoom without losing data: coord_cartesian(xlim = ..., ylim = ...) crops the view — the points outside still shape the fit. This is the preferred way to set limits.
  • Clip the data: xlim(), ylim(), and scale_*_continuous(limits = ...) drop out-of-range points (they become NA), which changes any smooth or summary.
  • Force the origin: expand_limits(x = 0, y = 0) extends the range so an axis starts at zero.
  • Log scale: scale_y_log10() (or transform = "log10") is the big-demand transform; scale_*_sqrt() is its milder cousin.
  • Log tick marks: annotation_logticks(sides = "bl") adds the minor decade ticks (superseded by guide_axis_logticks()).
  • Reverse a direction: scale_y_reverse() flips an axis high-to-low without flipping the whole plot.
  • Transform the space, keep the data: coord_trans() warps the coordinate system after the geom is computed — no points dropped.
  • Format tick labels for scaled axes with the scales package (label_log(), label_dollar(), label_percent()).
  • Every plot below is rendered for you — then tweak any example live in the sandbox.
Get the book — GGPlot2 Essentials (PDF)

Introduction

Once a ggplot2 plot is drawn, the next question is almost always about the axes: how do I zoom in, set a minimum and maximum, start the y-axis at zero, or switch to a log scale? In R, ggplot2 gives you two distinct levers — limits (the visible range) and scales/coordinates (how values map to position) — and the difference between them is the single most common source of confusion. This lesson works through both, with figures rendered right here that you can re-run or edit live.

The key distinction to hold onto: some functions clip (drop data outside the range, which can change a trend line or summary) while others only zoom (crop the view, keeping every point in the computation). We flag which is which at each step.

We use the built-in mtcars dataset — fuel economy and design for 32 cars from a 1974 magazine: wt (weight in 1000 lbs), mpg (miles per gallon), hp (gross horsepower), and disp (displacement). Its wide numeric ranges make it ideal for demonstrating axis limits and transformations.

Zoom with coord_cartesian() — limits without clipping

coord_cartesian(xlim = ..., ylim = ...) is the function to reach for first. It sets the visible window without removing any data: every point still contributes to smooths, regressions, and summaries — you are only cropping what’s drawn. Here we zoom into mid-range cars while the trend line is still fitted on all 32.

library(ggplot2)
ggplot(mtcars, aes(x = wt, y = mpg)) +
  geom_point(color = "#3a86d4", size = 2) +
  geom_smooth(method = "lm", se = FALSE, color = "grey30") +
  coord_cartesian(xlim = c(2.5, 5), ylim = c(10, 25)) +
  labs(x = "Weight (1000 lbs)", y = "Miles per gallon") +
  theme_minimal()

ggplot2 scatter plot of car weight versus mileage, zoomed with coord_cartesian to weights 2.5 to 5 and mileage 10 to 25, with a smooth fitted on all data.

Quick min/max with xlim() and ylim()

xlim() and ylim() are the shortcut for setting a range, but they clip: any point outside the range is set to NA and dropped (ggplot2 warns about removed rows). That’s fine for a pure scatter, but it also means a fitted line is computed only on the surviving points — so reach for these when you genuinely want to exclude data, not merely zoom.

library(ggplot2)
ggplot(mtcars, aes(x = wt, y = mpg)) +
  geom_point(color = "#3a86d4", size = 2) +
  xlim(2, 5) +
  ylim(10, 30) +
  labs(x = "Weight (1000 lbs)", y = "Miles per gallon") +
  theme_minimal()

ggplot2 scatter plot of car weight versus mileage with xlim 2 to 5 and ylim 10 to 30 applied, dropping points outside the range.

Limits via scale_*_continuous()

scale_x_continuous(limits = ...) and scale_y_continuous(limits = ...) set the same clipping limits as xlim()/ylim(), but in the scale layer — so you can pin the range and rename the axis or set custom breaks in one place. Like xlim(), this clips out-of-range data.

library(ggplot2)
ggplot(mtcars, aes(x = wt, y = mpg)) +
  geom_point(color = "#3a86d4", size = 2) +
  scale_y_continuous(name = "Miles per gallon", limits = c(10, 35),
                     breaks = seq(10, 35, by = 5)) +
  labs(x = "Weight (1000 lbs)") +
  theme_minimal()

ggplot2 scatter plot of car weight versus mileage with y-axis limits 10 to 35 and custom breaks set through scale_y_continuous.

Clip vs zoom: the distinction that bites

The two approaches above look similar but behave differently — and it matters whenever a layer is computed from the data (a smooth, a boxplot, a density). ylim()/scale_y_continuous() drop out-of-range points before the geom is computed; coord_cartesian() keeps them and only crops the view. The same regression line, fitted under each rule, can differ visibly:

library(ggplot2)
ggplot(mtcars, aes(x = hp, y = mpg)) +
  geom_point(color = "#3a86d4", size = 2) +
  geom_smooth(method = "lm", se = FALSE, color = "grey30") +
  # coord_cartesian zooms: the line is still fitted on every car
  coord_cartesian(ylim = c(10, 25)) +
  labs(x = "Horsepower", y = "Miles per gallon",
       title = "coord_cartesian keeps all points in the fit") +
  theme_minimal()

ggplot2 scatter plot of horsepower versus mileage with a linear fit, the y-axis zoomed to 10 to 25 using coord_cartesian so all points still drive the line.

Swap the coord_cartesian(ylim = ...) line for ylim(10, 25) in the sandbox below and watch the slope change — that’s the clip in action.

Force the origin with expand_limits()

By default ggplot2 fits the axes snugly around your data. To make an axis include zero (a common request for honest bar and area charts) or any other reference value, add expand_limits(). It extends the range to cover the value without clipping anything.

library(ggplot2)
ggplot(mtcars, aes(x = wt, y = mpg)) +
  geom_point(color = "#3a86d4", size = 2) +
  expand_limits(x = 0, y = 0) +
  labs(x = "Weight (1000 lbs)", y = "Miles per gallon") +
  theme_minimal()

ggplot2 scatter plot of car weight versus mileage with both axes expanded to include the origin at zero via expand_limits.

Log10 axis — the “ggplot log scale” demand

A log scale spreads out small values and compresses large ones — essential when a variable spans orders of magnitude. The clean way in ggplot2 is scale_y_log10() (or, equivalently, scale_y_continuous(transform = "log10")). Here car displacement is log-transformed on the x-axis.

library(ggplot2)
ggplot(mtcars, aes(x = disp, y = mpg)) +
  geom_point(color = "#3a86d4", size = 2) +
  scale_x_log10() +
  labs(x = "Displacement (log10, cubic inches)", y = "Miles per gallon") +
  theme_minimal()

ggplot2 scatter plot of engine displacement versus mileage with a base-10 logarithmic x-axis applied through scale_x_log10.

The transform = argument of any continuous scale accepts the same transforms by name ("log10", "log2", "sqrt", "reverse", …), so scale_x_continuous(transform = "log10") does exactly what scale_x_log10() does — handy when you also want custom breaks or a name on the same scale.

Log tick marks with annotation_logticks()

Between the labelled decades of a log axis, the minor log ticks — bunched near each power of ten, thinning out toward the next — help the eye read intermediate values (20, 30, 50…) on a scale whose gridlines only mark 10, 100, 1000. Add them with annotation_logticks(). It must pair with a log10 scale, because the ticks are placed in log space — on a plain linear axis they’d land at the wrong spots.

library(ggplot2)
ggplot(mtcars, aes(x = disp, y = mpg)) +
  geom_point(color = "#3a86d4", size = 2) +
  scale_x_log10() +
  annotation_logticks(sides = "b") +
  labs(x = "Displacement (log10, cubic inches)", y = "Miles per gallon") +
  theme_minimal()

ggplot2 scatter plot of engine displacement versus mileage on a log10 x-axis, with minor log tick marks added along the bottom axis by annotation_logticks, denser near each power of ten.

The sides = argument picks which axes get ticks: a string built from "t" (top), "r" (right), "b" (bottom), and "l" (left) — so "b" is the bottom only, "bl" is bottom and left, and "trbl" is all four. Only put ticks on a side whose axis is actually log-scaled: sides = "bl" needs both scale_x_log10() and scale_y_log10(), or the ticks on the untransformed axis sit at meaningless positions.

In current ggplot2, annotation_logticks() is superseded by guide_axis_logticks() — an axis guide that draws the same log ticks through the scale itself rather than as a separate annotation layer. You pass it to the scale’s guide = argument. It’s the recommended approach for new code; annotation_logticks() still works and stays the form most search results (and existing scripts) show.

library(ggplot2)
ggplot(mtcars, aes(x = disp, y = mpg)) +
  geom_point(color = "#3a86d4", size = 2) +
  scale_x_log10(guide = guide_axis_logticks()) +
  labs(x = "Displacement (log10, cubic inches)", y = "Miles per gallon") +
  theme_minimal()

ggplot2 scatter plot of engine displacement versus mileage on a log10 x-axis, with log tick marks placed by the modern guide_axis_logticks axis guide passed to the scale.

Square-root scale

scale_x_sqrt() / scale_y_sqrt() apply a milder, square-root compression — useful for right-skewed counts where a full log feels too aggressive.

library(ggplot2)
ggplot(mtcars, aes(x = hp, y = mpg)) +
  geom_point(color = "#3a86d4", size = 2) +
  scale_x_sqrt() +
  labs(x = "Horsepower (sqrt)", y = "Miles per gallon") +
  theme_minimal()

ggplot2 scatter plot of horsepower versus mileage with a square-root transformed x-axis applied through scale_x_sqrt.

Reverse an axis

To run an axis high-to-low — counting down a rank, or putting the largest value at the origin — use scale_x_reverse() / scale_y_reverse(). This reverses one scale’s direction; it does not swap the x and y roles (that’s coord_flip(), a separate idiom — see the related lessons).

library(ggplot2)
ggplot(mtcars, aes(x = wt, y = mpg)) +
  geom_point(color = "#3a86d4", size = 2) +
  scale_x_reverse() +
  labs(x = "Weight (1000 lbs, reversed)", y = "Miles per gallon") +
  theme_minimal()

ggplot2 scatter plot of car weight versus mileage with the x-axis reversed so heavier cars sit on the left.

Transform the coordinate space with coord_trans()

coord_trans(x = ..., y = ...) transforms the coordinate system after the geoms are computed, rather than transforming the data first. The practical difference from a scale_*_log10(): the points and any fitted line are calculated on the original linear scale, then the space is warped — so straight lines bend, and no data is dropped. Useful when you want a transformed display but a model fit on the raw values.

library(ggplot2)
ggplot(mtcars, aes(x = disp, y = mpg)) +
  geom_point(color = "#3a86d4", size = 2) +
  coord_trans(x = "log10") +
  labs(x = "Displacement (log10 space)", y = "Miles per gallon") +
  theme_minimal()

ggplot2 scatter plot of engine displacement versus mileage with the x coordinate space log10-transformed via coord_trans, keeping all data.

Format the tick labels of a scaled axis

When you log- or otherwise transform an axis, the raw tick numbers can read awkwardly. The scales package supplies ready-made formatters you pass to a scale’s labels = argument — label_log() for clean log exponents, label_dollar(), label_percent(), and label_comma() for thousands separators. Here we log the x-axis and label it with tidy exponents.

library(ggplot2)
ggplot(mtcars, aes(x = disp, y = mpg)) +
  geom_point(color = "#3a86d4", size = 2) +
  scale_x_log10(labels = scales::label_log()) +
  labs(x = "Displacement (cubic inches, log10)", y = "Miles per gallon") +
  theme_minimal()

ggplot2 scatter plot of displacement versus mileage on a log10 x-axis with tick labels formatted as clean log exponents via scales label_log.

For a base-2 log axis — common for log2 fold-changes in genomics — pass transform = "log2" and format the ticks with scales::label_log(base = 2); everything else is identical to the log10 example above.

Try it live

The plots above were rendered at build time. Want to experiment? Edit the code and press Run — it executes in your browser via webR (no server, no install). Try swapping coord_cartesian(ylim = c(10, 25)) for ylim(10, 25) to see clip vs zoom change the line.

Working in Python? A matplotlib axis-limits and log-scale guide (set_xlim, set_yscale) is coming to the Python series.

🟢 With an AI agent

Ask Prova “should I use coord_cartesian() or ylim() to zoom this regression — which one keeps the slope honest?” — it answers with code you can run, so the explanation is reproducible. The runtime is the judge. Ask Prova →

Frequently asked questions

coord_cartesian(xlim = ..., ylim = ...) only zooms — it crops the view while every point still contributes to smooths and summaries. xlim()/ylim() (and scale_*_continuous(limits = ...)) clip — they set out-of-range points to NA and drop them before a layer is computed, which can change a fitted line. Use coord_cartesian() to zoom, xlim()/ylim() only when you truly want to exclude data.

Because ylim() and scale_y_continuous(limits = ...) clip the data: out-of-range points are dropped before geom_smooth() fits, so the line is computed on fewer points and its slope shifts. To keep all points in the fit and only crop the view, use coord_cartesian(ylim = ...) instead.

Add scale_x_log10() or scale_y_log10() for a base-10 log axis. Equivalently, pass transform = "log10" to scale_x_continuous()/scale_y_continuous() — handy when you also want custom breaks or a name on the same scale ("log2" and "sqrt" work the same way).

Add annotation_logticks() to a plot that already uses a log10 scale, and choose the axes with sides = — e.g. scale_x_log10() + annotation_logticks(sides = "b") for bottom ticks, "bl" for bottom and left, "trbl" for all four. The ticks are placed in log space, so they only make sense on a log-scaled axis. In current ggplot2 the modern equivalent is scale_x_log10(guide = guide_axis_logticks()), which annotation_logticks() is superseded by.

Add expand_limits(x = 0, y = 0) (or just the axis you need, e.g. expand_limits(y = 0)). It extends the range to include zero without clipping any data — useful for honest bar and area charts.

Use scale_x_reverse() or scale_y_reverse() to run that one axis high-to-low. This reverses only the scale’s direction; it does not swap the x and y roles — that’s coord_flip(), a separate idiom.

Test your understanding

Complete the code so engine displacement (disp) is drawn on a base-10 logarithmic x-axis.

# Use scale_x_log10() — or equivalently scale_x_continuous(transform = "log10")
library(ggplot2) ggplot(mtcars, aes(x = disp, y = mpg)) + geom_point(color = "#3a86d4", size = 2) + scale_x_log10() + labs(x = "Displacement (log10)", y = "Miles per gallon") + theme_minimal()
library(ggplot2)
ggplot(mtcars, aes(x = disp, y = mpg)) +
  geom_point(color = "#3a86d4", size = 2) +
  scale_x_log10() +
  labs(x = "Displacement (log10)", y = "Miles per gallon") +
  theme_minimal()

Conclusion

You learned to control ggplot2 axes two ways — limits and scales — and, crucially, when each clips data versus only zooms the view. You zoomed with coord_cartesian(), set ranges with xlim()/ylim() and scale_*_continuous(limits = ), forced the origin with expand_limits(), applied log10, sqrt, and reversed scales, added minor log ticks with annotation_logticks(), warped the space with coord_trans(), and tidied tick labels with the scales package. Next, set the breaks and rotate tick text in the axis ticks lesson, or explore the scatter plot these examples build on.

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {Ggplot2 {Axis} {Limits} \& {Scales} in {R}},
  date = {2026-06-20},
  url = {https://www.datanovia.com/learn/data-visualization/ggplot2/axis-limits-scales},
  langid = {en}
}
For attribution, please cite this work as:
“Ggplot2 Axis Limits & Scales in R.” 2026. June 20. https://www.datanovia.com/learn/data-visualization/ggplot2/axis-limits-scales.