Automate on-brand, accessible data visualisations for your reporting using R and doestyle

Author

Sam Gardiner (and you!)

Published

June 4, 2026

Introduction

This workshop has been developed and delivered on Dharug country.

Abstract

A picture tells a thousand words - but it can be time-consuming to craft data visualisations that meet our accessibility obligations while remaining on-brand and visually appealing. This workshop for R users will guide participants in the construction of several data visualisations using doestyle, the department’s in-house R package for plot styling. We will explore workflows to improve the ease and speed at which data visualisations can be built into evaluation reporting, and how to tweak these workflow defaults to improve visual appeal.

Learning intentions

  • Use R, ggplot2 and doestyle to apply styling and theming to data visualisations
  • Understand accessibility requirements for colour and contrast, and ensure that your data visualisations are accessible to the broadest section of our community
  • Develop workflows to make building multiple plots for reporting as quick and simple as possible

Getting started

This workshop is designed to be completed in a web browser. It uses WebR and Quarto Live to run a WebAssembly-powered R session within your browser, with no installation required.

Note

Whenever you see a ‘Run Code’ button, you’ll need to click it to progress. You can also run the code in a block with Ctrl+Enter (Windows/Linux) or Command+Enter (Mac), if your text cursor is within the block. You can activate auto-completion with Ctrl+Space. You can edit any of the blocks and run them to see how your changes affect the results. Some of the blocks contain exercises - usually you’ll need to replace sections of code that are missing, denoted with ______.

We’ll be using a few packages throughout the rest of the workshop, so load them now:

As you loaded this web page, the doestyle R package was installed for your use during the workshop. However, if you want to use doestyle for your own work in RStudio or Visual Studio Code, you’ll need to install it on your own computer (but not today):

install.packages('doestyle', repos = c('https://nsw-education.r-universe.dev',
                                       'https://cloud.r-project.org'))

The doestyle package includes a dataset called public_schools, which is a snapshot of the NSW public schools master dataset.

Let’s check that everything is working by building a quick visualisation with ggplot2, doestyle and the public schools data. Run the code and have a look at the output:

You might notice that we didn’t have to specify axis labels. This is because newer versions of ggplot2 automatically use column label metadata, if it exists (van den Brand 2025). The public_schools object has column labels that were extracted from our metadata repository at Metadata.NSW.

Brand, Teun van den. 2025. ggplot2 4.0.0 - Labels.” September 11. https://tidyverse.org/blog/2025/09/ggplot2-4-0-0/#labels.

A quick tour of ggplot2, as helped by doestyle

The Grammar of Graphics + doestyle

  • ggplot2 lets you map your data onto geometries via aesthetics and scales, building up plots in layers
  • doestyle provides helpers for ggplot2:
    • Department of Education-branded scales
    • easy access to NSW Government-branded colours
    • a couple of custom geometries
    • tools to check colour contrast ratios and relative luminosity
    • tools to make it slightly easier to use the Public Sans typeface

If you’ve never used ggplot2 before, there are plenty of good resources to get started:

  • The paper introducing the package and its implementation of the grammar of graphics (Wickham 2011)
  • The amazing, free and frequently-updated official ggplot2 book (Wickham et al. 2026)
  • Jonathan’s alliterative introductory conference talk on the grammar of graphics (McGuire 2024)
Wickham, Hadley. 2011. ggplot2.” WIREs Computational Statistics 3 (2): 180–85. https://doi.org/10.1002/wics.147.
Wickham, Hadley, Danielle Navarro, and Thomas Lin Pederson. 2026. ggplot2: Elegant Graphics for Data Analysis (3e). Springer Cham. https://ggplot2-book.org/.
McGuire, Jonathan. 2024. “Charming Charts: A Gentle Guide to the Grammar of Graphics.” Paper presented Department of Education Evaluation Conference. (Parramatta, NSW). https://tinyurl.com/2rdjsv5r.

Warm-up exercises: colours, scales and themes

We’ll use the copy of public_schools included within doestyle, and filter it so we’re just looking at primary and secondary schools that have enrolments (latest_year_enrolment_FTE), a reported LBOTE percentage (LBOTE_pct) and a reported ICSEA (ICSEA_value).

Let’s visualise the relationship between school size and LBOTE percentage:

We’ve made the following declarations, and ggplot2 has taken care of the rest:

  • Map enrolment count values onto the x-axis
  • Map LBOTE proportion values onto the y-axis
  • Map level of schooling values onto colour
  • Display each of these mappings with the ‘point’ geometry
  • Tweak the x-axis scale so that number format includes a comma

The plot works and tells a story, but it doesn’t use the NSW Education brand palette. You can look up our colours in our brand guidelines, but the point of doestyle is to reduce the friction involved in making on-brand graphics.

