Observable JS in Quarto document

OJS
Quarto
Author

Jihong Zhang

Published

March 10, 2024

1 Overview

The documents regarding how to use Observable Javascript in Quarto is extremely lacking. Two main sources for this topic are quarto.org and quarto dashboard.

Because ojs cells cannot be executed by simply clicking on “Run” button in Rstudio, instead, you can only see the result in the rendering step. Thus, it is always recommended to use observable notebook to build ojs and copy-paste into Quarto document.

Interesting, the starting place for observablejs is their github page - observablehq/stdlib, which include standard libraries for observablejs.

For data visualization lovers, Inputs (Quarto doc; OJS GitHub doc) and Plot (Quarto doc) are the two most important libraries in OJS.

2 Inputs Library

2.1 Button

Inputs.button(content, options)

```{ojs}
Inputs.button("Confirm", {label: "Click me!"})
```
```{ojs}
Inputs.button([
  ["Increment", (value) => value + 1],
  ["Decrement", (value) => value - 1],
  ["Reset", () => 0]
], {label: "Counter", value: 0})
```

Example of progress bar

```{ojs}
clicks = view(Inputs.button("Click me"));
```

2.2 Checkbox

```{ojs}
Inputs.checkbox(["Torgersen", "Biscoe", "Dream"], 
                {value: ["Torgersen", "Biscoe"], label: "Islands:"})
```

3 OJS 101

This section is based on the hands-on tutorial in official observable js website - Learn Just Enough JavaScript: Introduction.

3.1 Variables

```{ojs}
myVariable = "Javascript is cool!"
myVariable
```
```{ojs}
myNumber = 25
myNumber
```
Type Description Example
Number A numeric value
5.1
Boolean A true or false value
true
String A set of characters in single or double quotes
"hello"
Null A value represent the intentional absence of a value
null
Array A collection of elements
[1, 2, 3]
Object An element with key-value pairs
{weight:165, height: 66}
Date A special object for representing dates
new Date("2021-01-22")

3.2 Objects

In OJS, objects are similar to named vectors in R, following the format ({key1: value1; key2:value2}). Do not forget the parenthesis to make OJS object work in Quarto.

```{ojs}
myObject = ({name:"Paul", age:25})
myObject.name
```

3.3 Arrays

Arrays in OJS is similar to list (data.frame) in R and tuple in Python. It is defined with braces []. Note that same to python, the index of first element in OJS is 0.

```{ojs}
myArray1 = [1, 2, 3]
myArray1[2] // the third element
```
```{ojs}
myArray2 = [[1, 2], [3, 4]]
myArray2[1][1] 
```
```{ojs}
myArray3 = [1, 'cat', ({name: "ketty"})]
myArray3[2]
```
```{ojs}
myArray3[2].name
```

3.4 Functions

```{ojs}
function add(x, y) {
  return x + y
}
add(1, 3)
```

3.5 Conditions

Using a double equal sign == is a logical test to see if two values are the same. In JavaScript, we have different types of values, like numerical values or strings. If you use a triple equal sign ===, you not only check if the values are the same, but you check if the value types are the same.

```{ojs}
1 == 1 // true
1 == '1' // true
1 === '1' // false
```

3.6 Loops

For loop follows the format for (let i = ${starting}; ${end condition}, i ++). Note that let is neccessary in let string='' to define a constant variable.

```{ojs}
{
  let string=''
  for (let i = 0; i <=5; i ++){
    string += i
  }
  return string
}
```

Given a array, we can loop through the array.

```{ojs}
myValues = [1, 20, 13, 4, 55, 6]
```
```{ojs}
{
  let largestNumber = 0; // Declare a variable for the largest number
  for(let i = 0; i < myValues.length - 1; i++) { // Loop through all the values in my array
    if(myValues[i] > largestNumber) { // Check if the value in the array is larger that the largestNumber
      largestNumber = myValues[i] // If so, assign the value as the new largest number
    }
  }
  return largestNumber
}
```

You can also use the helper function in D3.js.

```{ojs}
d3.max(myValues)
```

One example of repeating animation using while loops.

```{ojs}
{
  const width = 300;
  const height = 100;
  const r = 30;
  
  const svg = d3.create('svg')
    .attr('width', width)
    .attr('height', height);

  const circle = svg.append('circle')
    .attr('r', r)
    .attr('cy', height / 2)
    .attr('cx', r);

  let cx = 30;
  while(true) { // Loop goes on forever
    yield svg.node();
    await Promises.delay(2000); // This causes the loop to "wait" 2000 milliseconds
    cx == r ? cx = width - r : cx = r;
    circle.transition()
      .duration(1500)
      .attr('cx', cx);

  }
}
```

4 Example 1: Palmer Penguins

Currently, OJS doesn’t work interactively with RStudio (see the github discussion). As the document shows, the example based on Allison Horst’s Palmer Penguins.

```{r}
#| message: false
#| output: false
library(palmerpenguins)
library(here)
data(penguins)
write.csv(penguins, file = here("notes", "2024",'2024-03-10-Quarto-Observable-JS', 'palmer-penguins.csv'))
```
```{ojs}
data = FileAttachment('palmer-penguins.csv').csv({ typed: true})
```
```{ojs}
viewof bill_length_min = Inputs.range(
  [32, 50], 
  {value: 35, step: 1, label: "Bill length (min):"}
)
viewof islands = Inputs.checkbox(
  ["Torgersen", "Biscoe", "Dream"], 
  { value: ["Torgersen", "Biscoe"], 
    label: "Islands:"
  }
)
```
```{ojs}
filtered = data.filter(function(penguin) {
  return bill_length_min < penguin.bill_length_mm &&
         islands.includes(penguin.island);
})
```
```{ojs}
Plot.rectY(filtered, 
  Plot.binX(
    {y: "count"}, 
    {x: "body_mass_g", fill: "species", thresholds: 20}
  ))
  .plot({
    facet: {
      data: filtered,
      x: "sex",
      y: "species",
      marginRight: 80
    },
    marks: [
      Plot.frame(),
    ]
  }
)
```

5 Transfer from R to OJS

There’s a nice post from Duke library, introducing a nicer way of embedding R and OJS. The getting-started page of OJS official website has many examples of different types of plots.

```{r}
#| output: false
ojs_define(penguins = penguins)
```

5.1 Dot (scatter) plot

```{ojs}
Plot.plot({
  grid: true,
  inset: 10,
  marks: [
    Plot.dot(transpose(penguins), {
      x: "bill_length_mm",
      y: "bill_depth_mm",
      stroke: "species"
    }),
  ]
})
```

5.2 Histogram

```{r}
#| output: false
library(tidyverse)
penguins_summarized <- penguins |> 
  mutate(body_mass_group = cut(body_mass_g / 1000, 10)) |> 
  group_by(body_mass_group) |> 
  summarise(frequency = n())
ojs_define(penguins_summarized = penguins_summarized)
```
```{ojs}
Plot.plot({
  x: {padding: 0.1},
  marginTop: 50,
  marginRight: 0,
  marginBottom: 50,
  marks: [
    Plot.barY(transpose(penguins_summarized), {x: "body_mass_group", y: "frequency", dx: 2, dy: 2}),
    Plot.barY(transpose(penguins_summarized), {x: "body_mass_group", y: "frequency", fill: "green", dx: -2, dy: -2})
  ]
})
```
Back to top