Creating a Custom htmlwidget for Shiny

[This article was first published on Programming on nielsenmark.us, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.





A year ago, htmlwidgets were a mystery to me. I was first introduced to them at a conference years ago. I previously used rCharts which I really liked because of the ability it gave me to customize my interactive graphs in Shiny. I approached an instructor and explained my interest in rCharts to him and he pointed me in the direction of htmlwidgets. Last year I finally decided to take that leap and give it a try.

Setting Up the HTMLWidget

I started my learning with this tutorial from Ramnath V., Kenton R., and Rstudio on creating htmlwidgets, in which it defines that “the htmlwidgets package provides a framework for creating R bindings to JavaScript libraries.” Following along with this tutorial we see that we can easily create our first htmlwidget.

devtools::create("mywidget")
setwd("mywidget")
htmlwidgets::scaffoldWidget("mywidget")
devtools::install()

One thing to note about htmlwidgets is that they are always hosted in an R package to ensure full reproducibility.

File Structure

Next, let’s follow the tutorial further and take a look at the file structure.


.
├── DESCRIPTION
├── inst
│   └── htmlwidgets
│       ├── mywidget.js
│       └── mywidget.yaml
├── mywidget.Rproj
├── NAMESPACE
└── R
    └── mywidget.R

We see here that in order to bind our JavaScript library to our new R package we need to include both some R code (mywidget.R) and JavaScript (mywidget.js). All the JavaScript, YAML, and other dependencies will be located in the inst\htmlwidgets folder. The R code is located in the R folder which should define the inputs to our new function we are creating. Below you can see the sample htmlwidget we have created takes a character string as input and it will create a html page and pass through our character string to the JavaScript code.

library(mywidget)
mywidget("Hello World",height="100px")

Vioala! Your first htmlwidget AND the classic “Hello World”. Okay, okay… maybe this isn’t as awesome as you were thinking, but we can do even better. Are you ready to create your first htmlwidget?

Step 1: Adding your own JavaScript code

First let’s find some code for the popular JavaScript library D3. I am not a web developer so I found mine in a blog post by Mike Bostock. I really liked the functionality and look of his D3 implementation of hive plots. Hive plots are credited to Martin Krzysinski. You’ll find Martin’s introduction to hive plots here. A simpler version of Mike’s implementation is found here.

Now that I’ve got my code I’m going to replace the JavaScript code in ./inst/htmlwidgets/hive.js with this:


HTMLWidgets.widget({

  name: 'hive_no_int',

  type: 'output',

  factory: function(el, width, height) {

    // TODO: define shared variables for this instance

    return {

      renderValue: function(x) {

        // alias options
        var options = x.options;

        // convert links and nodes data frames to d3 friendly format
        var nodes = HTMLWidgets.dataframeToD3(x.nodes);
        var prelinks = HTMLWidgets.dataframeToD3(x.links);

        // create json of link sources and targets
        var links = [];
        prelinks.forEach(function(d){
          var tmp = {};
          tmp.source=nodes[d.source];
          tmp.target=nodes[d.target];
          links.push(tmp);
        });

        var innerRadius = options.innerRadius,
            outerRadius = options.outerRadius;

        var angle = d3.scale.ordinal().domain(d3.range(x.numAxis+1)).rangePoints([0, 2 * Math.PI]),
            radius = d3.scale.linear().range([innerRadius, outerRadius]),
            color = d3.scale.category10().domain(d3.range(20));

        // select the svg element and remove existing children
        var svg = d3.select(el).append("svg")
          .attr("width", width)
          .attr("height", height)
          .append("g")
          .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

        svg.selectAll(".axis")
            .data(d3.range(x.numAxis))
            .enter().append("line")
            .attr("class", "axis")
            .attr("transform", function(d) {
              return "rotate(" + degrees(angle(d)) + ")";
            })
            .attr("x1", radius.range()[0])
            .attr("x2", radius.range()[1]);

        // draw links
        var link = svg.selectAll(".link")
            .data(links)
            .enter().append("path")
            .attr("class", "link")
            .attr("d", d3.hive.link()
              .angle(function(d) { return angle(d.x); })
              .radius(function(d) { return radius(d.y); }))
            .style("stroke", function(d) { return color(d.source.color); })
            .style("stroke-width", 1.5)
            .style("opacity", options.opacity);

        // draw nodes
        var node = svg.selectAll(".node")
            .data(nodes)
            .enter().append("circle")
            .attr("class", "node")
            .attr("transform", function(d) {
              return "rotate(" + degrees(angle(d.x)) + ")";
            })
            .attr("cx", function(d) { return radius(d.y); })
            .attr("r", 5)
            .style("fill", function(d) { return color(d.color); })
            .style("stroke", "#000000");

        function degrees(radians) {
          return radians / Math.PI * 180 - 90;
        }

      }

    };
  }
});

Next, I copy supporting JS and CSS code into ./inst/htmlwidgets/lib/ folder. For this project I’ll need d3.js as well as some code from Mike’s post to create our visualization. Here’s what is now contained in the ./inst/htmlwidgets/lib/ folder:

## d3-3.0/d3.v3.min.js
## hive-0.1/d3.hive.min.js
## hive-0.1/hive.css

And finally, I define those dependencies in ./inst/htmlwidgets/hive.yaml as seen below:


# (uncomment to add a dependency)
dependencies:
  - name: d3
    version: 3.0
    src: htmlwidgets/lib/d3-3.0
    script:
      - d3.v3.min.js
  - name: hive
    version: 0.1
    src: htmlwidgets/lib/hive-0.1
    script:
      - d3.hive.min.js
    stylesheet:
      - hive.css

Now that our dependencies are defined we can now create the bindings between R and JavaScript.

Step 2: Create the Bindings

Okay, the goal in this next step is to get our R dataframe to look just like this d3 dataset from the hive plot D3 code.


var nodes = [
  {x: 0, y: .1},
  {x: 0, y: .9},
  {x: 1, y: .2},
  {x: 1, y: .3},
  {x: 2, y: .1},
  {x: 2, y: .8}
];
var links = [
  {source: nodes[0], target: nodes[2]},
  {source: nodes[1], target: nodes[3]},
  {source: nodes[2], target: nodes[4]},
  {source: nodes[2], target: nodes[5]},
  {source: nodes[3], target: nodes[5]},
  {source: nodes[4], target: nodes[0]},
  {source: nodes[5], target: nodes[1]}
];

First, let’s tell R what it needs to pass through to our JavaScript library. This is done by creating a function that will take our data and options as arguments and combine them into a list. This list is then passed through the htmlwidget::createWidget function to be picked up by our JavaScript code. Below I used code provided in Rstudio’s tutorial and also replicate the options innerRadius, outerRadius, and opacity from Mike Bostock’s function:

hive <- function(nodes, 
                 links, 
                 innerRadius = 40, 
                 outerRadius = 240, 
                 opacity = 0.7, 
                 width = NULL, 
                 height = NULL, 
                 elementId = NULL) {

  # sort in order of node id
  if("id" %in% colnames(nodes)) {
    nodes <- nodes[order(nodes$id),]
    nodes$id <- NULL
  }

  # color by axis if no coloring is supplied
  if(!("color" %in% colnames(nodes))) {
    nodes$color <- nodes$x
  }

  # forward options using x
  x = list(
    nodes = nodes,
    links = links,
    numAxis = max(nodes$x)+1,
    options = list(innerRadius=innerRadius,
                   outerRadius=outerRadius,
                   opacity=opacity)
  )

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

Notice above that the objects nodes and links are R dataframes and that the final list x is passed through to JS.

Now that we’ve defined our R binding, let’s take a minute and set up the JavaScript binding in the hive.js file. For d3, we use the dataframeToD3() helper function. I’m not awesome with JavaScript, so I’m going to avoid making too many changes to this code:


// alias options
var options = x.options;

// convert links and nodes data frames to d3 friendly format
var nodes = HTMLWidgets.dataframeToD3(x.nodes);
var prelinks = HTMLWidgets.dataframeToD3(x.links);

// create json of link sources and targets
var links = [];
prelinks.forEach(function(d){
  var tmp = {};
  tmp.source=nodes[d.source];
  tmp.target=nodes[d.target];
  links.push(tmp);
});

To give you an understanding of what is under the hood of the dataframeToD3 function, jsonlite::toJSON is used to convert the dataframe to long-form representation. And when you look at the data you can see that recreating nodes is easy. As for links, we read in the data as prelinks then we need to add a loop to loop through each item of prelinks and finally create links just like it is in Mike’s JavaScript code.

Step 3: Putting it all together

All of our bindings are set up and once I’ve built and loaded my package, we’re ready to define some dataframes and test out our new htmlwidget.

library(hiveD3)
nodes = data.frame(id=c(0,1,2,3,4,5,6,7,8),
                   x=c(0,0,1,1,2,2,3,3,4), 
                   y=c(.1,.9,.2,.3,.1,.8,.3,.5,.9))
links = data.frame(source=c(0,1,2,2,3,4,5,6,7,8,8),
                   target=c(2,3,4,5,5,6,7,8,8,0,1))


hive_no_int(nodes=nodes,links=links, width = "700px", height = "500px")

When we run the hive function we see our new visualization! Note that for demonstration purposes only I’ve renamed this first function hive_no_int.

Alright! We’re ready to show off our work, but can you guess the first question that is going to be asked of you? Your friends may think it’s cool, but will say “Why doesn’t it do anything when I hover over it?” or “Why can’t I interact with it?” Well, so much for not having to tweak any JavaScript code. It’s time to dive in and add some interactivity.

Step 4: Making Finishing Touches

Let’s look at some next steps in getting our htmlwidget ready for prime time: – Adding interaction – Creating and sharing your package – Creating R documentation using RStudio and roxygen2 – Adding your package to htmlwidget gallery

We’ve talked about adding interaction, and once that is ready you can share your new package in several ways. Make sure to create helpful documentation for your new package before sharing on Github or on the htmlwidget gallery.

The Final Product

Great! I’ve gone ahead and added my package to GitHub. Of course, I did this after making sure to create some documentation and interactivity… and finally, we can show it off.

library(devtools)
install_github('nielsenmarkus11/hiveD3')

library(hiveD3)

nodes = data.frame(id=c(0,1,2,3,4,5,6,7,8),
                   x=c(0,0,1,1,2,2,3,3,4), 
                   y=c(.1,.9,.2,.3,.1,.8,.3,.5,.9))
links = data.frame(source=c(0,1,2,2,3,4,5,6,7,8,8),
                   target=c(2,3,4,5,5,6,7,8,8,0,1))

hive(nodes=nodes,links=links, width = "700px", height = "500px")

Thanks for taking some time to check out my explorations with htmlwidgets. What are the next steps for your project? Maybe someday I’ll put my stuff out on CRAN, and I definitely want to add some more interactivity and flexibility to my package. You can download and check it out by installing it from my GitHub page. Good luck!

References

To leave a comment for the author, please follow the link and comment on their blog: Programming on nielsenmark.us.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.



If you got this far, why not subscribe for updates from the site? Choose your flavor: e-mail, twitter, RSS, or facebook...

Comments are closed.

Search R-bloggers

Sponsors

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)