Designing monochrome data visualisations

In data visualisations, colours are often used to show values or categories of data. However, sometimes you might not be able to or want to use colour. This blog post discusses some tips for designing better visualisations when you're restricted to a monochrome palette.

February 5, 2025

First of all, let’s start with a definition of what we mean by monochrome (or monochromatic). Creating a monochrome chart essentially means only using different shades of one colour. In most cases, this means different shades of grey (or black and white) which, can also be termed greyscale. The examples in this blog post will all be relating to creating charts using only black, white, and grey. However, you should find some of the advice useful if you ever need to make monochrome charts with a different colour.

There are several different reasons why you might need to make monochrome visualisations. A (frustratingly) common one is that some academic publishers still require versions of plots to be submitted in black and white. Another reason might be to improve accessibility - if a plot is understandable in greyscale, it’s much more likely to be colourblind-friendly. It also has the added benefit of preserving the colour representations in your charts for people who like to print out documents (and save money by not using colour ink)!

Although this blog post isn’t a follow-up to Working with colours in R, you might also find it helpful for implementing some of the ideas discussed here.

Can’t we just print existing plots in monochrome?

You might think the easiest way to produce a black and white visualisation is to take your existing, colourful plot and simply print it in black and white. You could do this by decreasing the saturation with a simple image editor, or choose Print to PDF. If only it was that easy!

For example, let’s see what happens when we use the default colour scheme from {ggplot2} in R (left), and then reduce the saturation (right). There are now very small differences between the colours, and the categories are difficult to tell apart.

Show code: bar chart in {ggplot2}
1
2
3
4
5
6
g_bar <- ggplot() +
  geom_bar(
    data = mpg,
    mapping = aes(y = class, fill = drv)
  )
g_bar

A stacked bar chart with salmon pink, blue, and green bars. A stacked bar chart with grey bars that are hard to distinguish.
A bar chart using the default colour palette in {ggplot2} (left) and a desaturated version of the image (right)

The approach of desaturating plots might work sometimes, but it’s not likely to give you the best results. When we create charts in monochrome, we have to rely only on the intensity of the colour to differentiate categories. For some colour palette choices, there will already be a range of colour intensities, and so they look okay when printed in greyscale. The ColorBrewer palettes are common choices for chart colours, and you can choose to view only Photocopy Friendly palettes (which gives palettes that will withstand black and white photocopying).

For example, the Set3 palette is classed as photocopy friendly, although it’s still not especially easy to differentiate categories when they don’t all appear in the same order (e.g. when some are missing).

Show code: using ColorBrewer in {ggplot2}
1
2
g_bar +
  scale_fill_brewer(palette = "Set3")

A stacked bar chart with light green, yellow, and purple bars. A stacked bar chart with grey bars that are partly difficult to distinguish.
A bar chart using the `Set3` colour palette (left) and a desaturated version of the image (right)

Other palette packages often don’t offer the option to view only photocopy safe palettes. However, you can use functions such as cvd_grid() in {colorblindr} to simulate how a plot will look desaturated. It’s also important to check that your colour choices are colour blind friendly (which the cvd_grid() function can also help with).

Monochrome colour palettes

When we talk about colour palettes, they typically fall into one of three categories:

  • Sequential: A gradual blend from a darker colour to a lighter shade (of the same or a different colour) used to show a continuous variable.
  • Diverging: Two contrasting colours at either end of the scale with a neutral colour (such white) in the middle, which is used to show differences above or below some baseline.
  • Discrete/Qualitative/Categorical: distinct colours with no natural ordering, used to show different categories.

Sequential palettes

Sequential scales are perhaps the easiest to work with in monochrome. The natural dark to light (or vice versa) palettes translate well. For example, darker colours may represent higher values and then gradually lighter shades of grey represent lower values.

You should also consider the background colour of your charts. A white background is likely to provide better contrast compared to other colour choices.

In {ggplot2}, you can use scale_*_gradient(low = "grey80", high = "black") to create a monochrome colour palette. I would recommend using a light grey instead of a white as the low argument to improve contrast against the plot background. You can remove the grey plot background using theme_bw().

Show code: sequential colours in {ggplot2}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
g_point <- ggplot() +
  geom_point(
    data = mpg,
    mapping = aes(x = displ, y = class, colour = hwy)
  )
