-
Notifications
You must be signed in to change notification settings - Fork 47
/
4-18-shiny-htmlwidgets.Rmd
693 lines (516 loc) Β· 23.6 KB
/
4-18-shiny-htmlwidgets.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
# Widgets with Shiny {#shiny-widgets}
We have seen how to make JavaScript and R communicate in Shiny applications by passing data from the server to the client and back. This chapter explores how to apply that to htmlwidgets so they can provide additional functionalities when used in Shiny applications.
To demonstrate how to integrate these functionalities in widgets, we shall implement them in the previously built gio package.
```{r, include=FALSE}
arcs <- jsonlite::fromJSON(
'[
{
"e": "CN",
"i": "US",
"v": 3300000
},
{
"e": "CN",
"i": "RU",
"v": 10000
}
]'
)
```
## Widgets to R {#shiny-widgets-to-r}
In a previous chapter on [Linking Widgets](#linking-widgets), the topic of callback function was briefly explored. Gio.js provides a callback function that is fired every time a country is selected on globe; this is currently used to share said country with other widgets as part of the implementation with crosstalk. This callback function could also send data back to the R server where they could be used for many things like fetching more data on the selected country from a database or use that information to generate a Shiny UI element like displaying the flag of the selected country, and much, much more.
The callback function in the gio package currently looks like this; it only makes use of some of the data for crosstalk.
```js
// define callback function
function callback (selectedCountry, relatedCountries) {
sel_handle.set([selectedCountry.ISOCode]);
}
// use callback function
controller.onCountryPicked(callback);
```
We can make further use of this to define two different Shiny inputs; one for the selected country and another for its related edges.
```js
// gio.js
// define callback function
function callback (selectedCountry, relatedCountries) {
sel_handle.set([selectedCountry.ISOCode]);
Shiny.setInputValue('selected', selectedCountry);
Shiny.setInputValue('related', relatedCountries);
}
```
However, this will generate an issue experienced in a previous chapter; multiple gio visualisations in a single Shiny application would be defining the values of a single input. This can be remedied to by using the id of the visualisation\index{visualisation} to generate the input name dynamically.
```js
renderValue: function(x) {
var controller = new GIO.Controller(el);
controller.addData(x.data);
controller.setStyle(x.style);
// callback
function callback (selectedCountry, relatedCountries) {
sel_handle.set([selectedCountry.ISOCode]);
Shiny.setInputValue(el.id + '_selected', selectedCountry);
Shiny.setInputValue(el.id + '_related', relatedCountries);
}
controller.onCountryPicked(callback);
// render
controller.init();
}
```
The package can then be installed with `devtools::install` so we can test these inputs in a Shiny application (Figure \@ref(fig:gio-shiny-no-handler)).
```r
library(gio)
library(shiny)
# large sample data
url <- paste0(
"https://raw.githubusercontent.com/JohnCoene/",
"javascript-for-r/master/data/countries.json"
)
arcs <- jsonlite::fromJSON(url)
ui <- fluidPage(
gioOutput("globe"),
fluidRow(
column(6, verbatimTextOutput("selectedCountry")),
column(6, verbatimTextOutput("relatedCountries"))
)
)
server <- function(input, output){
output$globe <- renderGio({
gio(arcs, source = i, target = e, value = v)
})
output$selectedCountry <- renderPrint({
print(input$globe_selected)
})
output$relatedCountries <- renderPrint({
print(input$globe_related)
})
}
shinyApp(ui, server)
```
```{r gio-shiny-no-handler, fig.pos="H", echo=FALSE, fig.cap='Gio with input data'}
knitr::include_graphics("images/gio-shiny-input-no-handler.png")
```
One thing to note before moving on, the data is sent from the client to the server whether the inputs are used or not, though this likely will not negatively impact gio it can reduce performances if the callback function is triggered too frequently. For instance, an input value set when the user hovers a scatter plot might lead to the event being fired very frequently and too much data being sent to the server, slowing things down and providing a poor experience.
Therefore one might consider making the capture of such event optional, so the web browser\index{web browser} is not strained unless explicitly asked by the developer of the application. This could be implemented with a simple function that sets a simple logical variable in the `x` object that is used in JavaScript to check whether to implement the callback function.
```r
#' @export
gio_capture_events <- function(g) {
g$x$capture_events <- TRUE
return(g)
}
```
Then this could be used in JavaScript with an if statement.
```js
if(x.capture_events)
controller.onCountryPicked(callback);
```
One might also consider not sending back all the data. For instance, gio returns the coordinates of the selected country wherefrom arcs connect; this might be considered unnecessary. The code below only sets the input to the ISO code of the country selected.
```js
function callback (selectedCountry, relatedCountries) {
Shiny.setInputValue(
el.id + '_selected',
selectedCountry.ISOCode
);
}
```
## Input Handlers for Widgets {#shiny-widgets-handlers}
Input handlers were explored in an earlier chapter where it was used to transform the results of an image classification model from a list to a data.frame. We can apply something very similar to this input data; the "related countries" returned consist of edges and should be reshaped into a data.frame as well (that looks like `gio`'s input data).
Below we create a handler that is going to loop over the list (over each arc) and return a data frame.
```r
# handler.R
related_countries_handler <- function(x, session, inputname){
purrr::map_dfr(x, as.data.frame)
}
```
Then the handler must be registered with Shiny since handlers can only be registered once an excellent place to put it this is in the `.onLoad` function of the package.
```r
# zzz.R
related_countries_handler <- function(x, session, inputname){
purrr::map_dfr(x, as.data.frame)
}
.onLoad <- function(libname, pkgname) {
shiny::registerInputHandler(
"gio.related.countries",
related_countries_handler
)
}
```
Finally, we can reinstall the package with `devtools::install` and create a Shiny application to observe the change. In Figure \@ref(fig:gio-input-handler) we use a large example dataset and, since the input now returns a data frame, we can display the input value in a table.
```r
library(DT)
library(gio)
library(shiny)
# large sample data
url <- paste0(
"https://raw.githubusercontent.com/JohnCoene/",
"javascript-for-r/master/data/countries.json"
)
arcs <- jsonlite::fromJSON(url)
ui <- fluidPage(
gioOutput("globe"),
DTOutput("relatedCountries")
)
server <- function(input, output){
output$globe <- renderGio({
gio(arcs, i, e, v)
})
output$relatedCountries <- renderDT({
datatable(input$globe_related)
})
}
shinyApp(ui, server)
```
```{r gio-input-handler, fig.pos="H", echo=FALSE, fig.cap='Gio input data transformed to a data frame'}
knitr::include_graphics("images/gio-input-handler.png")
```
## R to Widgets {#shiny-widgets-r-to-widgets}
This book previously explored how to send data from the Shiny server to the front end; this section applies this to htmlwidgets. Currently, using gio in Shiny consists of generating the globe with the `renderGio` and complimentary `gioOutput` functions. This generates the complete visualisation, it creates the HTML\index{HTML} element where it places the globe, draws the arcs based on the data, sets the style, etc.
Now imagine that only one of those aspects needs changing, say the data, or the style, given the functions currently at hand one would have to redraw the entire visualisation, only this time changing the data or the style. This is inelegant and not efficient, most JavaScript visualisation libraries, including gio.js, will enable changing only certain aspects of the output without having to redraw it all from scratch.
Before we look into the implementation, let us create a Shiny application which would benefit from such a feature. The Shiny application below (Figure \@ref(fig:gio-shiny-error)) provides a drop-down menu to select between two datasets to draw on the globe, running it reveals an issue with gio though. Upon selecting a dataset with the drop down a second globe appears underneath the original one. This is because internally gio.js creates a new element (`<canvas>`) within the `<div>` created by `htmlwidgets` when running `init` regardless of whether one was already created. Therefore, every call to `init` creates a new `<canvas>` with a different globe. Note that most visualisation libraries _will not have that issue_, they will detect the existing output and override it instead.
```r
library(gio)
library(shiny)
arcs1 <- data.frame(
e = c("US", "CN", "RU"),
i = c("CN", "RU", "US"),
v = c(100, 120, 130)
)
arcs2 <- data.frame(
e = c("CN", "CN", "JP"),
i = c("IN", "JP", "US"),
v = c(100, 120, 130)
)
ui <- fluidPage(
selectInput(
"dataset",
"Select a dataset",
choices = c("First", "Second")
),
gioOutput("globe")
)
server <- function(input, output){
reactive_arcs <- reactive({
if(input$dataset == "First")
return(arcs1)
return(arcs2)
})
output$globe <- renderGio({
gio(reactive_arcs(), i, e, v)
})
}
shinyApp(ui, server)
```
```{r gio-shiny-error, fig.pos="H", echo=FALSE, fig.cap='Gio issue in shiny'}
knitr::include_graphics("images/gio-shiny-error.png")
```
A solution to this is to ensure the container (`el`) is empty before generating the visualisation. Incidentally, this can be executed with a JavaScript method previously used in this book: `innerHTML`.
```js
// gio.js
el.innerHTML = ''; // empty el
controller = new GIO.Controller(el);
```
Now, using the dropdown to switch between dataset does not generate a new visualisation.
We got sidetracked, but this had to be fixed. Ideally, when the user selects a dataset from the dropdown the entire visualisation\index{visualisation} is not redrawn, only the underlying data (the arcs) changes. To do so, a new set of functions divorced from the ones currently at hand needs to be created. This separation will allow leaving the already created functions as-is, using `gio` and its corresponding `renderValue` JavaScript function to initialise a visualisation, and have a separate family of functions dedicated to working with different JavaScript functions which circumvent `renderValue` and directly change aspects of the visualisation, such as the underlying dataset.
This involves a few moving parts, thankfully some of them were already explored, just not in the context of htmlwidgets. The scheme is to send data from R to JavaScript using the formerly exploited `session$sendCustomMessage`, then in JavaScript fetch the instance of the visualisation (`controller` in the case of gio) to interact with it (`e.g. controller.addData(data);`).
### Send Data from Widgets {#shiny-widgets-send-data}
Let us start by creating the R function used to send data to JavaScript. This is hopefully reminiscent of a previous chapter; this function sends a message to JavaScript where it is paired with "message handler" that does something with said message. Note that for consistency the function uses the same arguments as the other function of the package which accepts data: `gio`.
```r
#' @export
gio_send_data <- function(id, data, source, target, value,
session = shiny::getDefaultReactiveDomain()){
data <- dplyr::select(
data,
i = {{ source }},
e = {{ target }},
v = {{ value }}
)
message <- list(id = id, data = data)
session$sendCustomMessage("send-data", message)
}
```
The function takes the id of the visualisation the data is destined for, the data object itself, and a Shiny session used to send the data. The id of the visualisation is sent as part of the message and will be used to retrieve the instance of the visualisation and subsequently apply the new dataset. Note that we give this message the `send-data` identifier, this will be needed when we write its handler.
There is one caveat that will make it such that the above will not work. To have gio.js work with data changes had to be made to the serialiser\index{serialise} (using the `TOJSON_ARGS` attribute), this cannot be used here. The data being sent with Shiny via the session object the problem reoccurs: Shiny, like htmlwidgets, serialises data frames column-wise and not row-wise. One can preview the way Shiny serialises\index{serialise} with `shiny:::toJSON` (three-colon).
```{r}
# preview shiny serialisation
shiny:::toJSON(arcs)
```
Unfortunately this serialiser cannot be changed, therefore we have to reformat the data to a list which resembles the JSON\index{JSON} output desired, using `apply` to turn every row into a list will do the job in most cases.
```r
#' @export
gio_send_data <- function(id, data, source, target, value,
session = shiny::getDefaultReactiveDomain()){
data <- dplyr::select(
data,
i = {{ source }},
e = {{ target }},
v = {{ value }}
)
message <- list(id = id, data = apply(data, 1, as.list))
session$sendCustomMessage("send-data", message)
}
```
### Retrieve Widget Instance {#shiny-widgets-retrieve}
We will need to be able to access the instance of the visualisation (`controller`) outside of the function `factory`. This can be made accessible by adding a function (technically a method) that returns the `controller`. Below we create a function called `getGlobe` which returns the `controller`.
```js
HTMLWidgets.widget({
name: 'gio',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
var controller;
// selection handle
var sel_handle = new crosstalk.SelectionHandle();
sel_handle.on("change", function(e) {
if (e.sender !== sel_handle) {
// clear selection
}
controller.switchCountry(e.value[0]);
});
return {
renderValue: function(x) {
el.innerHTML = '';
controller = new GIO.Controller(el, x.configs);
// group
sel_handle.setGroup(x.crosstalk.group);
// add data
controller.addData(x.data);
controller.setStyle(x.style);
// callback
controller.onCountryPicked(callback);
function callback (selectedCountry, relatedCountries) {
sel_handle.set([selectedCountry.ISOCode]);
Shiny.setInputValue(
el.id + '_selected',
selectedCountry
);
Shiny.setInputValue(
el.id + '_related:gio.related.countries',
relatedCountries
);
}
// use stats
if(x.stats)
controller.enableStats();
// render
controller.init();
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
controller.resizeUpdate()
},
getGlobe: function(){
return controller;
}
};
}
});
```
Then, anywhere in the `gio.js` file, we create a function that uses `HTMLWidgets.find` to retrieve the instance of the htmlwidgets it takes a selector as input. We concatenate the pound sign to select the widget based on its id (`#id`, `.class`). This, in effect, returns an object, which includes all the functions returned by the `factory` function: `renderValue`, `resize`, and `getGlobe`. We can therefore use the `getGlobe` method available from that object to retrieve the actual `controller`.
```js
// gio.js
function get_gio(id){
var widget = HTMLWidgets.find("#" + id);
var globe = widget.getGlobe();
return globe;
}
```
### Handle Data {#shiny-widgets-handle-data}
We can now turn our attention to actually applying the data sent from the R server to the visualisation: the "message handler." Registering the message handler is only relevant if Shiny is running. Therefore htmlwidgets comes with a function to check whether that is the case, which is useful to avoid needless errors. We can thus use it in an if-statement in which all message handlers will be registered.
```js
// gio.js
// check if shiny running
if (HTMLWidgets.shinyMode){
// send-data message handler
Shiny.addCustomMessageHandler(
type = 'send-data', function(message) {
});
}
```
What the currently empty `send-data` message handler should do is fetch the widget using the `id` sent from R with the `get_gio` function and then use the `addData` method to override the previously-defined arcs.
```js
// gio.js
// check if shiny running
if (HTMLWidgets.shinyMode){
// send-data message handler
Shiny.addCustomMessageHandler(
type = 'send-data', function(message) {
// retrieve controller
var controller = get_gio(message.id);
// add data
controller.addData(message.data);
});
}
```
We can then build a Shiny application to test the new functionality.
```r
library(gio)
library(shiny)
# two phoney datasets
arcs1 <- data.frame(
e = c("US", "CN", "RU"),
i = c("CN", "RU", "US"),
v = c(100, 120, 130)
)
arcs2 <- data.frame(
e = c("CN", "CN", "JP"),
i = c("IN", "JP", "US"),
v = c(100, 120, 130)
)
ui <- fluidPage(
selectInput(
"dataset",
"Select a dataset",
choices = c("First", "Second")
),
gioOutput("globe")
)
server <- function(input, output){
reactive_arcs <- reactive({
if(input$dataset == "First")
return(arcs1)
return(arcs2)
})
output$globe <- renderGio({
gio(arcs1, i, e, v)
})
observeEvent(input$dataset, {
if(input$dataset == "First")
data <- arcs1
else
data <- arcs2
gio_send_data("globe", data, i, e, v)
})
}
shinyApp(ui, server)
```
Switching dataset with the dropdown only changes the data; it makes for a much smoother animation, even the difference in the speed at which the effect is visible on the visualisation is perceivable.
## Proxy Function {#shiny-widgets-proxy}
Before we add other similar functions, we ought to pause and consider the API this provides the user. There are two points, every function such as `gio_send_data`, will need to accept the `id` and `session` arguments. It will be tedious to so every time, following the old "don't repeat yourself" adage we ought to abstract this further.
This can be remedied to by introducing what is often referred to as a "proxy." A proxy is just a representation of the graph, or pragmatically, an object that contains the id of the visualisation\index{visualisation} and a Shiny session. This object can subsequently be piped\index{pipe} to other functions, thereby providing not only a cleaner but also a more consistent API.
```r
#' @export
gio_proxy <- function(id,
session = shiny::getDefaultReactiveDomain()
){
list(id = id, session = session)
}
```
Above we create a function called `gio_proxy`; it takes the `id` of the chart one wants to build a proxy for, as well as the Shiny `session`, these are returned in the form of a list. Next, we should adapt the `gio_send_data` so that it accepts the output of `gioProxy` instead of the `id` and `session` as done previously. In order to allow chaining such functions, we also make sure it returns the `proxy` object.
```r
#' @export
gio_send_data <- function(proxy, data){
message <- list(id = proxy$id, data = apply(data, 1, as.list))
proxy$session$sendCustomMessage("send-data", message)
return(proxy)
}
```
## Clear Data {#shiny-widgets-clear-data}
In order to actively demonstrate the advantage of the "proxy" function as well as to hammer home how such functions work, we shall build another, which removes all data from the globe. In JavaScript, it's as simple as `controller.clearData();`.
The journey starts with the R code, where we create a new function that sends a message to clear the data; the message only needs to contain the id of the visualisation from which data needs to be cleared, as before it will be used to retrieve the `controller` from which the `clearData` method is available.
```r
#' @export
gio_clear_data <- function(proxy){
message <- list(id = proxy$id)
proxy$session$sendCustomMessage("clear-data", message)
return(proxy)
}
```
Now onto the JavaScript code to catch that message and actually clear the data from the globe. That function is very similar to what was previously shown, the only difference is the name of the message handler and the method used on the controller.
```js
// gio.js
Shiny.addCustomMessageHandler(
type = 'clear-data', function(message) {
var controller = get_gio(message.id);
controller.clearData();
});
```
Then one can build an application to test that new function. We build a Shiny application with a button to add the data to the visualisation and another to clear data from it, as shown in Figure \@ref(fig:giod-shiny-clear-data).
```r
library(gio)
library(shiny)
# phoney dataset
arcs <- data.frame(
e = c("US", "CN", "RU"),
i = c("CN", "RU", "US"),
v = c(100, 120, 130)
)
ui <- fluidPage(
actionButton("load", "Load data"),
actionButton("clear", "Clear data"),
gioOutput("globe")
)
server <- function(input, output){
output$globe <- renderGio({
gio(arcs, i, e, v)
})
observeEvent(input$load, {
gio_proxy("globe") %>%
gio_send_data(arcs, i, e, v)
})
observeEvent(input$clear, {
gio_proxy("globe") %>%
gio_clear_data()
})
}
shinyApp(ui, server)
```
```{r giod-shiny-clear-data, fig.pos="H", echo=FALSE, fig.cap='Gio with clear data proxy'}
knitr::include_graphics("images/gio-shiny-clear.png")
```
## Update the Widget {#shiny-widgets-update}
The previous proxies defined worked for reasons unbeknownst to the author of this book. It will not work with all methods. The reason it will not work is one that is likely to occur with many other visualisation libraries. For instance, one can attempt to develop a function to dynamically change the style without having to redraw the entire globe, starting again with the R function.
```r
#' @export
gio_set_style <- function(proxy, style){
message <- list(id = proxy$id, style = style)
proxy$session$sendCustomMessage("set-style", message)
return(proxy)
}
```
Then, adding the JavaScript handler.
```js
Shiny.addCustomMessageHandler(
type = 'set-style', function(message) {
var controller = get_gio(message.id);
controller.setStyle(message.style);
});
```
At this stage, one can try the function in a Shiny application, but it will not work because most such methods that change underlying aspects of a visualisation will not be reflected in real-time. Gio.js, and many other libraries, will require one to explicitly ask for an update so the changes take effect. This has multiple advantages, one can stack multiple visual changes to execute them at the same time, and one can manage the load on the front end.
```js
Shiny.addCustomMessageHandler(
type = 'set-style', function(message) {
var controller = get_gio(message.id);
controller.setStyle(message.style);
controller.update(); // force update the visualisation
});
```
This forces the chart to update, applying the new style. Below we write an application that provides a dropdown to switch between two styles (Figure \@ref(fig:gio-shiny-style)).
```r
library(gio)
library(shiny)
# two phoney datasets
arcs <- data.frame(
e = c("US", "CN", "RU"),
i = c("CN", "RU", "US"),
v = c(100, 120, 130)
)
ui <- fluidPage(
selectInput(
"style",
"Select a style",
choices = c("blueInk", "earlySpring")
),
gioOutput("globe")
)
server <- function(input, output){
output$globe <- renderGio({
gio(arcs1, i, e , v)
})
observeEvent(input$style, {
gio_proxy("globe") %>%
gio_set_style(input$style)
})
}
shinyApp(ui, server)
```
```{r gio-shiny-style, fig.pos="H", echo=FALSE, fig.cap='Gio with dynamic style'}
knitr::include_graphics("images/gio-shiny-style.png")
```