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.
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
Bostock M, Morin R (2012). Hive Plots. Retrieved from https://bost.ocks.org/mike/hive/.
Bostock M (2016). Hive Plot (Links). Retrieved from https://bl.ocks.org/mbostock/2066415.
Bostock M (2017). D3 Data-Driven Documents. Retrieved from https://d3js.org/.
Krzywinski M, Birol I, Jones S, Marra M (2011). Hive Plots — Rational Approach to Visualizing Networks. Briefings in Bioinformatics (early access 9 December 2011, doi: 10.1093/bib/bbr069).
- Vaidyanathan R, Russell K, RStudio, Inc. (2014-2015). Creating a widget. Retrieved from http://www.htmlwidgets.org/develop_intro.html.