g_point

g_point +
  scale_color_gradient(
    low = "grey80",
    high = "black"
  ) +
  theme_bw()

A scatter plot with a blue colour palette, and a version with a grey colour palette.
A scatter plot using the default continuous colour gradient in {ggplot2} (left) and a version using a monochrome grey palette (right)

Diverging palettes

Diverging scales don’t translate well (arguably not at all) to monochrome, so you need to think about a way to present your data that doesn’t rely on colour. For example, a lollipop chart where the length of the lines, in either direction, will represent the direction and difference from the baseline.

Discrete palettes

Discrete palettes are the most difficult type of colour palette to get right in monochrome visualisations. It’s partly because the colours used in discrete palettes may appear different in colour, but actually have similar colour intensities. It’s made more difficult since they’re used to differentiate categories rather than show a continuous value. This means that all colours need to be visually distinct. The ColorBrewer palettes discussed earlier only have a single Photocopy Friendly discrete colour palette, and it has a maximum of 3 colours! That’s not often enough for visualisations.

Rather than starting with a colour palette and trying to make it look okay in monochrome, start designing with a monochrome palette. We can use different, visually distinct, shades of grey to denote different categories in the example below. There’s an argument that this doesn’t quite match the data type - you’re showing categorical data with what is essentially a sequential grey colour palette. However, if you’re designing in monochrome, that may be unavoidable.

In {ggplot2}, you can use scale_*_grey() to get a grey, discrete palette. In base R bar charts, the default already is a grey palette for discrete variables!

Compare the two charts below. The left is the example from the start of this blog post where the chart with the default colours is desaturated, and the right is an example of starting with a monochrome (grey) palette. Which one is easier to read?

Show code: discrete (fill) colours in {ggplot2}
1
2
3
g_bar +
  scale_fill_grey() +
  theme_bw()

A stacked bar chart with grey bars that are hard to distinguish. A stacked bar chart with grey bars that are easy to distinguish.
A desaturated version of a bar chart made with the default colours in {ggplot2} (left) and a version using a monochrome grey palette (right)

The approach of using different shades of grey for different categories works well in bar charts, where each categories are shown as solid blocks of colour, positioned next to each other. When we make something like a scatter plot where each category is shown by a point with a much smaller area and not directly next to a different colour, we can use the same approach but it’s not quite as effective. We’ll look at some ways to improve this further a little bit later.

Show code: discrete colours in {ggplot2}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
g <- ggplot() +
  geom_point(
    data = mpg,
    mapping = aes(x = displ, y = class, colour = drv)
  )
g

g +
  scale_colour_grey() +
  theme_bw()

A scatter plot with three discrete coloured points, and a version using three shades of grey.
A scatter plot using the default colour palette in {ggplot2} (left) and a version using a monochrome grey palette (right)

Alternatives to colour

Rather than relying solely on colour (whether monochrome or not) to differentiate categories, it’s good practice to also use some other chart elements to differentiate the categories. For example, in a chart with multiple lines, you may show one category in an orange solid line and another in a blue dashed line.

Using patterns and shapes

Although the bar chart example above is reasonably clear, we may wish to make it even easier to distinguish the categories. This is especially important if we’re referring to specific categories when we’re talking about the chart in a presentation or report. We may combine different shades of colour with different patterns. In the example below, it’s much easier to refer to and identify the dotted bars compared to the light grey bars.

The {ggpattern} package in R makes it easy to use pattern-filled geometries. For example, you can use geom_bar_pattern() instead of geom_bar() to create your bar charts. The {fillpattern} package provides similar functionality, including for base R graphics.

Show code: using patterns in {ggplot2}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
ggplot() +
  geom_bar(
    data = mpg,
    mapping = aes(y = class, fill = drv)
  ) +
  scale_fill_grey() +
  theme_bw()

ggplot() +
  geom_bar_pattern(
    data = mpg,
    mapping = aes(y = class, fill = drv, pattern = drv)
  ) +
  scale_fill_grey() +
  theme_bw()

A stacked bar chart with grey bars that are easy to distinguish, and a version that additionally uses patterns.
A bar chart using only shades of grey for different categories (left) and a version which also uses different patterns (right)

