Blogs & News

Home Blogs & News

Upgrade your AnalytiXagility mini-apps by creating animated time series charts with dygraphs and htmlwidgets


Expanding on my previous blogs on creating personalised and custom UI elements and widgets in Shiny – the technology on which we’ve built our mini-apps capability – in this post I will discuss another framework for expanding the look, feel and functionality of your AnalytiXagility mini-apps.

Recently I have been working on creating a prototype mini-app for the CHART-ADAPT project, to display animated vital signs data similar to what you would see in a bedside-care monitor. This time series data lends itself to using the excellent dygraphs for R package to create auto-updating animated plots. This package provides an R interface to the dygraphs JavaScript charting library, giving users the ability to display time series data. However, it lacks the built-in ability to create animated plots, so I decided to use htmlwidgets to create a custom animated dygraphs widget which takes in an array of data to create an animated time series plot.

The htmlwidgets package provides a great framework to either to customise existing widgets or create your own, as it allows to easily create R bindings with JavaScript libraries. The great bit about htmlwidgets is how it manages all HTML, JavaScript or CSS dependencies, as the widgets are hosted within the R package where you can put the relevant source code, dependent libraries and stylesheets.

Creating a template

To create a new widget we must first initiate a new R package which will depend on the htmlwidgets, so first, we need to install both packages using the xap.require() or install.packages() methods.

xap.require('htmlwidgets','devtools')    #Installing packages in the AnalytiXagility platform

Next, we will create a template for our widget by creating a package using devtools::create() and htmltools::scaffoldWidget() – this will populate our new package with the underlining structure for the widget.

devtools::create("animatedDygraphs")               # create package using devtools
setwd("animatedDygraphs")                          # navigate to package dir
htmlwidgets::scaffoldWidget("animatedDygraphs")    # create widget scaffolding

htmlwidgets::scaffoldWidget(“animatedDygraphs”) will automatically create a boilerplate or a template for your widget containing the following files animatedDygraphs.js, animatedDygraphs.yaml and animatedDygraphs.R with the following structure:

|-- animatedDygraphs/
  |-- inst/
      |-- htmlwidgets/
           |-- animatedDygraphs.js
           |-- animatedDygraphs.yaml
  |-- R/
      |-- animatedDygraphs.R

As you can see, the animatedDygraphs.R file contains functions to initiate our widget, as well as input and output renderers for the interactive app.

animatedDygraphs.R

animatedDygraphs <- function(message, width = NULL, height = NULL, elementId = NULL) {
  # forward options using x
  x = list(
    message = message
  )
  # create widget
  htmlwidgets::createWidget(
    name = 'animatedDygraphs',
    x,
    width = width,
    height = height,
    package = 'animatedDygraphs',
    elementId = elementId
  )
}
animatedDygraphsOutput <- function(outputId, width = '100%', height = '400px'){
  htmlwidgets::shinyWidgetOutput(outputId, 'animatedDygraphs', width, height, package = 'animatedDygraphs')
}

renderAnimatedDygraphs <- function(expr, env = parent.frame(), quoted = FALSE) {
  if (!quoted) { expr <- substitute(expr) } # force quoted
  htmlwidgets::shinyRenderWidget(expr, animatedDygraphsOutput, env, quoted = TRUE)
}

Here, input bindings are automatically defined as animatedDygraphsOutput and renderAnimatedDygraphs – so your widget can be easily integrated inside Shiny apps.

The argument message represents the arguments passed into the widget – such as data or options – and htmlwidgets::createWidget() constructs a widget which is automatically recognised in animatedDygraphs.js.

Then, if you look inside the generated animatedDygraphs.js renderValue() function, you’ll see that currently it only displays the text of the passed element el.innerText = x.message, where x is the argument passed into the htmlwidgets.

animatedDygraphs.js

HTMLWidgets.widget({
  name: 'animatedDygraphs',
  type: 'output',
  factory: function(el, width, height) {
    // TODO: define shared variables for this instance
    return {
      renderValue: function(x) {
        // TODO: code to render the widget, e.g.
        el.innerText = x.message;   
      },
      resize: function(width, height) {
        // TODO: code to re-render the widget with a new size
      }
    };
  }
});

To try the widget you can run devtools:install() – this will build your widget as a package and place it in the package repository. If you run this in an interactive environment this widget will simply display the passed-in argument.

#devtools:install()  # Build widget package
library(animatedDygraphs) # Load widget package
animatedDygraphs('My argument', width = "100px", height = "100px") 

My argument

Managing dependencies – adding Dygraphs JS library

Since we are building an extension to the dygraphs package we will use the dygraphs source code as a dependency. You can download the dygraphs source code and place this into the lib/dygraphs folder inside the htmlwidgets directory. Then, we will add this as a dependency in the animatedDygraphs.yaml file.

animatedDygraphs.yaml

dependencies:
  - name: dygraphs
    version: 1.1.1
    src: "htmlwidgets/lib/dygraphs"
    script: dygraph-combined.js

Now, our widget comes with the dygraphs source code.

Including basic dygraphs

Now that we have included the dygraphs source code in our widget we can use it to create standard dygraphs time series plots. To do this we have to edit the animatedDygraphs.js file to include a code snippet which will invoke dygraphs inside the element of the widget using the data we send it to. In the factory function, we will define the container and will render dygraphs inside the renderValue function with the data we pass into the widget.

animatedDygraphs.js

  factory : function(el, width, height) {

    var elementId = el.id;
    var container = document.getElementById(elementId);

    return {

      renderValue: function(x) {
        var output = new Dygraph(container, x.message)
      },

    }
  };

