Observable for R users

Observable is a JavaScript-based programming framework for data exploration and visualisation, which is popular for creating interactive charts and dashboards. This blog post demonstrates why and how R users can integrate Observable into their existing R workflows.

April 1, 2025

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.

1
2
3
4
---
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:

1
2
3
4
5
```{r}
tuesdata <- tidytuesdayR::tt_load("2025-01-21")
exped_tidy <- tuesdata$exped_tidy
peaks_tidy <- tuesdata$peaks_tidy
```

We’ll focus on the peaks_tidy data here where the first few lines look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# A tibble: 480 × 29
   PEAKID PKNAME             PKNAME2 LOCATION HEIGHTM HEIGHTF HIMAL
   <chr>  <chr>              <chr>   <chr>      <dbl>   <dbl> <dbl>
 1 AMAD   Ama Dablam         Amai D Khumbu     6814   22356    12
 2 AMPG   Amphu Gyabjen      Amphu  Khumbu     5630   18471    12
 3 ANN1   Annapurna I        NA      Annapur    8091   26545     1
 4 ANN2   Annapurna II       NA      Annapur    7937   26040     1
 5 ANN3   Annapurna III      NA      Annapur    7555   24787     1
 6 ANN4   Annapurna IV       NA      Annapur    7525   24688     1
 7 ANNE   Annapurna I East   NA      Annapur    8026   26332     1
 8 ANNM   Annapurna I Middle NA      Annapur    8051   26414     1
 9 ANNS   Annapurna South    Annapu Annapur    7219   23684     1
10 APIM   Api Main           NA      Api Him    7132   23399     2
# ℹ 470 more rows
# ℹ 22 more variables: HIMAL_FACTOR <chr>, REGION <dbl>,
#   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>, …
# ℹ Use `print(n = ...)` to see more rows

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):

1
2
3
4
5
6
```{r}
library(dplyr)
plot_data <- peaks_tidy |> 
  filter(PSTATUS_FACTOR == "Climbed") |> 
  select(PYEAR, HEIGHTM, REGION_FACTOR)
```

Our data now looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 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
# ℹ Use `print(n = ...)` to see 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:

1
2
```{ojs}
```

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).

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):

1
2
3
```{r}
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:

1
2
3
```{ojs}
data = transpose(r_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:

1
2
3
```{ojs}
d3 = require("d3@7")
```

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().

1
2
3
4
5
6
7
```{ojs}
Plot.plot({
  marks: [
    Plot.dot(data, {x: "PYEAR", y: "HEIGHTM"})
  ]
})
```

Scatter plot of peak year vs height

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:

1
2
3
4
5
6
7
```{ojs}
dataTyped = data.map(({ PYEAR, HEIGHTM, REGION_FACTOR }) => ({
  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:

We can add comments to document our code using // in the same way we use # in R.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
```{ojs}
Plot.plot({
  // Draw points
  marks: [
    Plot.dot(dataTyped, {x: "PYEAR", y: "HEIGHTM"})
  ],
  // Grid and axes styling
  grid: true,
  x: {label: "Year of the first recorded climbing attempt on the peak"},
  y: {label: "Peak height (m)"}
})
```

Scatter plot of peak year vs height

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
```{ojs}
Plot.plot({
  // Draw points
  marks: [
    Plot.dot(dataTyped, {x: "PYEAR", y: "HEIGHTM", fill: "REGION_FACTOR"})
  ],
  // 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)"}
})
```

Scatter plot of peak year vs height

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
```{ojs}
Plot.plot({
  // Draw points
  marks: [
    Plot.dot(dataTyped, {x: "PYEAR", y: "HEIGHTM", fill: "REGION_FACTOR"})
  ],
  // 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
})
```

Scatter plot of peak year vs height

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


For attribution, please cite this work as:

Observable for R users.
Nicola Rennie. April 1, 2025.
nrennie.rbind.io/blog/observable-r-users
BibLaTeX Citation
@online{rennie2025,
  author = {Nicola Rennie},
  title = {Observable for R users},
  date = {2025-04-01},
  url = {https://nrennie.rbind.io/blog/observable-r-users}
}

Licence: creativecommons.org/licenses/by/4.0