In scatter plots, it’s common to use different shapes (either with or without different colours) to differentiate categories. In the example below, both the shade of grey and the shape identify the three categories. It’s an improvement on the previous version, but some of the categories are still quite hard to see.

Show code: using shapes in {ggplot2}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
g <- ggplot(
  data = mpg,
  mapping = aes(
    x = displ, y = class, colour = drv
  )
) +
  scale_colour_grey() +
  theme_bw()
g +
  geom_point()

g +
  geom_point(
    mapping = aes(shape = drv)
  )

A scatter plot using three shades of grey and a version that additionally uses shapes.
A scatter plot using only shades of grey for different categories (left) and a version which also uses different shapes (right)

Different plotting approaches

Rather than thinking of monochrome charts as an afterthought or a limitation, try to see it as an opportunity to really think about the design of your chart. Start with monochrome, and add colour if you need it. You may even find that you don’t need to use colour at all. It’s good way to force yourself to consider how best to communicate what you’re trying to show, rather than sticking to your default, colour-oriented visualisations.

Let’s take the scatter plot example from above, where we try to show three different groups of data using three different colours. One alternative to a single scatter plot, is to make three scatter plots and highlight each group on just one of the small multiple plots. You can see in the example below, each category is highlighted in black and this actually makes it easier to see the patterns. For example, straight away you can see that the first facet is dominated by the suv and pickup categories. That’s harder to see in the single scatter plot, colour version.

Show code: using facets and highlighting
1
2
3
4
5
6
7
8
9
ggplot() +
  geom_point(
    data = mpg,
    mapping = aes(x = displ, y = class)
  ) +
  scale_colour_grey() +
  facet_wrap(~drv) +
  gghighlight() +
  theme_bw()

A faceted plot with three panels with grey scatter plots, and a group highlighted in black on each panel.
A set of three small multiples, with each smaller chart highlighting one of three categories

We can also reconsider the bar chart example from above. Stacked bar charts are typically quite hard to interpret anyway as the stacked nature of them makes comparisons between any category (other than the one nearest the axis) quite difficult. We might instead choose to create a grouped bar chart, and add labels directly to the bars. There’s, again, perhaps an argument that colour is not needed at all with this approach.

Show code: adding labels in {ggplot2}
 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
ggplot() +
  geom_bar(
    data = mpg,
    mapping = aes(y = class, fill = drv)
  ) +
  scale_fill_grey() +
  theme_bw()

mpg |>
  dplyr::count(class, drv) |>
  ggplot() +
  geom_col(
    mapping = aes(x = n, y = class, fill = drv),
    position = position_dodge2(preserve = "single", width = 1)
  ) +
  geom_text(
    mapping = aes(
      x = n + 0.5, y = class,
      group = drv,
      label = paste0("drv:", drv)
    ),
    position = position_dodge2(preserve = "single", width = 1),
    hjust = 0
  ) +
  scale_fill_grey() +
  scale_x_continuous(limits = c(0, 60)) +
  theme_bw()

A stacked bar chart with grey bars that are easy to distinguish, and a version that instead uses direct labels and horizontal bars.
A stacked bar chart using only shades of grey for different categories (left) and a grouped version which also uses direct labelling (right)

There are other problems…

There are several other problems that we haven’t yet covered. For example, in multi-coloured visualisations, grey is often used to show missing values, especially in plots such as choropleth maps. If you’re using grey to show data, it can’t also be used to show missing data. However some of the solutions discussed earlier may also be help e.g. using a striped pattern fill for missing data.

The key message from this blog post is that, if you need to design monochrome visualisations, you need to actively design them rather than assuming things will just work when printed in black and white.

If you want to read a little bit more about monochrome visualisations:


For attribution, please cite this work as:

Designing monochrome data visualisations.
Nicola Rennie. February 5, 2025.
nrennie.rbind.io/blog/monochrome-data-visualisations
BibLaTeX Citation
@online{rennie2025,
  author = {Nicola Rennie},
  title = {Designing monochrome data visualisations},
  date = {2025-02-05},
  url = {https://nrennie.rbind.io/blog/monochrome-data-visualisations}
}

Licence: creativecommons.org/licenses/by/4.0