If we need them, we can also use the whole NSW brand palette:

We can also get the hex code for any named brand colours, without having to look at the swatches:

Try it for yourself. Pick any two DoE or NSW colour names and retrieve their hex values:

Now, use those hex codes in the plot we built earlier. We’ve added on scale_colour_manual(), which lets you modify the colour mapping from the Level_of_schooling data to the aesthetics used by geom_point. Add the hex codes your retrieved:

Copying and pasting hex codes isn’t a great workflow, so let’s just use doe_colours() inside scale_colour_manual():

So that you don’t have to look up colours for every plot, doestyle includes a ready-made scale and palettes that you can use instead of ggplot2::scale_colour_manual(): scale_colour_doe().

Our scale_colour_doe() function has a palette argument. Its default is doe_palettes$default, which provides on-brand defaults for up to 12 categories1:

1 Not that you should ever be trying to produce a plot that uses 12 colours - if so, it’s time to rethink what you’re trying to visualise!

Other built-in palettes are available in the doe_palettes object, which you can explore on your own.

If you want to define your own palettes, you can:

This will come in handy if you’re producing a series of plots with similar categories. Having the same colour language across your plots will reduce the cognitive burden of interpreting them.

Try it yourself. Create a palette with two on-brand colours of your choice, and apply the palette to the plot with scale-colour_doe():

Finally, if you don’t want to worry about setting scales or designing a palette, you can just apply theme_doe(), which will do its best to use sensible2 defaults. You’ll notice that theme_doe() applies some other defaults, like removing the panel background, increasing most text sizes and reducing the number of gridlines:

2 Sensible for a couple of colours, but after that you are better off doing your own decision-making.

Depending on the medium for which you’re designing your plots, you may want to tweak the theme_doe() defaults. If you are using more than one geom (for example, a labelled bar plot - geom_col() and geom_text()) you will definitely need to start thinking about which colour and fill scales to use.

There’s still plenty more we could do to polish this plot, but for now we’ll move on to our main topic.

Accessibility

From our very own website (NSW Department of Education 2026):

NSW Department of Education. 2026. “Accessibility Guidelines.” NSW Department of Education, March 16. https://education.nsw.gov.au/about-us/how-we-communicate/accessibility-basics/accessibility-guidelines.html.

Accessibility means providing equal access to information for all our audiences, with special consideration for people with disability.

Our approach to accessibility at the NSW Department of Education is about creating content that does not exclude anyone because of their ability, situation or circumstance. Rather, it is an inclusive approach to content that should make it easier for all users to understand, interact with and respond to our websites, apps, communications and materials.

  • Accessibility improves the experience for everyone, not just people with a disability
  • Starting your visualisations with accessibility in mind will often help you to design a simpler, more interpretable plot

The standard: WCAG 2.2

We aim for level AA conformance with Web Content Accessibility Guidelines 2.2 (World Wide Web Consortium 2024c).

World Wide Web Consortium. 2024c. “Web Content Accessibility Guidelines (WCAG) 2.2.” https://www.w3.org/TR/WCAG22/.

Web Content Accessibility Guidelines (WCAG) explains how to make web content more accessible to people with disabilities. WCAG covers web sites, applications, and other digital content.

There are three levels of conformance:

  • Level A is the minimum level.
  • Level AA includes all Level A and AA requirements. Many organizations strive to meet Level AA.
  • Level AAA includes all Level A, AA, and AAA requirements.

(World Wide Web Consortium 2024b)

World Wide Web Consortium. 2024b. “Web Content Accessibility Guidelines (WCAG) 2 Level AA Conformance.” World Wide Web Consortium. https://www.w3.org/WAI/WCAG2AA-Conformance.

You can read the whole guideline, but for our purpose today, we’re most interested in sections of Guideline 1.4 Distinguishable:

Make it easier for users to see and hear content including separating foreground from background.

Colour

Success Criterion 1.4.1 Use of Color

Color is not used as the only visual means of conveying information …

Caution

About 1 in 20 of your audience will have some form of colour blindness. If you use colour as the only way of communicating, you risk misinterpretation or even complete failure of your visualisation.

Contrast

Success Criterion 1.4.3 Contrast (Minimum)

The visual presentation of text and images of text has a contrast ratio of at least 4.5:1 …

Large-scale text and images of large-scale text have a contrast ratio of at least 3:1.

Large text means 18 points, or 14 points and bold.

World Wide Web Consortium. 2024a. “Understanding SC 1.4.3.” https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html.

The WWC provides excellent guidance and examples (World Wide Web Consortium 2024a).

How can we meet the standard?

