Custom tick marks with R's base graphics system

August 29, 2020

  R ticks axes
  graphics

Kevin Cazelles

Willian Vieira

     

Context

If you are using R’s base graphics system for your plots and if you like customizing your plots, you may have already wondered how to custom the tick marks of your plots! I do that quite a lot and I thought it would be worth explaining how I do so. Let’s consider the following plot,

1
2
3
cx <- seq(0, 2, 0.1)
cy <- cx + .5 * rnorm(length(cx))
plot(cx, cy)

By default, plot.default internally has its way to decide where tick marks should be added. It is always a good default choice, but sometimes not the one you’re looking for. Fortunately, the core package graphics includes all what you need to custom the tick marks and so, without further ado, let’s custom our ticks!

Remove axes and add them back

The first step is to remove all axes. There are basically two ways. One option is to use xaxt = "n" and yaxt = "n" to selectively remove the x-axis and the y-axis, respectively.

1
plot(cx, cy, xaxt = "n")
1
plot(cx, cy, xaxt = "n", yaxt = "n")

The second option is to set axes to FALSE in plot():

1
plot(cx, cy, axes = FALSE)

As you can see, when axes = FALSE the box is also removed and you can actually add it back with box():

1
2
plot(cx, cy, axes = FALSE)
box()

and change its style, if desired:

1
2
plot(cx, cy, axes = FALSE)
box(bty = "l")

That being said, let’s only remove the x-axis for the moment and add ticks at 0, 0.5, 1, 1.5 and 2 to the x-axis using axis():

1
2
plot(cx, cy, xaxt = "n")
axis(1, at = seq(0, 2, .5))

I can easily change the labels if values on the axis are not the ones that should be displayed, e.g.

1
2
plot(cx, cy, xaxt = "n")
axis(1, at = seq(0, 2, .5), labels = letters[1:5])

Second set of tick marks

Now, let’s add a second set tick marks! This can be done by calling axis() one more time.

1
2
3
plot(cx, cy, xaxt = "n")
axis(1, at = seq(0, 2, .5))
axis(1, at = setdiff(cx, seq(0, 2, .5)), labels = NA)

As you may have noticed, I use setdiff() to select the complementary set of ticks. I think it is an efficient of proceeding: I first select the finest gap between two ticks (here 0.1) and create the sequence with seq(), then create the main set of tick marks and finally use setdiff() will to add the remaining tick marks. Also here, as I don’t want to add extra labels, I just set the labels to NA.

Remove the extra line

The main reason why I adjust the tick marks on my plots is because axis() and box() add lines that partially overlap (this is also true when you use the default behavior of plot()): the lines that comes along with the ticks

1
2
3
4
plot(cx, cy, axes = FALSE)
axis(2)
axis(1, at = seq(0, 2, .5), labels = seq(0, 2, .5))
axis(1, at = setdiff(cx, seq(0, 2, .5)), labels = NA)

overlap with the box

1
2
plot(cx, cy, axes = FALSE)
box()

This may frequently goes unnoticed, but I personally tend to notice such overlap this and it annoys me… Anyway, one way to handle this is to set the line width to 0 in axis().

1
2
3
plot(cx, cy, xaxt = "n")
axis(1, at = seq(0, 2, .5), labels = seq(0, 2, .5), lwd = 0)
axis(1, at = setdiff(cx, seq(0, 2, .5)), labels = NA, lwd = 0)

and then to set the line with of the ticks, controlled by lwd.ticks, to something greater than 0

1
2
3
4
plot(cx, cy, xaxt = "n")
axis(1, at = seq(0, 2, .5), labels = seq(0, 2, .5), lwd = 0, lwd.ticks = 1)
axis(1, at = setdiff(cx, seq(0, 2, .5)), labels = NA, lwd = NA, lwd.ticks = 1)
box()

Note that if you only wish to remove the marks you can use tick = FALSE.

1
2
3
4
plot(cx, cy, xaxt = "n")
axis(1, at = seq(0, 2, .5), labels = seq(0, 2, .5), tick = FALSE)
axis(1, at = setdiff(cx, seq(0, 2, .5)), labels = NA, tick = FALSE)
box()

But if you just want to get rid of the extra line, but not the ticks, then you need set lwd to 0 and lwd.ticks to a positive values.

Custom the ticks

Having done the steps above, you may have realized that fine-tuning lwd.ticks is a good way to custom your tick marks!

1
2
3
4
plot(cx, cy, xaxt = "n")
axis(1, at = seq(0, 2, .5), labels = seq(0, 2, .5), lwd = 0, lwd.ticks = 1.5)
axis(1, at = setdiff(cx, seq(0, 2, .5)), labels = NA, lwd = 0, lwd.ticks = .5)
box()

A second parameter to further customize the tick marks is tck that actually belongs to par()

1
2
3
4
5
6
7
8
par(tck = -0.07)
plot(cx, cy, xaxt = "n")
axis(1,
  at = seq(0, 2, .5), labels = seq(0, 2, .5), lwd = 0,
  lwd.ticks = 1
)
axis(1, at = setdiff(cx, seq(0, 2, .5)), labels = NA, lwd.ticks = 1)
box()

but can also be used with axis() thanks to the ellipsis (...) which allow me to change it only for one set of ticks

1
2
3
4
5
6
7
plot(cx, cy, xaxt = "n")
axis(1,
  at = seq(0, 2, .5), labels = seq(0, 2, .5), lwd = 0, tck = -0.07,
  lwd.ticks = 1
)
axis(1, at = setdiff(cx, seq(0, 2, .5)), labels = NA, lwd.ticks = 1)
box()

