Skip to contents

{detourr} relies heavily on the HTMLWidgets package, which allows us to define visuals using javascript and CSS. Each of these visuals is referred to as a widget, and the {detourr} package allows for new widgets to be added independently of existing ones.

There are several reasons why HTMLWidgets and Javascript are used, for example:

  • Javascript, HTML, and CSS are portable. They can be used with any browser, RStudio, Shiny, and RMarkdown.
  • Perfomance of animations in JS is good, especially when using HTML Canvas elements. This means we can get smooth animations and display large datasets.
  • It’s easy to build interactive elements in JS
  • A large range of JS visualisation libraries can be used

Rather than writing javascript code directly, {detourr} widgets are written in typescript (TS) which is then transpiled in to javascript when the package is built. Typescript is used because it makes the code easier to read, and prevents a range of runtime bugs by type-checking the code.

Prerequisites

To set up the project for development, you’ll need to install node.js and yarn.

Then after cloning this repository, open the terminal and install the node.js dependencies with:

yarn install

Then bundle the typescript code and install {detourr} using:

yarn run build

Creating a widget

To get started, we’ll first create a new file srcts/hello_world/index.ts with the following contents:

export class HelloWorld {
  private containerElement: HTMLDivElement;
  private message: HTMLHeadElement;

  constructor(el: HTMLDivElement, width: number, height: number) {
    var h = document.createElement("h1");
    var t = document.createTextNode("Hello, world!");
    h.appendChild(t);
    this.message = h;
    this.resize(width, height);
  }

  public renderValue(inputData: any) {
    document.body.appendChild(this.message);
    console.log(inputData);
  }

  public resize(width: number, height: number) {
    console.log("Width: ", width, ", Height: ", height);
  }
}

Here, the HelloWorld widget is created as it’s own class with two public methods: renderValue and resize. The renderValue method takes an argument inputData, which will contain all the data passed from R to our widget including any configuration we need, our data set, and the sequence of projection matrices representing the tour path.

At the moment, all we are doing is logging the data to the browser console. This will will allow us to explore it’s structure when it’s up and running.

Bundling the widget

The HTMLWidgets package can’t deal with this typescript code as-is, so we need to modify our webpack configuration so that it gets transpiled to javascript and bundled when the package is built.

This only requires a small modification to the webpack/package.config.ts to include our TS code as an additional entrypoint:

webpack/package.config.ts:

const path = require("path");
const webpack = require("webpack");

module.exports = {
  mode: "production",
  entry: {
    "show_scatter_2d": "./srcts/show_scatter_2d/index.ts",
    "show_scatter_3d": "./srcts/show_scatter_3d/index.ts",
+   "hello_world": "./srcts/hello_world/index.ts",
  },
  ...

Now, when we build and install our package using yarn run build, a file hello_world.bundle.js is created in the inst/htmlwidgets/lib folder alongside the existing bundles.

Adding the configuration and javascript bindings

The steps in this section are very similar to the general example in the htmlwidgets documentation, so we will gloss over many of the technical details here.

In short, we require two files for our widget to work: inst/htmlwidgets/hello_world.yaml which contains the configuration, and inst/htmlwidgets/hello_world.js which contains the javascript bindings.

First we’ll set up the configuration file inst/htmlwidgets/hello_world.yaml:

dependencies:
  - name: hello_world 
    src: htmlwidgets/lib/
    version: 0.1.0
    script:
      # these are specified in the order they should load
      - runtime.bundle.js
      - hello_world.bundle.js

Notice the inclusion of runtime.bundle.js. This contains the webpack runtime, and our code won’t run without it. Any external packages that your widget loads will be bundled separately as well, so we need to include them in this file above the main bundle.

For the javascript binding, we need to create the file inst/htmlwidgets/hello_world.js. Because we implemented our widget as a typescript class with renderValue and resize methods, the contents of the javascript binding can remain quite simple even as the widget itself becomes more complex:

inst/htmlwidgets/hello_world.js:

HTMLWidgets.widget({

    name: "hello_world",
    type: "output",

    factory: function (el, width, height) {
        let widget = new hello_world.HelloWorld(el, width, height);
        return {
            renderValue: function (x) {
                widget.renderValue(x);
            },
            resize: function (width, height) {
                widget.resize(width, height)
            },
            s: widget
        }
    }
})

Adding the display method in R

Here we create the R binding for the show_hello_world function. This is simply a function that takes a detour object and returns an HTML Widget. In this example, we’ve also included a ... argument so that we can pass parameters through to htmlwidgets::createWidget, and included some sensible defaults for the widget sizingPolicy.

R/show_hello_world.R:

#' Display Hello World Example
#'
#' Note that these comments will be converted in to documentation by the
#' {roxygen2} package when we run `yarn run build`. As such, the `@export`
#' below is necessary, or else this function won't be made available to the
#' user
#' @param d a `detour` object
#' @param ... additional arguments to pass to `htmlwidgets::createWidget`
#' @export
show_hello_world <- function(x, ...) {
  dots <- list(...)
  htmlwidgets::createWidget(
    name = "hello_world",
    as.list(x),
    sizingPolicy = htmlwidgets::sizingPolicy(
      viewer.padding = 0,
      viewer.paneHeight = 500,
      browser.fill = TRUE,
      knitr.defaultWidth = 800,
      knitr.defaultHeight = 500
    ),
    package = "detourr",
    width = dots$width,
    height = dots$height
  )
}

All the values in the detour object d are passed through to the hello_world widget’s renderValue method. This includes the tour path, data, and some basic configuration. Any configuration specific to this widget can be appended to the detourr object before the widget is created.

Trying it out

Now the hard part is done, we need to bundle everything together and install the package:

yarn run build

And then test out the widget in R:

library(detourr)
detour(tourr::flea, tour_aes(projection = where(is.numeric))) |>
  show_hello_world()

Success!

If we open up the browser console, we can see the full structure of the data set which is passed in to our widget. We can add additional configuation and aesthetic mappings as we need them by modifying the show_hello_world function: