Shiny Reactives

Overview

Earlier we described how to use the ojs_define() function to make data from Python and R available in OJS cells. In this scenario, data pre-processing is done once during render time then all subsequent interactions are handled on the client.

But what if you want to do data transformation dynamically in response to user inputs? This is also possible with ojs_define(), as it can be passed not just static values but also Shiny reactives (assuming it’s running inside a Shiny interactive document).

Hello, Shiny

Here is the K-Means Clustering example from the Shiny Gallery implemented with an OJS client and Shiny Server:

You can see the document deployed at https://jjallaire.shinyapps.io/kmeans-shiny-ojs/.

Source Code

Let’s take a look at the source code. On the client we have familiar looking OJS inputs and a plot laid out using panel: sidebar and panel: fill:

```{ojs}
//| panel: sidebar
vars = ["Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width"]
viewof xcol = Inputs.select(vars, {label: "X Variable"})
viewof ycol = Inputs.select(vars, {label: "Y Variable", value: vars[1]})
viewof count = Inputs.range([1, 9], {label: "Cluster Count", step: 1, value: 3})
```

```{ojs}
//| panel: fill
Plot.plot({
  color: {
    type: "ordinal",
    scheme: "category10"
  },
  marks: [
    Plot.dot(transpose(selectedData), {
      x: xcol,
      y: ycol,
      fill: (d, i) => clusters.cluster[i],
    }),
    Plot.dot(clusters.centers, { 
      x: d => d[0],
      y: d => d[1],
      r: 10,
      stroke: "black",
      fill: (d, i) => i + 1
    }),
  ]
})
```

Note that the plotting code references the variables selectedData and clusters. These will be provided by reactive expressions within the Shiny server code. Note also that we use the transpose() function to reshape the data into the row-oriented format that the Plot library expects.

Here is the server code:

```{r}
#| context: server

selectedData <- reactive({
  iris[, c(input$xcol, input$ycol)]
})

clusters <- reactive({
  kmeans(selectedData(), input$count)
})

ojs_define(selectedData, clusters)
```

We designate this code as running on the server via the context: server option.

Note that we reference several inputs that were defined by viewof expressions on the client (e.g. input$xcol). When these inputs change they will cause the appropriate server side reactives to re-execute.

We create two reactive values (selectedData and clusters) and provide them to the client using ojs_define(). The plot will be automatically re-drawn on the client side when these values change.

Examples

Here are some examples that demonstrate various ways to use OJS with Shiny:

Example Source Description
K-Means Code Simple example of binding OJS inputs to Shiny inputs and shiny reactives to OJS plots.
Binning Code Demonstrates fast binning of a medium sized dataset (32mb) on the server.
Data Binding Code Demonstrates importing a notebook from https://observablehq.com and binding it’s data field to a Shiny reactive.

Bindings

OJS to Shiny

In the example above we took advantage of the fact that by default OJS viewof expressions are automatically propagated to Shiny inputs (e.g input$xcol). This provides a reasonable separation of concerns, and prevents excess network traffic in the case that you have large OJS variables.

However, if you want to use other OJS variables as Shiny inputs this is also possible using the ojs-export option. The default behavior maps to the following configuration:

---
server:
  type: shiny
  ojs-export: viewof
---

You can also specify ojs-export: all to cause all OJS reactives to be bound to Shiny inputs:

---
server:
  type: shiny
  ojs-export: all
---

Alternatively, you can specify a list of OJS reactives by name (including using ~ to filter out reactives), and optionally combine this with the viewof and/or all options. For example:

---
server:
  type: shiny
  ojs-export: [all, ~large_dataset]
---

Shiny to OJS

Less common but occasionally useful is the ability to bind Shiny inputs into OJS. By default no such bindings occur, however you can use the ojs-import option to opt-in for specific Shiny inputs. For example:

---
server: 
  type: shiny
  ojs-import: [minimum, maximum]
---