How to add functionality to JavaScript widgets
Introduction
In the How to build a JavaScript based widget article, we created a simple C3 based gauge widget. In this tutorial, we expand the widget to create smooth transitions like those in the showcase dashboard app. We also provide code and examples for the other charts types shown in the dashboard: a pie chart, a line + bar chart and a stacked area chart. In this tutorial, we focus on pairing charts and data in such a way that C3 can animate the transition between old and new data.
example of a gauge with animated transitions between old and new data
Before we do all that, let’s take a better look at the HTMLWidgets.widget
function we saw in the first tutorial, which is related to the concept of factory methods and closures.
Factory methods and closures
In the previous tutorial, we saw that the skeleton of our widget initially looks like this
HTMLWidgets.widget({
name: 'C3Gauge',
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
}
};
}
});
If you are new to JavaScript, this code may look a bit strange. We see that factory
is a method, but the return type is not a simple value. What’s with that?
The construction above is a design pattern that software engineers call a factory method. This pattern contains a method that returns an object instance. That may sound complicated, so let’s dig a little deeper to see what it means.
In more general terms, a factory method like this:
factory: function () {return {
a: function() { ... },
b: function() { ... }
}};
is equivalent to:
// factory is a method of some object
factory: function () {
// define a new object
var obj = new Object();
// create a method a for it
obj.a = function() { ... };
// create a method b for it
obj.b = function() { ... };
// return the object instance
return obj;
}
Hence, in our case, the factory
method in HTMLWidgets.widget returns an object
of the widget instance. This object itself has two methods i.e. renderValue
, which draws our widget, and resize
, which resizes the widget (if necessary).
In the implementation above, renderValue
and resize
are both implemented as closures, which means that the functions remember the scope they were created in and have access to the variables defined in that scope. A more in depth discussion on JavaScript closures is offered here. Of note, the concept of closures also exists in R, see here.
The implication of renderValue
and resize
being closures is that they have access to the el
, width
and height
parameters passed to factory
. Furthermore, we can initialize variables in the body of factory
(above the definitions of renderValue
and resize
) that we want to use during the lifetime of our widget.
Now that we have a better understanding of what our HTMLWidgets.widget
function is doing, let’s add some transitions to our gauge widget!
Adding transitions to a C3 gauge
Even though the gauge widget from the first tutorial works, we can improve it. The current widget always creates a completely new gauge if we send it a value via shiny (using the renderC3Gauge
function, see tutorial 1). It would be nicer if the widget used an animation to smoothly transition from its old state to its new state, as in the example at the top of the page. Luckily, C3 can create such transitions automatically. To take advantage of this, we will have to modify the code of our factory method.
In pseudo code, our new factory method will looks something like this:
factory: function(el, width, height){
// we can define any variables we wish to use to keep track of the state of the widget here
// lets create an empty chart
var chart = null;
return {
renderValue: function(x) {
// check if the chart exists
if(chart === null){
// the chart did not exist and we want to create a new chart
chart = createChart(x);
// store the chart on el so we can get it later
el.chart = chart;
}
// at this stage the chart always exists
// get the chart stored in el and update it
el.chart.updateChart(x);
}
}
}
To understand the pseudo code, it is helpful to realize that there are two basic stages in the lifetime of a chart:
- the chart does not yet exist
- the chart already exists and we want to pass in new data, set options or send messages to it
The factory method has three arguments: el
, width
and height
. Here el
is the container element created by htmlwidgets in which we will house our chart. On top, we create a variable called chart
, which we initially set to null, indicating that the chart does not yet exist.
Inside the renderValue
method, we first check if the chart exists. If the chart does not exist, we create a chart via the toy function createChart, which in this case uses information in x
. Subsequently, we store the chart on el
, so we can access it later.
Technically, the closure allows us to render the chart even in the first line, i.e. at the line var chart = null;
. In C3, however, we often need information stored in x
to create a chart. Note that x
contains the data and chart options passed via shiny from R to JavaScript (using jsonlite, see tutorial 1). As C3 often needs information in x
during initialization, here we initialize the chart inside the renderValue
method and subsequently check if the chart was already created. This is slightly inelegant, however, it is quite easy and the overhead is minimal.
Note that the chart will exist in all subsequent calls to renderValue
. In those instances, we first retrieve the chart via el
, which we can access because renderValue
is a closure. Second, we call an update method on the chart, which will again use information stored in x
.
Let’s convert the previous pseudo code into actual code for our gauge!
The next JavaScript code block shows how we can load new data into a C3 based chart created via c3.generate.
var chart = c3.generate({
// chart code here
...
});
// some data
var newData = 50;
// load data into the chart
chart.load({
json: newData
});
If we combine this code snippet with the previous factory pseudo code we end up with the code we’re looking for:
HTMLWidgets.widget({
name: 'C3Gauge',
type: 'output',
factory: function(el, width, height) {
// create an empty chart
var chart = null;
return {
renderValue: function(x) {
// check if the chart exists
if(chart === null){
// the chart did not exist and we want to create a new chart via c3.generate
chart = c3.generate({
bindto: el,
data: {
json: x,
type: 'gauge',
},
gauge: {
label:{
format: function(value, ratio){ return value;}
},
min: 0,
max: 100,
width: 15,
units: 'value'
}
});
// store the chart on el so we can get it later
el.chart = chart;
}
// at this stage the chart always exists
// get the chart stored in el and update it
el.chart.load({json: x});
}
};
}
});
Of note, for brevity here we omitted the resize
method (which is optional).
Code repository and demo app
You can find the complete code for the updated gauge widget, as well as the code for the widgets below at this repo, which contains a C3 package. The C3 package contains updated code for the gauge widget, a pie chart widget, a combination bar + line chart and a stacked area chart. The code of the latter three widgets is discussed in detail below. The R code of these widgets can be found here. The JavaScript and YAML files for each widget are located here.
You can install the updated C3 package by running the following command in R:
devtools::install_github("FrissAnalytics/shinyJsTutorials/tutorials/materials2/C3")
In order to help you understand the various C3 widgets in this tutorial, we’ve built a shiny C3 demo app that shows how you can use these widgets in a shiny context. You can download this dashboard here. To see the app in action press the screenshot below! In the next tutorials we will extend the capabilities of these widgets even further when we take a closer look at JavaScript events.