Moreover, using positive value you can make the tick go up!

1
2
3
4
5
6
7
plot(cx, cy, xaxt = "n")
axis(1,
  at = seq(0, 2, .5), labels = seq(0, 2, .5), lwd = 0, tck = 0.07,
  lwd.ticks = 1
)
axis(1, at = setdiff(cx, seq(0, 2, .5)), labels = NA, lwd.ticks = 1)
box()

And finally you change many aspect of them, including color then and line type:

1
2
3
4
plot(cx, cy, xaxt = "n")
axis(1, at = seq(0, 2, .5), labels = seq(0, 2, .5), lwd = 0, lwd.ticks = 1.5, tck = -.07, col = 2, lty = 2)
axis(1, at = setdiff(cx, seq(0, 2, .5)), labels = NA, lwd.ticks = .5, tck = -.03, col = 3)
box()

One more tip, if you need to adjust the position of the tick you would have to use mgp (also documented in par) which is a vector of three elements controlling the following features:

  1. the position of the axis labels,
  2. the position of the tick labels,
  3. the positon on the tick marks.
1
2
3
4
5
6
7
8
par(mgp = c(2.5, 1.6, 0))
plot(cx, cy, xaxt = "n")
axis(1,
  at = seq(0, 2, .5), labels = seq(0, 2, .5), lwd = 0, lwd.ticks = 1.5,
  tck = -.1, col = 2, lty = 2
)
axis(1, at = setdiff(cx, seq(0, 2, .5)), labels = NA, lwd.ticks = .5, tck = -.03, col = 3)
box()

Note that, just as for tck, I can use mgp in axis(). In this example, it won’t affected the axis labels because they were added by plot().

1
2
3
4
5
6
7
plot(cx, cy, xaxt = "n")
axis(1,
  at = seq(0, 2, .5), labels = seq(0, 2, .5), lwd = 0, lwd.ticks = 1.5,
  tck = -.1, col = 2, lty = 2
)
axis(1, at = setdiff(cx, seq(0, 2, .5)), labels = NA, lwd.ticks = .5, tck = -.03, col = 3, mgp = c(2.5, 1.6, 0))
box()

Wrap all that up in a function

All the steps above may appear overwhelming at first because you need to memorize where is what… But once everything gets clear, you would realize that for most of your plots you need to tweak the same parameters and so that you can create your own function that would cover your needs. For instance, I often use a function similar to the one below:

1
2
3
4
5
6
7
myaxis <- function(side, all, main, lab = main, col1 = 1, col2 = 1, ...) {
  axis(side, at = main, labels = lab, lwd = 0, lwd.ticks = 1, col = col1, ...)
  axis(side,
    at = setdiff(all, main), labels = NA, lwd.ticks = .75, tck = -.025,
    col = col2, ...
  )
}

which basically makes the customization of tick marks very easy!

1
2
3
plot(cx, cy, xaxt = "n", yaxt = "n")
myaxis(1, cx, seq(0, 2, .5))
myaxis(2, seq(-0.5, 2.8, .1), seq(-0.5, 2.5, .5), las = 1)

That’s all folks 😄!

Display information relative to the R session used to render this post.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
sessionInfo()
#R>  R version 4.4.2 (2024-10-31)
#R>  Platform: x86_64-pc-linux-gnu
#R>  Running under: Ubuntu 22.04.5 LTS
#R>  
#R>  Matrix products: default
#R>  BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#R>  LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.20.so;  LAPACK version 3.10.0
#R>  
#R>  locale:
#R>   [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8        LC_COLLATE=C.UTF-8    
#R>   [5] LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8    LC_PAPER=C.UTF-8       LC_NAME=C             
#R>   [9] LC_ADDRESS=C           LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   
#R>  
#R>  time zone: UTC
#R>  tzcode source: system (glibc)
#R>  
#R>  attached base packages:
#R>  [1] stats     graphics  grDevices utils     datasets  methods   base     
#R>  
#R>  other attached packages:
#R>  [1] inSilecoRef_0.1.1
#R>  
#R>  loaded via a namespace (and not attached):
#R>   [1] sass_0.4.9        generics_0.1.3    xml2_1.3.6        blogdown_1.19     stringi_1.8.4    
#R>   [6] httpcode_0.3.0    digest_0.6.37     magrittr_2.0.3    evaluate_1.0.1    bookdown_0.41    
#R>  [11] fastmap_1.2.0     plyr_1.8.9        jsonlite_1.8.9    backports_1.5.0   crul_1.5.0       
#R>  [16] promises_1.3.2    bibtex_0.5.1      jquerylib_0.1.4   cli_3.6.3         shiny_1.10.0     
#R>  [21] rlang_1.1.4       cachem_1.1.0      yaml_2.3.10       tools_4.4.2       dplyr_1.1.4      
#R>  [26] httpuv_1.6.15     DT_0.33           rcrossref_1.2.0   curl_6.0.1        vctrs_0.6.5      
#R>  [31] R6_2.5.1          mime_0.12         lifecycle_1.0.4   stringr_1.5.1     fs_1.6.5         
#R>  [36] htmlwidgets_1.6.4 miniUI_0.1.1.1    pkgconfig_2.0.3   pillar_1.10.0     bslib_0.8.0      
#R>  [41] later_1.4.1       glue_1.8.0        Rcpp_1.0.13-1     xfun_0.49         tibble_3.2.1     
#R>  [46] tidyselect_1.2.1  knitr_1.49        xtable_1.8-4      htmltools_0.5.8.1 rmarkdown_2.29   
#R>  [51] compiler_4.4.2