16.5 Advanced topics
This section covers several aspects of creating widgets that are not required by all widgets, but are an essential part of getting bindings to certain types of JavaScript libraries to work properly. Topics covered include:
Transforming JSON representations of R objects into representations required by JavaScript libraries (e.g., an R data frame to a D3 dataset).
Passing JavaScript functions from R to JavaScript (e.g., a user-provided formatting or drawing function)
Generating custom HTML to enclose a widget (the default is a
<div>, but some libraries require a different element, e.g., a<span>).Creating a widget without creating an R package in the first place.
16.5.1 Data transformation
R objects passed as part of thex parameter to thecreateWidget() function are transformed to JSON using the internal functionhtmlwidgets:::toJSON()15, which is basically a wrapper function ofjsonlite::toJSON() by default. However, sometimes this representation is not what is required by the JavaScript library you are interfacing with. There are two JavaScript functions that you can use to transform the JSON data.
16.5.1.1 HTMLWidgets.dataframeToD3()
R data frames are represented in “long” form (an array of named vectors) whereas D3 typically requires “wide” form (an array of objects each of which includes all names and values). Since the R representation is smaller in size and much faster to transmit over the network, we create the long-form representation of R data, and then transform the data in JavaScript using thedataframeToD3() helper function.
Here is an example of the long-form representation of an R data frame:
{"Sepal.Length":[5.1,4.9,4.7],"Sepal.Width":[3.5,3,3.2],"Petal.Length":[1.4,1.4,1.3],"Petal.Width":[0.2,0.2,0.2],"Species":["setosa","setosa","setosa"]}After we applyHTMLWidgets.dataframeToD3(), it will become:
[{"Sepal.Length":5.1,"Sepal.Width":3.5,"Petal.Length":1.4,"Petal.Width":0.2,"Species":"setosa"},{"Sepal.Length":4.9,"Sepal.Width":3,"Petal.Length":1.4,"Petal.Width":0.2,"Species":"setosa"},{"Sepal.Length":4.7,"Sepal.Width":3.2,"Petal.Length":1.3,"Petal.Width":0.2,"Species":"setosa"}]As a real example, thesimpleNetwork (https://christophergandrud.github.io/networkD3/#simple) widget accepts a data frame containing network links on the R side, and transforms it to a D3 representation within the JavaScriptrenderValue function:
renderValue:function(x) {// convert links data frame to d3 friendly formatvar links= HTMLWidgets.dataframeToD3(x.links);// ... use the links, etc ...}16.5.1.2 HTMLWidgets.transposeArray2D()
Sometimes a 2-dimensional array requires a similar transposition. For this thetransposeArray2D() function is provided. Here is an example array:
[[5.1,4.9,4.7,4.6,5,5.4],[3.5,3,3.2,3.1,3.6,3.9],[1.4,1.4,1.3,1.5,1.4,1.7],[0.2,0.2,0.2,0.2,0.2,0.4],["setosa","setosa","setosa","setosa","setosa","setosa"]]HTMLWidgets.transposeArray2D() can transpose it to:
[[5.1,3.5,1.4,0.2,"setosa"],[4.9,3,1.4,0.2,"setosa"],[4.7,3.2,1.3,0.2,"setosa"],[4.6,3.1,1.5,0.2,"setosa"],[5,3.6,1.4,0.2,"setosa"],[5.4,3.9,1.7,0.4,"setosa"]]As a real example, thedygraphs widget uses this function to transpose the “file” (data) argument it gets from the R side before passing it on to the dygraphs library:
renderValue:function(x) {// ... code excluded ...// transpose array x.attrs.file= HTMLWidgets.transposeArray2D(x.attrs.file);// ... more code excluded ...}16.5.1.3 Custom JSON serializer
You may find it necessary to customize the JSON serialization of widget data when the default serializer inhtmlwidgets does not work in the way you have expected. For widget package authors, there are two levels of customization for the JSON serialization: you can either customize the default values of arguments forjsonlite::toJSON(), or just customize the whole function.
jsonlite::toJSON()has a lot of arguments, and we have already changed some of its default values. Below is the JSON serializer we use inhtmlwidgets at the moment:function (x, ...,dataframe ="columns",null ="null",na ="null",auto_unbox =TRUE,digits =getOption("shiny.json.digits",16),use_signif =TRUE,force =TRUE,POSIXt ="ISO8601",UTC =TRUE,rownames =FALSE,keep_vec_names =TRUE,strict_atomic =TRUE){if (strict_atomic) x<-I(x) jsonlite::toJSON(x,dataframe = dataframe,null = null,na = na,auto_unbox = auto_unbox,digits = digits,use_signif = use_signif,force = force,POSIXt = POSIXt,UTC = UTC,rownames = rownames,keep_vec_names = keep_vec_names,json_verbatim =TRUE, ...)}For example, we convert data frames to JSON by columns instead of rows (the latter is
jsonlite::toJSON’s default). If you want to change the default values of any arguments, you can attach an attributeTOJSON_ARGSto the widget data to be passed tocreateWidget(), e.g.,fooWidget=function(data, name, ...) {# ... process the data ... params=list(foo = data,bar =TRUE)# customize toJSON() argument valuesattr(params,'TOJSON_ARGS')=list(digits =7,na ='string' ) htmlwidgets::createWidget(name,x = params, ...)}We changed the default value of
digitsfrom 16 to 7, andnafromnulltostringin the above example. It is up to you, the package author, whether you want to expose such customization to users. For example, you can leave an extra argument in your widget function so that users can customize the behavior of the JSON serializer:fooWidget=function( data, name, ...,JSONArgs =list(digits =7)) {# ... process the data ... params=list(foo = data,bar =TRUE)# customize toJSON() argument valuesattr(params,'TOJSON_ARGS')= JSONArgs htmlwidgets::createWidget(name,x = params, ...)}You can also use a global option
htmlwidgets.TOJSON_ARGSto customize the JSON serializer arguments for all widgets in the current R session, e.g.options(htmlwidgets.TOJSON_ARGS =list(digits =7,pretty =TRUE))If you do not want to usejsonlite, you can completely override the serializer function by attaching an attribute
TOJSON_FUNCto the widget data, e.g.,fooWidget=function(data, name, ...) {# ... process the data ... params=list(foo = data,bar =TRUE)# customize the JSON serializerattr(params,'TOJSON_FUNC')= MY_OWN_JSON_FUNCTION htmlwidgets::createWidget(name,x = params, ...)}Here
MY_OWN_JSON_FUNCTIONcan be an arbitrary R function that converts R objects to JSON. If you have also specified theTOJSON_ARGSattribute, it will be passed to your custom JSON function, too.
16.5.2 Passing JavaScript functions
As you would expect, character vectors passed from R to JavaScript are converted to JavaScript strings. However, what if you want to allow users to provide custom JavaScript functions for formatting, drawing, or event handling? For this case, thehtmlwidgets package includes aJS() function that allows you to request that a character value is evaluated as JavaScript when it is received on the client.
For example, thedygraphs widget (https://rstudio.github.io/dygraphs) includes adyCallbacks function that allows the user to provide callback functions for a variety of contexts. These callbacks are “marked” as containing JavaScript so that they can be converted to actual JavaScript functions on the client:
library(dygraphs)dyCallbacks(clickCallback =JS(...)drawCallback =JS(...)highlightCallback =JS(...)pointClickCallback =JS(...)underlayCallback =JS(...))Another example is in theDT (DataTables) widget (https://rstudio.github.io/DT), where users can specify aninitComplete with JavaScript to execute after the table is loaded and initialized:
datatable(head(iris,20),options =list(initComplete =JS("function(settings, json) {","$(this.api().table().header()).css({ 'background-color': '#000', 'color': '#fff' });","}")))If multiple arguments are passed toJS() (as in the above example), they will be concatenated into a single string separated by\n.
16.5.3 Custom widget HTML
Typically the HTML “housing” for a widget is just a<div> element, and this is correspondingly the default behavior for new widgets that do not specify otherwise. However, sometimes you need a different element type. For example, thesparkline widget (https://github.com/htmlwidgets/sparkline) requires a<span> element, so it implements the following custom HTML generation function:
sparkline_html=function(id, style, class, ...){ htmltools::tags$span(id = id,class = class)}Note that this function is looked up within the package implementing the widget by the conventionwidgetname_html, so it need not be formally exported from your package or otherwise registered withhtmlwidgets.
Most widgets will not need a custom HTML function, but if you need to generate custom HTML for your widget (e.g., you need an<input> or a<span> rather than a<div>), you should use thehtmltools package (as demonstrated by the code above).
16.5.4 Create a widget without an R package
As we mentioned in Section16.3, it is possible to create a widget without creating an R package in the first place. Below is an example:
#' @param text A character string.#' @param interval A time interval (in seconds).blink=function(text,interval =1) { htmlwidgets::createWidget('blink',list(text = text,interval = interval),dependencies = htmltools::htmlDependency('blink','0.1',src =c(href =''),head ='<script>HTMLWidgets.widget({ name: "blink", type: "output", factory: function(el, width, height) { return { renderValue: function(x) { setInterval(function() { el.innerText = el.innerText == "" ? x.text : ""; }, x.interval * 1000); }, resize: function(width, height) {} }; }});</script>' ) )}blink('Hello htmlwidgets!', .5)The widget simply shows a blinking character string, and you can specify the time interval. The key of the implementation is the HTML dependency, in which we used thehead argument to embed the JavaScript binding. The value of thesrc argument is a little hackish due to the current restrictions inhtmltools (which might be removed in the future). In therenderValue method, we show or hide the text periodically using the JavaScript functionsetInterval().
Note that it is not exported fromhtmlwidgets, so you are not supposed to call this function directly.↩︎