-
Notifications
You must be signed in to change notification settings - Fork 124
/
Copy pathjavascript.Rmd
532 lines (410 loc) · 32.8 KB
/
javascript.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
# (PART) Event handling in JavaScript {-}
# Introduction {#javascript}
\sectionmark{Introduction}
The same plotly.js events that we leveraged in **shiny** through `event_data()` in Section \@ref(shiny-plotly-inputs) can also be handled using JavaScript (JS) instead of R, which offers numerous advantages:
1. Your web browser natively understands JS, so writing event handlers in JS instead of R offers the potential of having a purely client-side webpage instead of client-server app, making the end result easier to share, host, and maintain.^[Comparatively speaking, client-server apps require way more runtime software dependencies. In the case of **shiny** apps, RStudio provides accessible resources for hosting shiny apps https://shiny.rstudio.com/articles/#deployment, but using these services to host apps that encounter lots of traffic will either cost money and/or time for setting up the proper computational infrastructure.] That being said, the effort required to rewrite a 'heavy-duty' client-server app as a 'light-weight' client-side webpage isn't always worth the investment; so before doing so, you should have a clear vision for the interactivity you desire, be fairly confident that vision won't change in the future, and have a use-case that doesn't require sophisticated statistical computations to be run dynamically in response to user events (i.e., it's not practical or feasible to pre-compute).^[Compared to JS, R has way more facilities for statistical computing.]
2. There are certain things you can do in JS that you can't necessarily do in R, such as accessing the web browser's `window` API to open hyperlinks in response to plotly.js click events (e.g., Figure \@ref(fig:click-open)).
3. JS event handlers can be noticeably faster than running comparable code on an external R process, especially with a slow internet connection.
For those new to JS, you may find it helpful to compare code examples from this part of the book to code examples from Section \@ref(proxies).^[In fact, converting some examples from that section from **shiny** to JavaScript/HTML would be a good exercise!] That's because, the `plotlyProxy()` interface which powers that section is just an R interface to [plotly.js's JavaScript functions](https://plot.ly/javascript/plotlyjs-function-reference), and is primarily for updating graphs within an event handler. Therefore, if you understand how the examples in that section work, you can translate a good amount of that knowledge to a JS context as well.
However, when handling these events in JS instead of R, you'll want to be familiar with JavaScript Object Notation (JSON), which is introduced in Chapter \@ref(json). That chapter also offers a minimal JS programming foundation for manipulating JSON, then Chapter \@ref(js-event-handlers) quickly covers how to attach JS event handlers to various plotly.js events, which is really all that's required to loosely understand the bulk of the examples in Chapters \@ref(supplying-custom-data) and \@ref(client-side).
<!--
This portion of the book focuses on writing custom event handlers to commonly used plotly.js events, but a lot of the same tools and techniques covered here could be used to extend these graphs in other ways as well.
-->
An important thing to know about when doing any sort of web development is how to open and navigate to the web browser's developer tools. Through the developer tools, you can access a JS console to run and test out JS code, inspect and debug the JS/CSS/HTML code behind a website, query components of the Document Object Model (DOM), inspect network traffic, and much more. In our use case of writing plotly.js event handlers, the JS console will come in handy especially to see what information a plotly.js event is firing (think of it as the analog of printing output to the R console in a shiny app), before writing the actual event handler. To open the console of a web browser (including RStudio), you can likely do: right-click -> "Inspect Element" -> "Console" tab (or similar).
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}}
One way to write a custom event handler for a **plotly** graph is to leverage the `onRender()` function from the **htmlwidgets** package. This R function accepts a JS function as a string and calls that function when the widget is done rendering in the browser. The JS function needs (at least) one argument, `el`, which is the DOM element containing the **plotly** graph. It's worth noting that `htmlwidgets::onRender()` serves a more general purpose than registering plotly.js event handlers, so you could use it to a bunch of other things, such as adding conditional logic based on information stored in `el`. Figure \@ref(fig:console-log) shows how you could use `onRender()` to log (and inspect) the DOM element to your browser's JS console. To demonstrate some useful DOM element properties, Figure \@ref(fig:console-log) uses Firefox to inspect the element as a global variable, but as Figure \@ref(fig:console-log-event) shows, Chrome currently offers better tools for code debugging the JS function provided to `onRender()` (e.g., setting breakpoints in virtual memory).
```r
library(htmlwidgets)
plot_ly(z = ~volcano) %>%
onRender("function(el) { console.log(el); }")
```
```{r console-log, echo = FALSE, fig.cap="(ref:console-log)"}
include_vimeo("307598574", height = "700")
```
If you're completely new to JS and JSON, Chapter \@ref(json) provides a foundation for understanding the subsequent sections, but those who are already familiar can skip ahead to Chapter \@ref(js-event-handlers), which shows how to log plotly.js event data to the JS console via `htmlwidgets::onRender()`.
# Working with JSON {#json}
\sectionmark{Introduction}
JavaScript (JS) and other web technologies are intimidating and time-consuming to learn, but by borrowing some knowledge of R's data structures^[If you'd like a nice succinct overview on the topic, see http://adv-r.had.co.nz/Data-structures.html], we can get up and running with useful examples fairly quickly. JavaScript Object Notation (JSON) is a popular data-interchange format that JS uses to work with data. As it turns out, working with JSON in JS is somewhat similar to working with `list()`s in R; both are recursive and heterogeneous data structures that have similar semantics for accessing values. In JSON, there are three basic building blocks: objects, arrays, and primitive data types (e.g., number, string, Boolean, `null`, `undefined`).
Loosely speaking, a JSON array is similar to a un-named `list()` in R and a JSON object is similar to an un-named `list()`. In fact, if you're already comfortable creating and subsetting named and un-named `list()`s in R, you can transfer some of that knowledge to JSON arrays and objects.
## Assignment, subsetting, and iteration
In R, the `<-` operator assigns a value to a name, and the `[[` operator extracts a list element by index:
```r
arr <- list("hello", "world", 10)
arr[[1]]
#> "hello"
```
In JS, the `=` assigns a value to a name. When assigning a new name, you should include the `var` keyword (or similar) to avoid creation of a global variable. The `[` operator extracts list elements by index, but **be careful, indexing in JS starts at 0 (not 1)!**
```js
var arr = ["hello", "world", 10];
arr[0]
// "hello"
```
In R, the `$` and `[[` operator can be used to extract list elements by name. The difference is that `$` does partial matching of names, while `[[` requires the exact name.
```r
obj <- list(x = c("hello", "world"), zoo = 10)
obj$z
#> 10
obj[["zoo"]]
#> 10
```
In JS, the `.` and `[` operator can be used to extract list elements by name. In either case, the naming must be exact.
```js
var obj = {
x: ["hello", "world"],
zoo: 10
}
obj.zoo
// 10
obj['zoo']
// 10
```
Unlike R `list()`s, arrays and objects in JS come with properties and methods that can be accessed via the `.` operator. Arrays, in particular, have a `length` property and a `map()` method for applying a function to each array element:
```js
arr.length
// 3
arr.map(function(item) { return item + 1; });
// ["hello1", "world1", 11]
```
In R, both the `lapply()` and `purrr::map()` family of functions provide a similar functional interface. Also, note that operators like `+` in JS do even more type coercion than R, so although `item + 1` works for strings in JS, it would throw an error in R (and that's ok, most times you probably don't want to add a string to a number). If instead, you wanted to only add 1 to numeric values, you could use `is.numeric()` in R within an if else statement.
```r
purrr::map(arr, function(item) {
if (is.numeric(item)) item + 1 else item
})
#> [[1]]
#> [1] "hello"
#>
#> [[2]]
#> [1] "world"
#>
#> [[3]]
#> [1] 11
```
In JS, you can use the `typeof` keyword to get the data type as well as the conditional ternary operator (`condition ? exprT : exprF`) to achieve the same task.
```js
arr.map(function(item) {
return typeof item == "number" ? item + 1 : item;
});
// ["hello", "world", 11]
```
There are a handful of other useful [array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) and [object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) methods, but to keep things focused, we'll only cover what's required to comprehend in Chapter \@ref(js-event-handlers). A couple of examples in that section use the `filter()` method, which like `map()` applies a function to each array element, but expects a logical expression and returns only the elements that meet the condition.
```js
arr.filter(function(item) { return typeof item == "string"; });
// ["hello", "world"]
```
## Mapping R to JSON
\indexc{jsonlite::toJSON()}
In R, unlike JSON, there is no distinction between scalars and vectors of length 1. That means there is ambiguity as to what a vector of length 1 in R should map to in JSON. The **jsonlite** package defaults to an array of length 1, but this can be avoided by setting `auto_unbox = TRUE`.
```r
jsonlite::toJSON("A string in R")
#> ["A string in R"]
jsonlite::toJSON("A string in R", auto_unbox = TRUE)
#> "A string in R"
```
It's worth noting that plotly.js, which consumes JSON objects, has specific expectations and rules about scalars versus arrays of length 1. If you're calling the plotly.js library directly in JS, as we'll see later in Chapter \@ref(js-event-handlers), you'll need to be mindful of the difference between scalars and arrays of length 1. Some attributes, like `text` and `marker.size`, accept both scalars and arrays and apply different rules based on the difference. Some other attributes, like `x`, `y`, and `z`, only accept arrays and will error out if given a scalar. To learn about these rules and expectations, you can use the `schema()` function from R to inspect plotly.js's specification as shown in Figure \@ref(fig:json-schema). Note that attributes with a `val_type` of `'data_array'` require an array while attributes with an `arrayOk: true` field accept either scalars or arrays.
\indexc{schema()}
```r
schema()
```
```{r json-schema, echo = FALSE, fig.cap = "(ref:json-schema)"}
include_vimeo("307597826")
```
In JSON, unlike R, there is no distinction between a heterogeneous and homogeneous collection of data types. In other words, in R, there is an important difference between `list(1, 2, 3)` and `c(1, 2, 3)` (the latter is an atomic vector and has a different set of rules). In JSON, there is no strict notion of a homogeneous collection, so working with JSON arrays is essentially like being forced to use `list()` in R. This subtle fact can lead to some surprising results when trying to serialize R vectors as JSON arrays. For instance, if you wanted to create a JSON array, say `[1,"a",true]` using R objects, you may be tempted to do the following:
```r
jsonlite::toJSON(c(1, "a", TRUE))
#> ["1","a","TRUE"]
```
But this actually creates an array of strings instead of the array with a number, string, and Boolean that we desire. The problems actually lie in the fact that `c()` coerces the collection of values into an atomic vector. Instead, you should use `list()` over `c()`:
```r
jsonlite::toJSON(list(1, "a", TRUE), auto_unbox = TRUE)
#> [1,"a",true]
```
# Adding custom event handlers {#js-event-handlers}
\sectionmark{Introduction}
When using `onRender()` to provide a JS function to be called upon static render of a **plotly** object, the relevant DOM element (`el`) has an `on()` method that accepts a function to be called whenever a plotly.js (or DOM) event occurs on that DOM element. Currently all plotly.js event handlers accept a function with a single argument, and that argument either contains nothing (e.g., `"plotly_afterplot"`, etc.) or a single object with all the relevant information about the event (e.g., `"plotly_hover"`, `"plotly_selected"`, etc.). Figure \@ref(fig:console-log-event) logs and inspects data (`d`) emitted during the `"plotly_hover"`, `"plotly_click"`, and `"plotly_selected"` events. The object emitted for these events includes a key, named `points`, with information tying the selection back to the input data. The `points` key is always an array of object(s) where each object represents a different data point. This object contains any supplied `customdata`, the relevant `x`/`y` location, and a reference back to the input `data`.
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!Logging event data}
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!plotly\_hover}
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!plotly\_click}
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!plotly\_selected}
```r
library(htmlwidgets)
plot_ly(mtcars, x = ~wt, y = ~mpg) %>%
onRender("
function(el) {
el.on('plotly_hover', function(d) {
console.log('Hover: ', d);
});
el.on('plotly_click', function(d) {
console.log('Click: ', d);
});
el.on('plotly_selected', function(d) {
console.log('Select: ', d);
});
}
")
```
```{r console-log-event, echo = FALSE, fig.cap="(ref:console-log-event)"}
include_vimeo("307597974", height = "700")
```
# Supplying custom data
\sectionmark{Introduction}
As covered in Section \@ref(shiny-plotly-inputs), it's often useful to supply meta-information (i.e., custom data) to graphical marker(s) and use that information when responding to a event. For example, suppose we'd like each point in a scatterplot to act like a hyperlink to a different webpage. In order to do so, we can supply a url to each point (as metadata) and instruct the browser to open the relevant hyperlink on a click event. Figure \@ref(fig:click-open) does exactly this by supplying urls to each point in R through the `customdata` attribute and defining a custom JS event to `window.open()` the relevant `url` upon a click event. In this case, since each point represents one row of data, the `d.point` is an array of length 1, so we may obtain the `url` of the clicked point with `d.points[0].customdata`.
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!Open hyperlink on click}
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!customdata@\texttt{customdata}}
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!plotly\_click}
```r
library(htmlwidgets)
p <- plot_ly(mtcars, x = ~wt, y = ~mpg) %>%
add_markers(
text = rownames(mtcars),
customdata = paste0("http://google.com/#q=", rownames(mtcars))
)
onRender(
p, "
function(el) {
el.on('plotly_click', function(d) {
var url = d.points[0].customdata;
window.open(url);
});
}
")
```
```{r click-open, echo = FALSE, fig.cap="(ref:click-open)"}
include_vimeo("307598425")
```
In addition to using `window.open()` to open the `url`, we could also add it to the plot as an annotation using the plotly.js function `Plotly.relayout()`, as done in Figure \@ref(fig:hover-annotate). Moreover, since plotly annotations support HTML markup, we can also treat that url as a true HTML hyperlink by wrapping it in an HTML `<a>` tag. In cases where your JS function starts to get complex, it can help to put that JS function in its own file, then use the R function `readLines()` to read it in as a string and pass along `onRender()` as done below:
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!customdata@\texttt{customdata}}
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!plotly\_hover}
```r
onRender(p, readLines("js/hover-hyperlink.js"))
```
```{js, eval = FALSE, summary = "Click to show the 'js/hover-hyperlink.js' file", code = readLines("js/hover-hyperlink.js")}
```
```{r hover-annotate, echo = FALSE, fig.cap="(ref:hover-annotate)"}
include_vimeo("327588345")
```
```{block, type='rmdtip'}
When using `Plotly.relayout()`, or any other plotly.js function to modify a plot, you'll need to know the id attribute of the relevant DOM instance that you want to manipulate. When working with a single object, you can simply use `el.id` to access the id attribute of that DOM instance. However, when trying to target another object, it gets trickier because id attributes are randomly generated by **htmlwidgets**. In that case, you likely want to pre-specify the id attribute so you can reference it client-side. You can pre-specify the id for any **htmlwidgets** object, say `widget`, by doing `widget$elementId <- "myID"`.
```
The `customdata` attribute can hold any R object that can be serialized as JSON, so you could, for example, attach complex data to markers/lines/text/etc. using base64 strings. This could be useful for a number of things such as displaying an image on hover or click. For security reasons, plotly.js doesn't allow inserting images in the tooltip, but you can always define your own tooltip by hiding the tooltip (`hoverinfo='none'`), then populating your own tooltip with suitable manipulation of the DOM in response to `"plotly_hover"`/`"plotly_unhover"` events. Figure \@ref(fig:tooltip-image) demonstrates how to leverage this infrastructure to display a PNG image in the top-left corner of a graph whenever a text label is hovered upon.^[As long as you are not allowing down-stream users to input paths to the input files (e.g., in a **shiny** app), you shouldn't need to worry about the security of this example.]
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!Display image on hover}
```r
x <- 1:3
y <- 1:3
logos <- c("r-logo", "penguin", "rstudio")
# base64 encoded string of each image
uris <- purrr::map_chr(
logos, ~ base64enc::dataURI(file = sprintf("images/%s.png", .x))
)
# hoverinfo = "none" will hide the plotly.js tooltip, but the
# plotly_hover event will still fire
plot_ly(hoverinfo = "none") %>%
add_text(x = x, y = y, customdata = uris, text = logos) %>%
htmlwidgets::onRender(readLines("js/tooltip-image.js"))
```
```{js, eval = FALSE, summary = "Click to show the 'js/tooltip-image.js' file", code = readLines("js/tooltip-image.js")}
```
```{r tooltip-image, echo = FALSE, fig.cap = "(ref:tooltip-image)"}
include_vimeo("310463565")
```
It's worth noting that the JavaScript that powers Figure \@ref(fig:tooltip-image) works for other Cartesian charts, even `heatmap` (as shown in Figure \@ref(fig:tooltip-image-heatmap)), but it would need to be adapted for 3D chart types.
```r
plot_ly(hoverinfo = "none") %>%
add_heatmap(
z = matrix(1:9, nrow = 3),
customdata = matrix(uris, nrow = 3, ncol = 3)
) %>%
htmlwidgets::onRender(readLines("js/tooltip-image.js"))
```
```{r tooltip-image-heatmap, echo = FALSE, fig.cap = "(ref:tooltip-image-heatmap)"}
include_vimeo("310466116")
```
On the JS side, the `customdata` attribute is designed to support _any_ JS array of appropriate length, so if you need to supply numerous custom values to particular marker(s), list-columns in R provide a nice way to do so. Figure \@ref(fig:tx-annotate) leverages this idea to bind both the `city` and `sales` values to each point along a time series and display those values on hover. It also demonstrates how one can use the graphical querying framework from Section \@ref(graphical-queries) in tandem with a custom JS event. That is, `highlight_key()` and `highlight()` control the highlighting of the time series, while the custom JS event adds the plot annotation (all based on the same `"plotly_hover"` event). In this case, the highlighting, annotations, and circle shapes are triggered by a `"plotly_hover"` event and they all work in tandem because event handlers are cumulative. That means, if you wanted, you could register multiple custom handlers for a particular event.
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!Annotate plot on hover}
```r
library(purrr)
sales_hover <- txhousing %>%
group_by(city) %>%
highlight_key(~city) %>%
plot_ly(x = ~date, y = ~median, hoverinfo = "name") %>%
add_lines(customdata = ~map2(city, sales, ~list(.x, .y))) %>%
highlight("plotly_hover")
onRender(sales_hover, readLines("js/tx-annotate.js"))
```
```{js, eval = FALSE, summary = "Click to show the 'js/tx-annotate.js' file", code = readLines("js/tx-annotate.js")}
```
```{r tx-annotate, echo = FALSE, fig.cap ="(ref:tx-annotate)"}
include_vimeo("307597956")
```
Sometimes supplying and accessing `customdata` alone is not quite enough for the task at hand. For instance, what if we wish to add the average monthly sales to the annotation for the city of interest in Figure \@ref(fig:tx-annotate)? In cases like this, we may need to use `customdata` to query a portion of the plot's input data, like Figure \@ref(fig:tx-annotate) does to compute and display average sales for a given city. This implementation leverages the fact that each selected point (`pt`) contains a reference to the entire trace it derives from (`pt.data`). As discussion behind Figure \@ref(fig:scatter-lines) noted, this particular plot has a *single trace* and uses missing values to create separate lines for each city. As a result, `pt.data.customdata` contains all the `customdata` we supplied from the R side, so to get all the `sales` for a given city, we first need to filter that array down to only the elements that belong to that city (while being careful of missing values!).
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!Display summary on click}
```r
onRender(sales_hover, readLines("js/tx-mean-sales.js"))
```
```{js, eval = FALSE, summary = "Click to show the 'js/tx-mean-sales.js' file", code = readLines("js/tx-mean-sales.js")}
```
```{r tx-mean-sales, echo = FALSE, fig.cap ="(ref:tx-mean-sales)"}
include_vimeo("307598016")
```
Figure \@ref(fig:tx-inset-plot) uses the same `customdata` supplied to Figure \@ref(fig:tx-mean-sales) in order to display a histogram of monthly sales for the relevant city on hover. In addition, it displays a vertical line on the histogram to reflect the monthly sales for the point closest to the mouse cursor. To do all this efficiently, it's best to add the histogram trace on the first hover event using `Plotly.addTraces()`, then supply different `sales` data via `Plotly.restyle()` (generally speaking, `restyle()` is way less expensive than `addTraces()`). That's why the implementation leverages the fact that the DOM element (`el`) contains a reference to the current graph data (`el.data`). If the current graph has a trace with a type of histogram, then it adds a histogram trace; otherwise, it supplies new `x` values to the histogram.
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!Display embedded plot on click}
```r
sales_hover %>%
onRender(readLines("js/tx-annotate.js")) %>%
onRender(readLines("js/tx-inset-plot.js"))
```
```{js, eval = FALSE, summary = "Click to show the 'js/tx-inset-plot.js' file", code = readLines("js/tx-inset-plot.js")}
```
```{r tx-inset-plot, echo = FALSE, fig.cap ="(ref:tx-inset-plot)"}
include_vimeo("307598624")
```
# Leveraging web technologies from R {#client-side}
## Web infrastructure
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!Serve external files}
\index{HTML in R}
Sometimes supplying `customdata` isn't the best way to achieve a particular interactive feature. In those cases, you likely want to leverage other R lower-level interfaces to web technologies. Recall from Section \@ref(arranging-htmlwidgets) that **htmlwidgets** objects are a special case of **htmltools** tags. That means you can always complement your widget(s) with arbitrary HTML content by adding additional tags. Figure \@ref(fig:correlation-client-side) leverages this idea to place an empty HTML `<div>` container below the correlation heatmap which is then populated with a **plotly** scatterplot upon clicking a cell. As it turns out, you *could* implement Figure \@ref(fig:correlation-client-side) by binding x/y data to each heatmap cell via `customdata`, but that would require the browser to store twice the amount of data as what's required here. Instead, this approach serializes the input data (`mtcars`) into a JSON file via **jsonlite** so the webpage can read and parse the full dataset once and select just the two required columns when required (on click). There are a lot of ways to read JSON in JavaScript, but here we use the d3.js library's `d3.json()` since **plotly** already comes bundled with the library [@d3js]. Also, since the HTML file is reading the JSON from disk, most browsers won't render the HTML file directly (at least, by default, for security reasons). To get around that, we can start up a simple web server from R using **servr** to serve both the HTML and JSON in a way that your browser will deem safe to run [@servr].
```{r, eval = FALSE, summary = "Click to see code"}
library(plotly)
library(htmltools)
nms <- names(mtcars)
p <- plot_ly(colors = "RdBu") %>%
add_heatmap(
x = nms,
y = nms,
z = ~round(cor(mtcars), 3)
) %>%
onRender("
function(el) {
Plotly.d3.json('mtcars.json', function(mtcars) {
el.on('plotly_click', function(d) {
var x = d.points[0].x;
var y = d.points[0].y;
var trace = {
x: mtcars[x],
y: mtcars[y],
mode: 'markers'
};
Plotly.newPlot('filtered-plot', [trace]);
});
});
}
")
# In a temporary directory, save the mtcars dataset as json and
# the html to an index.html file, then open via a web server
withr::with_path(tempdir(), {
jsonlite::write_json(as.list(mtcars), "mtcars.json")
html <- tagList(p, tags$div(id = 'filtered-plot'))
save_html(html, "index.html")
if (interactive()) servr::httd()
})
```
```{r correlation-client-side, echo = FALSE, fig.cap ="(ref:correlation-client-side)"}
include_vimeo("307598118", height = "700")
```
## Modern JS and React {#react}
\index{htmlwidgets::onRender()@\texttt{htmlwidgets::onRender()}!Modern JavaScript}
All the JavaScript (JS) we've seen thus far is natively supported by modern web browsers, but for larger projects, you may want to leverage modern versions of JS (i.e., ES6, ES7, etc.) and modern JS development tools (e.g., Babel, Webpack, etc.) for compiling modern JS to a version that all browsers can support (i.e., ES2015). The current landscape of JS development tooling is large, complex, fragmented, difficult for non-experts to navigate, and mostly beyond the scope of this book. However, thanks to R packages like **V8**, **reactR**, and **runpkg**, it turns out we can effectively leverage React^[React is a modern JavaScript library, backed by Facebook, for building and distributing components of a website -- <https://reactjs.org/>] components^[There are thousands of React components available. To get a sense of what's available, see this list <https://github.com/brillout/awesome-react-components>. ] from R without fussing with system commands or setting up a complicated JS build toolchain.
The R package **runpkg** makes it easy to download any [npm](https://www.npmjs.com/) (the main repository network for JS) package (via <https://unpkg.com/>) and include it in a webpage generated through the **htmltools** package [@runpkg]. It does so by returning a `htmltools::htmlDependency()` object which encapsulates the downloaded files and includes the JS scripts (or CSS stylesheets) into any page that depends on that object. Here we use it to download a standalone bundle of a React library for rendering all sorts of different video formats, called `react-player`.
```r
react_player <- runpkg::download_files(
"react-player",
"dist/ReactPlayer.standalone.js"
)
```
This `react-player` library provides a function called `renderReactPlayer()` that requires a placeholder (i.e., a DOM element) for inserting the video as well as a url (or file path) to the video. Figure \@ref(fig:react-player) demonstrates how we could use it to render a YouTube video in response to a **plotly** click event:
```{r, eval = FALSE, summary = "Click to see code"}
library(htmltools)
# the video placeholder
video <- tags$div(id = "video", align = "center")
# upon clicking the marker, populate a video
# in the DOM element with an id of 'video'
p <- plot_ly(x = 1, y = 1, size = I(50)) %>%
add_text(
text = emo::ji("rofl"),
customdata = "https://www.youtube.com/watch?v=oHg5SJYRHA0",
hovertext = "Click me!",
hoverinfo = "text"
) %>%
onRender(
"function(el) {
var container = document.getElementById('video');
el.on('plotly_click', function(d) {
var url = d.points[0].customdata;
renderReactPlayer(container, {url: url, playing: true});
})
}"
)
# create the HTML page
browsable(tagList(p, video, react_player))
```
```{r react-player, echo = FALSE, fig.cap = "(ref:react-player)"}
include_vimeo("315308561", height = "700")
```
This `react-player` React library is rather unique in that it provides a standalone function, `renderReactPlayer()`, that enables rendering of a React component without loading React itself or leveraging special React syntax like JSX. It's more likely that the React component library will explicitly require you to import both React and ReactDOM. You could use **runpkg** to download these React/ReactDOM as well, but the `html_dependency_react()` function from **reactR** package makes this even easier [@reactR]. Furthermore, **reactR** provides a `babel_transform()` function which will compile modern JS (e.g., ES6, ES2017, etc.) as well as special React markup (e.g., JSX) to a version of JS that all browsers support (e.g., ES5). For a toy example, Figure \@ref(fig:babel) demonstrates how one could leverage ES6, React, and React's JSX syntax to populate a `<h1>` title filled with a `customdata` message in response to a **plotly** click event.
```{r, eval = FALSE, summary = "Click to see code"}
library(reactR)
# a placeholder for our react 'app'
app <- tags$div(id = "app")
p <- plot_ly(x = 1, y = 1) %>%
add_markers(customdata = "Powered by React") %>%
onRender(babel_transform(
"el => {
el.on('plotly_click', d => {
let msg = d.points[0].customdata;
ReactDOM.render(
<h1>{msg}</h1>,
document.getElementById('app')
)
})
}"
))
# create the HTML page
browsable(tagList(p, app, html_dependency_react()))
```
```{r babel, echo = FALSE, fig.cap ="(ref:babel)"}
include_vimeo("315309034")
```
For a more serious example, we could leverage another React component, named `react-data-grid`, to display the data within a **plotly** scatterplot brush, as done in Figure \@ref(fig:react-data-grid). Again, we can use **runpkg** to download a bundle of `react-data-grid`, but this library doesn't come with `React`/`ReactDOM`, so we must explicitly include it this time around. In fact, this approach of explicitly importing and calling `ReactDOM.render()` on your component is a more common approach than the custom standalone interface approach (i.e., `renderReactPlayer()`) used in Figure \@ref(fig:react-player).
\index{layout()@\texttt{layout()}!dragmode@\texttt{dragmode}!Rectangular selection}
```{r, eval = FALSE, summary = "Click to see code"}
data_grid_js <- runpkg::download_files(
"react-data-grid",
"dist/react-data-grid.min.js"
)
# the data table placeholder
data_grid <- tags$div(id = "data-grid")
# upon clicking the marker, populate a video
# in the DOM element with an id of 'video'
p <- plot_ly(mtcars, x = ~wt, y = ~mpg) %>%
add_markers(customdata = row.names(mtcars)) %>%
layout(dragmode = "select") %>%
onRender(babel_transform(
"el => {
var container = document.getElementById('data-grid');
var columns = [
{key: 'x', name: 'Weight'},
{key: 'y', name: 'MPG'},
{key: 'customdata', name: 'Model'}
];
el.on('plotly_selecting', d => {
if (d.points) {
var grid = <ReactDataGrid
columns={columns}
rowGetter={i => d.points[i]}
rowsCount={d.points.length}
/>;
ReactDOM.render(grid, container);
}
});
el.on('plotly_deselect', d => {
ReactDOM.render(null, container);
});
}"
))
# create the HTML page
browsable(
tagList(p, data_grid, html_dependency_react(), data_grid_js)
)
```
```{r react-data-grid, echo = FALSE, fig.cap = "(ref:react-data-grid)"}
include_vimeo("325933744")
```