Skip to contents

The problem

The scales package provides a family of easy to use functions like scales::label_number or scales::label_percent to format numbers in decimal format, as percentages or currencies, which could also be passed to the labels argument of ggplot2s family of scale functions to format axes or legends.

However, let’s say you have to prepare a report or charts where you have to use country-specific style conventions to display numbers which differ from the default anglo-american style.

While that could be achieved with the scales package as well, in practice formatting numbers and styling axes quickly becomes cumbersome and annoying as you always have to

  • set the decimal and grouping marks (if deviating from the defaults) so you end up with

    scales::label_number(big.mark = ".", decimal.mark = ",")(x)

    to format a number x in decimal format according to style conventions found in many European countries.

  • provide a suitable labeller function to the labels argument of scale_xxx_yyy so you end up with

    scale_x_continuous(
      labels = scales::label_percent(big.mark = ".", decimal.mark = ",")
    )

    to display numbers on the x axis as percentages using the style convention found in many European countries.

As of scales 1.4.0 (released 2025-04-24), you no longer have to repeat big.mark/decimal.mark on every single call within a session — a new scales::number_options(decimal.mark = ",", big.mark = ".") sets these (and style_positive/style_negative, and currency-specific equivalents) as global defaults for every subsequent label_number()/label_currency() call. That closes part of the gap above for a single country’s conventions applied for the rest of a session — but it’s still one global default at a time. It doesn’t help if you need several countries’ conventions side by side in the same report or chart (like the G20 example in the README), and it doesn’t touch the harder problems countryscales also handles under the hood: correct currency symbol/sign positioning and locale-correct percent-sign placement and spacing.

The solution

A first and simple solution to this problem would be to add some simple wrappers at the beginning of your R script or R markdown document like

my_label_number <- function() {
  scales::label_number(big.mark = ".", decimal.mark = ",")
}

But then you end up copy and pasting from one report to the next, so sooner or later you probably put these helpers inside package.

And that’s the goal of countryscales: Providing out-of-the-box helpers to format numbers using country-specific style conventions.

As a first example consider formatting a number according to style conventions used in Germany and several other European countries where a dot (.) is used as the big mark or grouping mark or thousands separator and a comma (,) as the decimal mark.

Using scales::label_number this requires switching the default decimal and big marks:

library(scales)

label_number(big.mark = ".", decimal.mark = ",", accuracy = .1)(x)
#> [1] "12.345.690,0"

Using countryscales this could be achieved with less typing using countryscales::label_number_de:

library(countryscales)

label_number_de(accuracy = .1)(x)
#> [1] "12.345.690,0"

To provide an example of using countryscales with ggplot2 let’s first prepare a small example dataset of the top 10 countries according to population size:

top10_pop <- gapminder15[order(-gapminder15$pop), c("country", "pop")]
top10_pop$pct <- top10_pop$pop / sum(top10_pop$pop)
top10_pop <- head(top10_pop, 10)

As a basic example let’s make a simple barchart of population size by country

library(ggplot2)

p <- ggplot(top10_pop, aes(pop, reorder(country, pop))) +
  geom_col() +
  theme_minimal() +
  theme(
    panel.grid.major = element_blank(),
    panel.grid.minor = element_blank()
  ) +
  labs(
    x = NULL, y = NULL,
    title = "Top 10 of countries by population size in 2015",
    caption = "Data: gapminder.org"
  )

While this chart is fine it’s not ready for publication. Let’s say we want to display population sizes on the x axis in decimal format and additionally add the share of each country on World population formatted as percentages as labels to the bars using German style conventions.

Using scales this could be achieved like so:

lbl_pct <- label_percent(big.mark = ".", decimal.mark = ",", accuracy = .1)
lbl_num <- label_number(big.mark = ".", decimal.mark = ",")
p +
  # Add percentages to bars using German style conventions
  geom_text(
    aes(label = lbl_pct(pct)),
    hjust = 1.1, size = 8 / .pt, color = "white") +
  # Format numbers as percentages
  scale_x_continuous(
    labels = lbl_num,
    expand = c(0, 0, .05, 0))

As this simple example shows displaying numbers in decimal format or as percentages requires some typing, especially if you want to or have to deviate from default decimal and grouping marks.

That’s where countryscales comes in handy as using label_percent_de and scale_x_number_de the same could be achieved with less typing like so:

p +
  # Add percentages to bars using German style conventions
  geom_text(
    aes(
      label = label_percent_de(accuracy = .1)(pct)
    ),
    hjust = 1.1, size = 8 / .pt, color = "white"
  ) +
  # Format numbers as percentages
  scale_x_number_de(expand = c(0, 0, .05, 0))

Beyond Germany

Every example above uses label_number_de()/scale_x_number_de(), but that’s just the tip of the iceberg. Under the hood, all of these delegate to a general-purpose locale engine, label_number_locale()/ scale_x_number_locale() (and their _percent_/_currency_ counterparts), which work with any of the 764 locale codes countryscales supports – run show_locales() to list them all. On top of that engine, countryscales also ships the same ready-to-use _de()-style convenience family for 27 countries, from Argentina to the United States (see the reference index for the full list).

Getting locale-specific formatting right also involves more than just the decimal and grouping marks shown here: correct currency symbol and sign positioning (which side of the number, with or without a space) and locale-correct percent-sign placement and spacing are full of exceptions that scales::label_number()/label_currency() don’t attempt to solve. countryscales handles these under the hood using a modified version of label_number() – see the README’s Credits section for the full story.