If we use colour to convey information, often we’ll need to add a second aesthetic mapping (in addition to colour) to represent categories. Depending on your plot type and data, there are many workable options:

  • Colour + shape
  • Colour + pattern or line type
  • Colour + text annotation

If you have more than about 3 categories, there’s a good chance you’ll be better off designing a simpler visual:

  • Can you move your coloured categories to a labelled axis?
  • Can you use small multiples instead (e.g. ggplot2::facet_wrap() or facet_grid())?
  • Would a table be easier to interpret?

Getting away from colour alone

For this exercise, we’ll look at schools with an attached preschool. Here’s our dataset:

We’re counting the number of schools with an attached preschool, by directorate. Here’s a plot that uses colour alone to distinguish between the types of school:

We can improve the accessibility of the plot by using linetype in addition to colour:

We should be able to make the plot a little bit more attractive, while still overloading the colour aesthetic, by choosing colour-linetype combinations more carefully, using scale_fill_manual() and scale_linetype_manual().

Is it worth trying a different type of plot altogether?

Exercise - Getting away from colour alone

It’s your turn. Using the same preschools dataset, we want to investigate the relationship between school size and its ICSEA.

There’s plenty we can learn from this plot, but unfortunately it relies on the colour aesthetic alone for differentiation between school types.

How could we improve it? Make your changes here:

Hint

Check out the documentation for scale_shape().

Text contrast

Sometimes your stakeholders will ask for data labels or other text annotations3.

3 An appropriate question when asked for data labels is “Would a table be clearer?”

If we want to meet WCAG 2.2 Success Criterion 1.4.3 Contrast (Minimum) (Level AA), we need to make sure that text has a contrast ratio of at least 4.5:1 against its background. For WCAG, W3C defines contrast ratio as

\[ \text{CR} = \frac{L_1 + 0.05}{L_2 + 0.05} \]

where \(L_1\) and \(L_2\) are the relative luminances of the brighter and darker colours being compared. The function for relative luminance is piecewise and depends on the red-, green- and blueness of the colour (World Wide Web Consortium 2024d).

World Wide Web Consortium. 2024d. “Web Content Accessibility Guidelines (WCAG) 2.2 - Relative Luminance.” https://www.w3.org/TR/WCAG22/#dfn-relative-luminance.

Fortunately, doestyle provides some help with choosing contrasting colours for text.

  • contrast_ratio()
  • relative_luminance()

Contrast ratio has a maximum of 21:

… and a minimum of 1:

The Department of Education Brand Guidelines provide a helpful matrix of combinations that meet the 4.5:1 contrast requirement, but it is helpful to be able to compute them in R, as well. Suppose you want to check several combinations at once:

You could even somewhat automate the check with some logic, to make sure the palette you’ve chosen meets the 3.5:1 ratio. In fact, we do something like this in the automated tests that doestyle goes through before a release:

Exercise: find adequate text contrast

We’ll be looking at the proportion of schools with opportunity classes:

Can you improve the text contrast of this plot?

Step 1: find a colour that has a contrast ratio of at least 4.5:1 when plotted over blue-01 and red-02:

Step 2: apply your high-contrast colour to the text layer:

text_colours() and secondary_colours()

doestyle provides some pre-baked text and secondary colours so you don’t have to go searching for them.

If these secondary and text colours look familiar, it’s because they’re used in show_colours():

Workflows

Bundling customisations

If you’re producing multiple plots with similar subject matter, it’s helpful to define common scales. If you store them in a standard R list, ggplot2 will apply them all when you add the list to a plot. This helps you prepare reusable components for your publication.

Now we can use this scale in multiple plots, and have a consistent visual language.

The same logic applies to more than scales. If you want to bundle a lot of customisations, put them in a list() and add the list to your plot(s).

Make your own templates

If you need to make similar plots over and over, it’s probably time to write a function. A simple pattern is to write a function that takes a dataframe and returns a ggplot2 plot object.

Because the function takes data and returns a ggplot2, you have flexibility to easily customise its inputs and further customise its output. Here’s the base case, for all schools:

We can filter the input to a single principal network:

We can further customise the output:

… and we can use our function with functionals and/or loops to really get going with automation:

Exercise: factor out common parts of your workflow

Can you bundle several customisations so that you have a consistent, accessible and on-brand design language for these two plots?

Make your changes here.

Other packages

  • colorspace provides more tools for assessing colours and palettes using contrast ratios and a variety of other measures. Includes tools for simulating colour blindness.
  • ggpattern helps you define and apply pattern fills. We didn’t use any patterns today, but they are another way to avoid relying on colour alone.
  • ggpackets provides a framework for easily templating ggplots.
  • ggrepel helps automate label placement to avoid overlapping text. Super useful if you have a lots of plots to make and don’t want to hand-craft the label positions for each one.