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.
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()
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()
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()
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 carcoord_cartesian(ylim =c(10, 25)) +labs(x ="Horsepower", y ="Miles per gallon",title ="coord_cartesian keeps all points in the fit") +theme_minimal()
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()
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()
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()
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()andscale_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()
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()
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()
Transform the coordinate space with coord_trans()
coord_trans(x = ..., y = ...) transforms the coordinate systemafter 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()
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()
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
NoteWhat’s the difference between coord_cartesian() and xlim()/ylim() in ggplot2?
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.
NoteWhy does my ggplot regression line change when I set the y-axis limits?
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.
NoteHow do I put a ggplot2 axis on a log scale?
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).
NoteHow do I add log tick marks in ggplot2?
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.
NoteHow do I make a ggplot2 axis start at zero?
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.
NoteHow do I reverse the direction of an axis in ggplot2?
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
ImportantExercise: put the x-axis on a log10 scale
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.
This lesson is reproducible: the figures are executed at build time (if they render, the code works), and the sandbox + quiz re-run live in your browser. The runtime is the judge.