<- tidytuesdayR::tt_load("2025-01-21")
tuesdata <- tuesdata$exped_tidy
exped_tidy <- tuesdata$peaks_tidy peaks_tidy
Observable for R users
Observable is a JavaScript-based reactive programming environment, commonly used for interactive data exploration, visualisation, and dashboards. You can create and publish Observable notebooks at observablehq.com (which is also a great place to look for inspiration and support) or you can use Observable Javascript in standalone documents and websites.
So why would an R user want to learn or use Observable? Although there are several different packages for creating interactive charts directly in R, these can sometimes be hard to customise, load slowly, or require an R server for deployment. With Observable, you can use different JavaScript libraries for advanced customisation (though the learning curve might be quite steep for some). Since it’s JavaScript-based, there’s no need for a server to run code, and its reactive model and efficient rendering in a browser, means that its interactive visualisations can often be faster.
Although you can do some data wrangling and modelling within Observable, R arguably has much better capabilities for statistical modelling. If you want the best of both worlds, it might make sense to use for wrangling and modelling, then use Observable for creating interactive, web-based visualisations. Luckily, there’s an easy way to do just that using Quarto. This blog post will walk through the process of performing some data wrangling in R, passing the data to Observable, and creating a visualisation using Observable.
Quarto is an open-source scientific and technical publishing system, that allows you to easily combine code with narrative text to create reproducible outputs. It works with code written in Python, R, Julia, or Observable. If you haven’t used Quarto before, I’d recommend checking the Get Started section of the documentation. You can also view my Introduction to Quarto training course materials.
If you’re someone who is intrigued by JavaScript libraries like D3.js for creating visualisations, but are discouraged by how complicated it looks and how steep the learning curve might be, then using Observable with Quarto can be a gentler introduction. You also don’t need to install or set up any additional software. As long as you have Quarto installed, your document will render in such a way that enables the use of Observable within your outputs.
Data wrangling in R
Let’s dive into an example! We start by creating a new Quarto document. You can choose to leave the YAML empty if you want, but I recommend at least hiding the code by setting echo: false
to make sure only your visualisations end up in the final output.
---
execute:
echo: false
---
We’ll be using data on the history of Himalayan Mountaineering Expeditions, which was used as a TidyTuesday dataset in January 2025. We can load the data using the {tidytuesdayR}
package before performing any data wrangling in R as we normally would:
We’ll focus on the peaks_tidy
data here where the first few lines look like:
# A tibble: 480 × 29
PEAKID PKNAME PKNAME2 LOCATION HEIGHTM HEIGHTF HIMAL HIMAL_FACTOR REGION
<chr> <chr> <chr> <chr> <dbl> <dbl> <dbl> <chr> <dbl>
1 AMAD Ama Dablam Amai D… Khumbu … 6814 22356 12 Khumbu 2
2 AMPG Amphu Gyab… Amphu … Khumbu … 5630 18471 12 Khumbu 2
3 ANN1 Annapurna I <NA> Annapur… 8091 26545 1 Annapurna 5
4 ANN2 Annapurna … <NA> Annapur… 7937 26040 1 Annapurna 5
5 ANN3 Annapurna … <NA> Annapur… 7555 24787 1 Annapurna 5
6 ANN4 Annapurna … <NA> Annapur… 7525 24688 1 Annapurna 5
7 ANNE Annapurna … <NA> Annapur… 8026 26332 1 Annapurna 5
8 ANNM Annapurna … <NA> Annapur… 8051 26414 1 Annapurna 5
9 ANNS Annapurna … Annapu… Annapur… 7219 23684 1 Annapurna 5
10 APIM Api Main <NA> Api Him… 7132 23399 2 Api/Byas Ri… 7
# ℹ 470 more rows
# ℹ 20 more variables: REGION_FACTOR <chr>, OPEN <lgl>, UNLISTED <lgl>,
# TREKKING <lgl>, TREKYEAR <dbl>, RESTRICT <chr>, PHOST <dbl>,
# PHOST_FACTOR <chr>, PSTATUS <dbl>, PSTATUS_FACTOR <chr>, PEAKMEMO <dbl>,
# PYEAR <dbl>, PSEASON <dbl>, PEXPID <chr>, PSMTDATE <chr>, PCOUNTRY <chr>,
# PSUMMITERS <chr>, PSMTNOTE <chr>, REFERMEMO <dbl>, PHOTOMEMO <dbl>
Here, we’ll keep it reasonably simple and look at the relationship between the first year a climb was recorded (PYEAR
) and the height of the peak (HEIGHTM
). We’ll also look at the Himalayan region that each peak is in (REGION_FACTOR
). We can filter the data and select the columns we’re interested in using {dplyr}
(or base R if you prefer):
library(dplyr)
<- peaks_tidy |>
plot_data filter(PSTATUS_FACTOR == "Climbed") |>
select(PYEAR, HEIGHTM, REGION_FACTOR)
Our data now looks like this:
# A tibble: 368 × 3
PYEAR HEIGHTM REGION_FACTOR
<dbl> <dbl> <chr>
1 1961 6814 Khumbu-Rolwaling-Makalu
2 1953 5630 Khumbu-Rolwaling-Makalu
3 1950 8091 Annapurna-Damodar-Peri
4 1960 7937 Annapurna-Damodar-Peri
5 1961 7555 Annapurna-Damodar-Peri
6 1955 7525 Annapurna-Damodar-Peri
7 1974 8026 Annapurna-Damodar-Peri
8 1980 8051 Annapurna-Damodar-Peri
9 1964 7219 Annapurna-Damodar-Peri
10 1960 7132 Kanjiroba-Far West
# ℹ 358 more rows
It’s reasonably tidy, and we’re ready to start plotting (in Observable).
Observable code blocks
In Quarto, an Observable code block is added in a very similar way to R code blocks. Instead of specifying the language using {r}
, we use {ojs}
instead. Here, ojs
stands for Observable JavaScript (OJS), although it’s important that {ojs}
is in lowercase:
```{ojs}
// Observable code goes here
```
Unlike R, Observable code blocks don’t need to be in order. This means that you can group all of your output code together, and all of your data processing code together to keep your document looking cleaner (similar to the separation of ui
and server
in Shiny apps).
We can add comments to document our code using
//
in the same way we use#
in R.
Passing data from R to Observable
To pass our clean, wrangled data from R to Observable, we use the ojs_define()
function, where we define what we want the object to be called in Observable (r_data
) and what object we want to pass from R (plot_data
):
ojs_define(r_data = plot_data)
Note that this is an R code block, not an Observable code block. The Observable object name doesn’t need to be r_data
, and can be the same name as the R object (or almost anything else)!
R passes the data to Observable in a by column format. Depending on which functions or libraries in Observable you use to visualise your data, it might need to be passed in a by row format instead. In Observable, we can use the transpose()
function to switch from by column to by row format - since the basic Plot()
function we’ll be using later requires it in this format:
= transpose(r_data) data
An alternative approach could be to save the wrangled data to a CSV or JSON file in R, and read it into Observable as a local file using the FileAttachment()
function. See the data sources section of the Quarto documentation for different ways to read in data.
Using other libraries
When using core libraries that come with Observable (such as Observable Plot
), there’s no need to install or load anything additional. However, if you want to use non-core libraries like D3 or Arquero (a library inspired by {dplyr} for data transformation), it’s fairly straightforward to do it. We simply need to be explicit about importing those libraries. For example, to access functions from D3, we would run:
```{ojs}
= require("d3@7")
d3 ```
For this example, we’ll be able to do everything use the core Observable Plot library, so we don’t need to use any additional libraries.
Plotting with Observable
Observable Plot is a JavaScript library, primarily aimed at creating exploratory data visualisations, which is one of the core Observable libraries. You can draw many of the most common types of charts using the plot()
function from the Plot
library. Within Plot.plot()
, we specify marks
to define the geometries that are drawn such as lines or dots; color
to define the colours that are used; as well as other arguments like title
and subtitle
which can use to add text. If you’re a {ggplot2}
user, you might notice some similarities since it also follows a Grammar of Graphics approach.
Again, we’ll keep it simple for this introductory example and create a basic scatter plot of year against peak height for our Himalayan Expeditions data. In the marks
argument, we start by passing in the function for the geometry we want to draw. To create a scatter plot, we need to draw dots, so we use the Plot.dot()
function. The first argument is the data set we’re plotting, and the second argument is where we specify which columns of the data map to which axis. Again, if you’re a {ggplot2}
user, this is very similar to setting the arguments for the data and aesthetic mapping using aes()
.
.plot({
Plotmarks: [
.dot(data, {x: "PYEAR", y: "HEIGHTM"})
Plot
] })
This gives us a very basic scatter plot, and there are a few adjustments that we might want to make to improve it’s clarity. You might notice that the x-axis looks a little bit odd for data representing years. The year values are treated as numbers, and so Observable formats them with commas. This is helpful when we are plotting large numbers, but not so helpful when they are actually years. There are a couple of approaches we could take, but the easiest way is to convert the PYEAR
column from a number to a date, and Observable will know how to format it correctly:
= data.map(({ PYEAR, HEIGHTM, REGION_FACTOR }) => ({
dataTyped PYEAR: new Date(PYEAR, 0, 1),
,
HEIGHTM
REGION_FACTOR }))
Let’s also edit the axis labels to something more readable than the column names, and add a grid in the background. We use the grid
, x
, and y
arguments to make these adjustments, remembering to also update the data to our new (correctly typed) dataset:
.plot({
Plot// Draw points
marks: [
.dot(dataTyped, {x: "PYEAR", y: "HEIGHTM"})
Plot,
]// Grid and axes styling
grid: true,
x: {label: "Year of the first recorded climbing attempt on the peak"},
y: {label: "Peak height (m)"}
})
To change the colour of the points, we edit the mapping to also set the fill
colour of the points to be based on the values in the REGION_FACTOR
column. Within the color
argument, we can set whether or not we want a legend, and also choose a colour palette in the scheme
argument.
Observable colour palettes: The Observable documentation has an interactive colour palette viewer, where you can browse different sequential, diverging, and discrete colour palettes. The built-in options include the ColorBrewer palettes, which are also available in R.
.plot({
Plot// Draw points
marks: [
.dot(dataTyped, {x: "PYEAR", y: "HEIGHTM", fill: "REGION_FACTOR"})
Plot,
]// Colours
color: {legend: true, scheme: "set2"},
// Grid and axes styling
grid: true,
x: {label: "Year of the first recorded climbing attempt on the peak"},
y: {label: "Peak height (m)"}
})
Finally, we can add a title, subtitle, and caption to the plot. We can also set the size of the plot area, and change the sizes of the margins to add a little bit more space around the plot:
.plot({
Plot// Draw points
marks: [
.dot(dataTyped, {x: "PYEAR", y: "HEIGHTM", fill: "REGION_FACTOR"})
Plot,
]// Colours
color: {legend: true, scheme: "set2"},
// Grid and axes styling
grid: true,
x: {label: "Year of the first recorded climbing attempt on the peak"},
y: {label: "Peak height (m)"},
// Text
title: "The History of Himalayan Expeditions",
subtitle: "For peaks in the Himalayas that have been climbed, this chart shows the year of the first recorded climb and the height of the peak.",
caption: "Data: The Himalayan Database (2017)",
// Size
height: 400,
width: 800,
marginLeft: 50,
marginRight: 50
})
This introductory example showcases only a very small amount of what you can do with Observable. If you start making more plots in Observable, you’ll likely notice that there are lots of different ways to do things. For example, you could edit the axis labels using the Plot.AxisX()
function instead of in the x
argument. You could also use another library entirely (such as D3) to create a scatter plot. And since Observable is JavaScript-based, you can also use CSS to edit the styling of plot elements, or add hover effects. In terms of interactivity, you can easily add dropdown menus or sliders to plot different subsets of your data, using the Observable Inputs
(core) library.
One of the other nice features of Observable is that, if you see a complicated plot that you like, it’s fairly straightforward to import the notebook that creates it and replace the data with your own. See quarto.org/docs/interactive/ojs/examples/population for an example of importing a sunburst diagram.
Saving a static image
When you create a plot using Observable in your Quarto document, the images are rendered as SVG . However, you may also want to save a static or raster image file (such as a PNG) to share on social media, for example. Of course, the easiest way might be to simply take a screenshot. But you could automate it using the webshot()
function from the {webshot2}
package, and using CSS selectors to capture the Observable cell output with selector = ".cell-output.cell-output-display"
.
Additional resources
The Quarto documentation has lots of information about using Observable in Quarto, including with R and Python. It also has plenty of examples with code.
Deepali Kank has lots of great examples of charts created with Observable and D3, and many with the data wrangling performed in R. This chart about fragrances in perfumes is especially beautiful!
If you’re looking for a different way to work with D3 and R together, then the {r2d3} package provides functionality for working with D3 visualisations from R. This Jumping Rivers blog post by Mandy Norrbo provides a nice introduction.
Reuse
Citation
@online{rennie2025,
author = {Rennie, Nicola},
title = {Observable for {R} Users},
date = {2025-04-01},
url = {https://nrennie.rbind.io/blog/observable-r-users/},
langid = {en}
}