forked from JohnCoene/javascript-for-r
-
Notifications
You must be signed in to change notification settings - Fork 0
/
3-23-htmlwidgets-peity.Rmd
373 lines (293 loc) Β· 13.1 KB
/
3-23-htmlwidgets-peity.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
# A Realistic Widget {#widgets-realistic}
```{r, include = FALSE, echo = FALSE}
library(peity)
```
In this section, we build a package called `peity`, which wraps the JavaScript library of the same name, [peity.js](https://github.com/benpickles/peity), to create inline charts. This builds upon many things we explored in the playground package built in the previous chapter.
```r
usethis::create_package("peity")
htmlwidgets::scaffoldWidget("peity")
```
As done with candidate libraries, as explained in an earlier chapter, there is no avoiding going through the documentation of the library one wants to use to observe how it works. Forging a basic understanding of the library, we can build the following basic example.
```html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
<!-- Import libraries -->
<script src="jquery-3.5.1.min.js"></script>
<script src="jquery.peity.min.js"></script>
</head>
<body>
<!-- div to hold visualisation -->
<span id="bar">5,3,9,6,5,9,7,3,5,2</span>
<!-- Script to create visualisation -->
<script>
$("#bar").peity("bar");
</script>
</body>
</html>
```
Peity.js depends on jQuery\index{jQuery}. Hence the latter is imported first; the data for the chart is placed in a `<span>`, and the `peity` method is then used on the element containing the data. Peity.js uses `<span>` HTML\index{HTML} tags as these work inline, using a `<div>` the chart will still display, but the purpose of using peity.js would be defeated.
## Dependencies {#widgets-realistic-deps}
Once the package is created and the widget scaffold\index{scaffold} laid down, we need to add the JavaScript dependencies\index{dependency} without which nothing can move forward.
Two dependencies\index{dependency} are required in order for peity.js to run: peity.js and jQuery\index{jQuery}. Instead of using the CDN\index{CDN} those are downloaded as this ultimately makes the package more robust (more easily reproducible outputs and no requirement for internet connection). Each of the two libraries is placed in its own respective directory.
```r
dir.create("./inst/htmlwidgets/jquery")
dir.create("./inst/htmlwidgets/peity")
peity <- paste0(
"https://raw.githubusercontent.com/benpickles/",
"peity/master/jquery.peity.min.js"
)
jquery <- paste0(
"https://code.jquery.com/jquery-3.5.1.min.js"
)
download.file(
jquery, "./inst/htmlwidgets/jquery/jquery.min.js"
)
download.file(
peity, "./inst/htmlwidgets/peity/jquery.peity.min.js"
)
```
This produces a directory that looks like this:
```
.
βββ DESCRIPTION
βββ NAMESPACE
βββ R
β βββ peity.R
βββ inst
βββ htmlwidgets
βββ jquery
β βββ jquery.min.js
βββ peity.js
βββ peity.yaml
βββ peity
βββ jquery.peity.min.js
```
In htmlwidgets, dependencies\index{dependency} are specified in the `.yaml` file located at `inst/htmlwidgets`, which at first contains a commented template.
```yml
# (uncomment to add a dependency)
# dependencies:
# - name:
# version:
# src:
# script:
# stylesheet:
```
Let's uncomment those lines as instructed at the top of the file and fill it in.
```yml
dependencies:
- name: jQuery
version: 3.5.1
src: htmlwidgets/jquery
script: jquery.min.js
- name: peity
version: 3.3.0
src: htmlwidgets/peity
script: jquery.peity.min.js
```
```{block, type='rmdnote'}
The order of the dependencies matters. Peity.js depends on jQuery\index{jQuery} hence the latter comes first in the `.yaml`.
```
The order in which one specifies the dependencies\index{dependency} matters, just like it does in an HTML file, therefore jQuery\index{jQuery} is listed first. The `stylesheet` entries were removed as none of these libraries require CSS files. The `src` path points to the directory containing the JavaScript files and stylesheets relative to the `inst` directory of the package; this is akin to using the `system.file` function to return the full path to a file or directory within the package.
```r
devtools::load_all()
system.file("htmlwidgets/peity", package = "peity")
#> "/home/me/packages/peity/inst/htmlwidgets/peity"
```
We should verify that this is correct by using the one R function the package features and check the source code of the output to verify that the dependencies\index{dependency} are well present in the HTML output. We thus run `peity("test")`, open the output in the browser (![](images/open-in-browser.png)) and look at the source code of the page. At the top of the page, you should see `jquery.min.js` and `jquery.peity.min.js` imported, clicking those links will either present you with the content of the file or an error.
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<style>body{background-color:white;}</style>
<script src="lib/htmlwidgets-1.5.1/htmlwidgets.js"></script>
<script src="lib/jQuery-3.5.1/jquery.min.js"></script>
<script src="lib/peity-3.3.0/jquery.peity.min.js"></script>
<script src="lib/peity-binding-0.0.0.9000/peity.js"></script>
...
```
## Implementation {#widgets-realistic-implementation}
The JavaScript code for peity.js is relatively uncomplicated. It is just one function, but integrating it with htmlwidgets requires some thinking. In the example below, peity is applied to the element with `id = 'elementId'`; the first argument of `peity` is the type of chart to produce from the data, and the second optional argument is a JSON of options.
```js
$("#elementId").peity("bar", {
fill: ["red", "green", "blue"]
})
```
Also, the data that peity uses to draw the inline chart is not passed to the function but taken from the HTML element.
```html
<span id="elementId">5,3,9,6</span>
```
Therefore, the htmlwidget will have to insert the data in the HTML element, then run the `peity` function to render the chart. Inserting the data is actually already done by htmlwidgets by default. Indeed the default htmlwidgets template takes a `message` from the R function, and inserts said message in the HTML element, passing a vector instead of a message produces precisely what peity expects!
```r
peity(c(1,5,6,2))
```
```html
<div
id="htmlwidget-495cf47d1a2a4a56c851"
style="width:960px;height:500px;"
class="play html-widget">
1,5,6,2
</div>
```
The argument ought to be renamed nonetheless from `message` to `data`.
```r
peity <- function(data, width = NULL, height = NULL,
elementId = NULL) {
# forward options using x
x = list(
data = data
)
# create widget
htmlwidgets::createWidget(
name = 'peity',
x,
width = width,
height = height,
package = 'peity',
elementId = elementId
)
}
```
The change in the R code must be mirrored in the `peity.js` file, where it should set the `innerText` to `x.data` instead of `x.message`.
```js
// peity.js
// el.innerText = x.message;
el.innerText = x.data;
```
This leaves the implementation of peity.js to turn the data into an actual chart. The way we shall go about it is to paste one of the examples in the `renderValue` function.
```js
renderValue: function(x) {
// insert data
el.innerText = x.data;
// run peity
$("#elementId").peity("bar", {
fill: ["red", "green", "blue"]
})
}
```
One could be tempted to run `devtools::load_all`, but this will not work, namely because the function uses a selector that will not return any object; it needs to be applied to the div created by the widget not `#elementId`. As explained in the previous chapter, the selector of the element created is accessible from the `el` object. As a matter of fact, we did log in the browser console the id of the created div taken from `el.id`. Therefore concatenating the pound sign and the element id produces the selector to said element (`.class`, `#id`).
```js
$("#" + el.id).peity("bar", {
fill: ["red", "green", "blue"]
})
```
This will work but can be further simplified; there is no need to recreate a selector using the `id` of the `el` element; the latter can be used in the jQuery\index{jQuery} selector directly.
```js
$(el).peity("bar", {
fill: ["red", "green", "blue"]
})
```
This will now produce a working widget, albeit limited to creating charts of a predefined type and colour. Next, these options defining the chart type, fill colours, and others must be made available from R.
Below we add a `type` argument to the `peity` function; this `type` argument is then forwarded to `x`, so it is serialised\index{serialise} and accessible in the JavaScript file.
```r
peity <- function(data, type = c("bar", "line", "pie", "donut"),
width = NULL, height = NULL, elementId = NULL) {
type <- match.arg(type)
# forward options using x
x = list(
data = data,
type = type
)
# create widget
htmlwidgets::createWidget(
name = 'peity',
x,
width = width,
height = height,
package = 'peity',
elementId = elementId
)
}
```
This should then be applied by replacing the hard-coded type (`"bar"`) to `x.type`.
```js
$(el).peity(x.type, {
fill: ["red", "green", "blue"]
})
```
Reloading the package will now let one create a chart and define its type, but some options remain hard-coded. These can be made available from R in a variety of ways depending on the interface one wants to provide users of the package. Here we make them available via the three-dot construct (`...`), which are captured in a list and forwarded to the `x` object.
```r
peity <- function(data, type = c("bar", "line", "pie", "donut"),
..., width = NULL, height = NULL, elementId = NULL) {
type <- match.arg(type)
# forward options using x
x = list(
data = data,
type = type,
options = list(...)
)
# create widget
htmlwidgets::createWidget(
name = 'peity',
x,
width = width,
height = height,
package = 'peity',
elementId = elementId
)
}
```
These can then be easily accessed from JavaScript.
```js
$(el).peity(x.type, x.options)
```
This makes (nearly) all of the functionalities of peity.js available from R. Below we use `htmltools::browsable` to create multiple widgets at once, the function only accepts a single value, so the charts are wrapped in an `htmltools::tagList`. Let us explain those in reverse order, `tagList` accepts a group of tags or valid HTML\index{HTML} outputs like htmlwidgets\index{htmlwidgets} and wraps them into one, it is necessary here because the function `browsable` only accepts one value. Typically htmltools tags are just printed in the console; here we need them to be opened in the browser instead. Remember to run `devtools::load_all` so you can run the `peity` function we just wrote.
```r
library(htmltools)
browsable(
tagList(
peity(runif(5)),
peity(runif(5), type = "line"),
peity("1/4", type = "pie", fill = c("#c6d9fd", "#4d89f9")),
peity(c(3,5), type = "donut")
)
)
```
```{r, eval = FALSE, echo=FALSE}
htmltools::browsable(
htmltools::tagList(
peity(runif(5)),
htmltools::br(),
peity(runif(5), type = "line"),
htmltools::br(),
peity("1/4", type = "pie", fill = c("#c6d9fd", "#4d89f9")),
htmltools::br(),
peity(c(3,5), type = "donut")
)
)
```
```{r peity-divs, fig.pos="H", echo=FALSE, fig.cap='Peity output with DIV'}
knitr::include_graphics("images/peity-div.png")
```
There is nonetheless one remaining issue in Figure \@ref(fig:peity-divs): peity.js is meant to create inline charts within `<span>` HTML\index{HTML} tags but these are created within `<div>` hence each chart appears on a new line.
## HTML Element {#widgets-realistic-html-element}
As pointed out multiple times, the widget is generated in a `<div>`, which is working fine for most visualisation libraries. However, we saw that peity.js works best when placed in a `<span>` as this allows placing the charts inline.
This can be changed by placing a function named `widgetname_html`, which is looked up by htmlwidgets and used if found. This is probably the first such function one encounters and is relatively uncommon, but it is literally how the htmlwidgets\index{htmlwidgets} package does it: it scans the namespace of the package looking for a function that starts with the name of the widget and ends in `_html` and if found uses it. Otherwise, it uses the default `div`. This function takes the three-dot construct (`...`) and uses them in an htmltools tag. The three-dots are necessary because internally htmlwidgets need to be able to pass the `id`, `class`, and `style` attributes to the tag.
```r
widget_html.peity <- function(...) {
htmltools::tags$span(...)
}
```
This can also come in handy if some arguments must be hard-coded, such as assigning a specific class to every widget.
```r
widget_html.myWidget <- function(..., class){
htmltools::tags$div(..., class = paste(class,"my-class"))
}
```
Reloading the package after placing the function above anywhere in the package will produce inline charts, as show in Figure \@ref(fig:peity-spans).
```r
browsable(
tagList(
p(
"We can now", peity(runif(5)),
"use peity", peity(runif(5), type = "line"),
"inline with text!",
peity(c(4,2), type = "donut")
)
)
)
```
```{r peity-spans, fig.pos="H", echo=FALSE, fig.cap='Peity output with SPAN'}
knitr::include_graphics("images/peity-span.png")
```