Now, if we run our widget again we will see a basic static dygraph.

#devtools:install()  # Build widget package
library(animatedDygraphs) # Load widget package
t <- seq(1,10)
d <- runif(10,0,1)
df <- ts(data.frame(x=t,y=d))  #xts-compatible time-series objec
animatedDygraphs(df)

Adding animation

Let’s add animation logic now. We fetch the data passed into the html widget and define rate and span. The variable rate will represent the duration interval, determining how often to render a new point. Span determines how many points to fit into the time series plot. We then create initiation logic, which starts dygraphs with the number of empty points determined by the span argument. Because we want our points to be updated from left to right, we remove one point from the left side of the plot and add a new point fetched in the loop from our data on every interval call. The speed of the interval is regulated by the rate parameter.

animatedDygraphs.js

HTMLWidgets.widget({
  name: 'animatedDygraphs',
  type: 'output',
  factory: function(el, width, height) {

    var elementId = el.id;
    var container = document.getElementById(elementId);

    return {

      renderValue: function(x) {
              // Set up global variables          
              var data = x.message                   // Fetch data
              var rate = 1000;                      // Determines how fast plot will update in ms
              var span = 50;                       // Determines how many points will be displayed at once in the plot 
              var label = ['animated Dygraph'];   // Plot label placeholder
              var options = [];                  // Options placeholder
              var t = new Date();               // Date object to be used as a timestamp 
              var upadtedPoints = [];          // A collection of points
              
              // Passing empty points to fill the span of points with Nulls
              // This creates an empty dygraph which will be later updated from Left-to-right with our data
              for (var i = span; i >= 0; i--) {
                  var s = new Date(t.getTime() - i * rate);
                  updatedPoints.push([s, null]);
                  };
                
              // Creating dygraphs output object
              var output = new Dygraph(container, updatedPoints); // Initially created with empty span of data
                
              // Adding animation logic
              var j = 0;
              window.intervalId = setInterval(function() {
                
                //Shift data
                updatedPoints.shift()
                
                // new timestamp
                var d = new Date(); 
                
                // Enable loop, if iterator exceeds length of data - start from beginning
                if (j >= data.length) {
                       j = 0;
                          }
                          
                // Update updatedPoints 
                var push = data[j]; 
                updatedPoints.push([d, push]);  // Pushes timestamp with the new point
                 
                // Update dygraph
                output.updateOptions( { 'file': updatedPoints } );
                 
                // Increment iterator
                j++;
                                },rate);   // Using rate parameter to wait the intervalId, min - (10ms)

      },
      resize: function(width, height) {
      }
    };
  }
});

Now, if we run our updated animatedDygraph widget it will animate rendering of the included data argument.

#devtools:install()  # Build widget package
library(animatedDygraphs)

data <-   runif(100, 0.0, 1.0)
animatedDygraphs(data)

Adding additional parameters

Dygraphs are highly customisable by default and contain options arguments where we can pass a list of options to adjust the look and behaviour of our time series. The detailed list of options can be found in dygraphs options reference.

animatedDygraphs.R

animatedDygraphs <- function(message, width = NULL, height = NULL, elementId = NULL) {

  
  # forward options using x
  x = list(
    data = message$data,
    rate = message$rate,
    span = message$span,
    options = message$options
  )

  # create widget
  htmlwidgets::createWidget(
    name = 'animatedDygraphs3',
    x,
    width = width,
    height = height,
    package = 'animatedDygraphs3',
    elementId = elementId
  )}

Now, we include the list of options in Dygraph(container, updatedPoints, options) inside the animatedDygraphs.js.

animatedDygraphs.js

      renderValue: function(x) {
              // Set up global variables          
              var data = x.data                   // Fetch data
              var rate = x.rate;                 // Determines how fast plot will update in ms
              var span = x.span;                // Determines how many points will be displayed at once in the plot 
              var options = x.options;         // Options placeholder
              var t = new Date();             // Date object to be used as a timestamp 
              var upadtedPoints = [];        // A collection of points
              
              var output = new Dygraph(container, updatedPoints, options) // Adding options argument

Since our widget takes any time series data, I’ve decided to use heart-rate data which is available at http://ecg.mit.edu/time-series and pass it into the updated animatedDygraphs widget. Here, we also create a list of options in which we specify the colour as red, disable the range selector and fill the graph under the line.

#devtools:install()  # Build widget package
#Final widget
library(animatedDygraphs)

data <- read.csv(src= "http://ecg.mit.edu/time-series/hr.7257", stringsAsFactors = FALSE) # 
options <- list(color = c('red'),
                showRangeSelector = FALSE,
                fillGraph=TRUE,
                drawPoints = FALSE,
                showRoller =  TRUE)

animatedDygraphs(list(data = data, rate = 10, span = 1000,options = options))

As you can see in the animation above, using the framework described can go a long way to making your mini-apps much more interactive and engaging, and shows how they can be used to bridge the gap between research and clinical utility. In fact, you can see a similar animation in the CHART-ADAPT project video where it is used to display the results arising from the analysis of high and low frequency data collected from in-hospital patient monitoring systems in a neurological critical care unit, ultimately presenting the outputs at the bedside in order to advance medical research and patient treatment.
mini-apps

Now that you know how to use devtools and htmltools to enhance the functionality of existing widgets, you can now go on to use this method to create completely new widgets by providing your own JavaScript backends as dependencies. This allows to greatly expand the capabilities of your mini-apps, display data in various creative ways, and ultimately generate vivid research outputs to reach a wider audience of users. I hope this was useful and will help you to develop your own great widgets and visualisations. As always, feedback is welcome so feel free to get in touch with comments or questions.