16.2 A widget example (sigma.js)
To start with, we will walk through the creation of a simple widget that wraps the sigma.js graph visualization library. When we are done, we will be able to use it to display interactive visualizations of GEXF (Graph Exchange XML Format) data files. For example (see Figure 16.1 for the output, which is interactive if you are reading the HTML version of this book):
library(sigma)
system.file("examples/ediaspora.gexf.xml", package = "sigma")
d =sigma(d)
There is remarkably little code required to create this binding. Next we will go through all of the components step by step. Then we will describe how you can create your own widgets, including automatically generating basic scaffolding for all of the core components.
16.2.1 File layout
Let’s assume that our widget is named sigma and is located within an R package of the same name. Our JavaScript binding source code file is named sigma.js. Since our widget will read GEXF data files, we will also need to include both the base sigma.min.js
library and its GEXF plugin. Here are the files that we will add to the package:
R/
| sigma.R
inst/
|-- htmlwidgets/
| |-- sigma.js
| |-- sigma.yaml
| |-- lib/
| | |-- sigma-1.0.3/
| | | |-- sigma.min.js
| | | |-- plugins/ | | | | |-- sigma.parsers.gexf.min.js
Note the convention that the JavaScript, YAML, and other dependencies are all contained within the inst/htmlwidgets
directory, which will subsequently be installed into a package sub-directory named htmlwidgets
.
16.2.2 Dependencies
Dependencies are the JavaScript and CSS assets used by a widget, included within the inst/htmlwidgets/lib
directory. They are specified using a YAML configuration file that uses the name of the widget as its base filename. Here is what our sigma.yaml file looks like:
dependencies:
- name: sigma
version: 1.0.3
src: htmlwidgets/lib/sigma-1.0.3
script:
- sigma.min.js
- plugins/sigma.parsers.gexf.min.js
The dependency src
specification refers to the directory that contains the library, and script
refers to specific JavaScript files. If your library contains multiple JavaScript files specify each one on a line beginning with -
as shown above. You can also add stylesheet
entries, and even meta
or head
entries. Multiple dependencies may be specified in one YAML file. See the documentation on the htmlDependency()
function in the htmltools package for additional details.
16.2.3 R binding
We need to provide users with an R function that invokes our widget. Typically this function will accept input data as well as various options that control the widget’s display. Here is the R function for the sigma
widget:
#' @import htmlwidgets
#' @export
function(
sigma =drawEdges = TRUE, drawNodes = TRUE, width = NULL,
gexf, height = NULL
) {
# read the gexf file
paste(readLines(gexf), collapse = "\n")
data =
# create a list that contains the settings
list(drawEdges = drawEdges, drawNodes = drawNodes)
settings =
# pass the data and settings using 'x'
list(data = data, settings = settings)
x =
# create the widget
::createWidget(
htmlwidgets"sigma", x, width = width, height = height
) }
The function takes two classes of input: the GEXF data file to render, and some additional settings that control how it is rendered. This input is collected into a list named x
, which is then passed on to the htmlwidgets::createWidget()
function. This x
variable will subsequently be made available to the JavaScript binding for sigma
(to be described in the next section). Any width or height parameter specified is also forwarded to the widget (widgets size themselves automatically by default, so typically do not require an explicit width or height).
We want our sigma widget to also work in Shiny applications, so we add the following boilerplate Shiny output and render functions (these are always the same for all widgets):
#' @export
function(outputId, width = "100%", height = "400px") {
sigmaOutput =::shinyWidgetOutput(
htmlwidgets"sigma", width, height, package = "sigma"
outputId,
)
}#' @export
function(expr, env = parent.frame(), quoted = FALSE) {
renderSigma =if (!quoted) { expr = substitute(expr) } # force quoted
::shinyRenderWidget(
htmlwidgetsquoted = TRUE
expr, sigmaOutput, env,
) }
16.2.4 JavaScript binding
The third piece in the puzzle is the JavaScript required to activate the widget. By convention, we will define our JavaScript binding in the file inst/htmlwidgets/sigma.js
. Here is the full source code of the binding:
.widget({
HTMLWidgets
name: "sigma",
type: "output",
factory: function(el, width, height) {
// create our sigma object and bind it to the element
var sig = new sigma(el.id);
return {
renderValue: function(x) {
// parse gexf data
var parser = new DOMParser();
var data = parser.parseFromString(x.data, "application/xml");
// apply settings
for (var name in x.settings)
.settings(name, x.settings[name]);
sig
// update the sigma object
.parsers.gexf(
sigma, // parsed gexf data
data, // sigma object
sigfunction() {
// need to call refresh to reflect new settings
// and data
.refresh();
sig
};
),
}
resize: function(width, height) {
// forward resize on to sigma renderers
for (var name in sig.renderers)
.renderers[name].resize(width, height);
sig,
}
// make the sigma object available as a property on the
// widget instance we are returning from factory(). This
// is generally a good idea for extensibility -- it helps
// users of this widget interact directly with sigma,
// if needed.
s: sig
;
}
}; })
We provide a name and type for the widget, plus a factory
function that takes el
(the HTML element that will host this widget), width
, and height
(width and height of the HTML element, in pixels — you can always use offsetWidth
and offsetHeight
for this).
The factory
function should prepare the HTML element to start receiving values. In this case, we create a new sigma
element and pass it to the id
of the DOM element that hosts the widget on the page.
We are going to need access to the sigma
object later (to update its data and settings), so we save it as a variable sig
. Note that variables declared directly inside of the factory
function are tied to a particular widget instance (el
).
The return value of the factory
function is called a widget instance object. It is a bridge between the htmlwidgets runtime, and the JavaScript visualization that you are wrapping. As the name implies, each widget instance object is responsible for managing a single widget instance on a page.
The widget instance object you create must have one required method, and may have one optional method:
The required
renderValue
method actually pours our dynamic data and settings into the widget’s DOM element. Thex
parameter contains the widget data and settings. We parse and update the GEXF data, apply the settings to our previously-createdsig
object, and finally callrefresh
to reflect the new values on-screen. This method may be called repeatedly with different data (i.e., in Shiny), so be sure to account for that possibility. If it makes sense for your widget, consider making your visualization transition smoothly from one value ofx
to another.The optional
resize
method is called whenever the element containing the widget is resized. The only reason not to implement this method is if your widget naturally scales (without additional JavaScript code needing to be invoked) when its element size changes. In the case of sigma.js, we forward the sizing information on to each of the underlying sigma renderers.
All JavaScript libraries handle initialization, binding to DOM elements, dynamically updating data, and resizing slightly differently. Most of the work on the JavaScript side of creating widgets is mapping these three functions, factory
, renderValue
, and resize
, correctly onto the behavior of the underlying library.
The sigma.js example uses a simple object literal to create its widget instance object, but you can also use class based objects or any other style of object, as long as obj.renderValue(x)
and obj.resize(width, height)
can be invoked on it.
You can add additional methods and properties on the widget instance object. Although they will not be called by htmlwidgets itself, they might be useful to users of your widget that know some JavaScript and want to further customize your widget by adding custom JS code (e.g., using the R function htmlwidgets::onRender()
). In this case, we add a property s
to make the sigma object itself available.
library(sigma)
library(htmlwidgets)
library(magrittr)
system.file("examples/ediaspora.gexf.xml", package = "sigma")
d =sigma(d) %>% onRender("function(el, x) {
// this.s is the sigma object
console.log(this.s);
}")
16.2.5 Demo
Our widget is now complete! If you want to test drive it without reproducing all of the code locally you can install it from GitHub as follows:
::install_github('jjallaire/sigma') devtools
Here is the code to try it out with some sample data included with the package:
library(sigma)
sigma(system.file("examples/ediaspora.gexf.xml", package = "sigma"))
If you execute this code in the R console, you will see the widget displayed in the RStudio Viewer (or in an external browser if you are not running RStudio). If you include it within an R Markdown document, the widget will be embedded into the document.
We can also use the widget in a Shiny application:
library(shiny)
library(sigma)
system.file("examples/ediaspora.gexf.xml", package = "sigma")
gexf =
shinyUI(fluidPage(
ui =checkboxInput("drawEdges", "Draw Edges", value = TRUE),
checkboxInput("drawNodes", "Draw Nodes", value = TRUE),
sigmaOutput('sigma')
))
function(input, output) {
server =$sigma = renderSigma(
outputsigma(gexf,
drawEdges = input$drawEdges,
drawNodes = input$drawNodes)
)
}
shinyApp(ui = ui, server = server)