Preview for developers
Warning: the package has not reached stable version.
R package sketch
Creates interactive illustrations embeddable in RMarkDown documents. The package compiles R code into JavaScript code by applying rewriting rules to the R AST. This allows users to develop JS-style visualisations using only the R syntax.
Basic conversion from R to JavaScript
The package current has ~56 AST rewriting rules. Here are a list of the basic ones.
| Description | R | JavaScript |
|---|---|---|
| Assignment of variable | x <- 1 |
let x; x = 1 |
| Access object attribute | obj$x |
obj.x |
| Vector (JS Array) | c(1, 2, 3, 4, 5) |
[1, 2, 3, 4, 5] |
| List (JS Object) | list(x = 1, y = 2) |
{x:1, y:2} |
| Conditionals | if (cond) {...} else if (cond) {...} else {...} |
if (cond) {...} else if (cond) {...} else {...} |
| For-loop | for (x in iterable) {...} |
for (let x of iterable) {...} |
| While-loop | while (cond) {...} |
while (cond) {...} |
Note that the conversion is not perfect, e.g.
-
R vector cannot be nested, while JS array can be nested.
c(1, 2, c(3,4), 5)is the same asc(1, 2, 3, 4, 5), but[1, 2, [3, 4], 5]is not the same as[1, 2, 3, 4, 5].
-
R list can be unnamed, but JS object must be fully named.
- But don’t worry, when you try to convert an R unnamed list to JS
object,
sketchwill warn you about it :)
- But don’t worry, when you try to convert an R unnamed list to JS
object,
News
- 2019-12-09:
- Switched to
math.jsto support vectorised operations. - Added R
groupGenericfunctions. - Added Travis CI and code coverage.
- Switched to
- 2019-12-01: Added
readme.md: “Preview for developers”
Index
1. Preliminaries
1.1 Installation
# install.packages("devtools")
devtools::install_github("kcf-jackson/sketch")1.2 Run a sketch R file
There are two ways to run a sketch R file.
-
If you use RStudio, there is an add-in “source a sketch R file” listed under this package (consider binding that to the key combination alt-shift-s), and that sources the active tab in the editor.
-
Save the sketch R file (with the usual .R extension), then call
sketch::source_rwith it.
1.3 Embed a sketch R file in RMarkDown document
Simply use sketch::insert_sketch in a code chunk:
sketch::insert_sketch(
file = "main.R", id = "sketch_1",
width = 800, height = 600
)Note that sketch::insert_sketch will create a temporary folder at the
working directory.
2. Basic examples
2.1 p5.js
The functions setup, draw, createCanvas, background,
frameCount, fill and circle are from the p5.js library, and one
can use them as they are in the R code. This also works for all the
other functions in the
library.
#! load_script(src = "https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.js")
setup <- function() {
createCanvas(400, 300)
}
draw <- function() {
background(0, 0, 33) # RGB colors
for (i in 1:3) {
dia <- sin(frameCount * 0.025) * 30 * i
fill(255, 70 * i, 0) # RGB colors
circle(100 * i, 150, dia) # (x, y, diameter)
}
}2.2 DOM manipulation
The variable document and all of its methods are available, and
attributes also work as expected. (Of course it works, because we are
really writing JavaScript!)
Here we also demonstrate how to source one sketch file in another. At the moment, the working path must be at the parent directory of your sketch file for the source-directive to work. This will be fixed in the later release.
helper.R (R-style abstraction)
# Create a new DOM element with class and innerText
dom <- function(tag_name, class_name, inner_text) {
declare (el)
el <- document$createElement(tag_name) # document and its methods are available!
el$className <- class_name # attributes also works as expected
el$innerText <- inner_text
return (el)
}
# Insert a DOM element into another
insert_into <- function(x, y) {
document$querySelector(y)$appendChild(x)
}Be aware that the word class is a keyword in JavaScript.
main.R
#! load_script("helper.R")
# Textbox
textbox <- dom("div", "text-box-1", "Hello World!")
# Two buttons
button_1 <- dom("button", "button-1", "1")
button_1$type <- "button"
button_1$onclick <- function() { textbox$innerText <- "Hello again!" }
button_2 <- dom("button", "button-2", "2")
button_2$type <- "button"
button_2$onclick <- function() { textbox$innerText <- "Bye!" }
# html
insert_into(textbox, "body")
insert_into(button_1, "body")
insert_into(button_2, "body")This gives a simple way to create a narrative along with your data visualisation.
2.3 d3.js
Here we learn about how to source a data file with a slightly more
elaborate example using d3.js.
data.json
[
{"x": [1, 2, 3, 4, 5], "y":[2, 9, 6, 2, 10], "r": [2, 4, 6, 8, 10]},
{"x": [1, 2, 3, 4, 5], "y":[8, 3, 10, 5, 5], "r": [9, 7, 5, 3, 1]}
]helper.R (creates reusable R-style components)
create_canvas <- function(width, height) {
d3$select("body")$append("svg")$ # alternatively one can select "#new_sketch", which
attr("width", width)$ # is the default placeholder for a sketch file
attr("height", height)
}
linear_scale <- function(from, to) {
return(d3$scaleLinear()$domain(from)$range(to))
}
draw_circles <- function(data0, scales) {
declare (data_id, selection0)
data_id <- 0:(length(data0$x) - 1)
selection0 <- d3$select("svg")$selectAll("circle")$data(data_id)
# add new circles
selection0$enter()$
append("circle")$
attr("cx", d %=>% scales$x(data0$x[d]))$
attr("cy", d %=>% scales$y(data0$y[d]))$
attr("r", d %=>% scales$r(data0$r[d]))
# update existing circles
selection0$
transition()$duration(300)$
attr("cx", d %=>% scales$x(data0$x[d]))$
attr("cy", d %=>% scales$y(data0$y[d]))$
attr("r", d %=>% scales$r(data0$r[d]))
# remove old circles
selection0$exit()$remove()
}main.R
#! load_script("https://d3js.org/d3.v5.min.js")
#! load_script("helper.R")
#! load_script("data.json")
# Data
# The data is now available in the data_json variable.
stage_1 <- data_json[0] # JavaScript uses 0-based indexing
stage_2 <- data_json[1]
# Canvas and Scales
create_canvas(600, 400)
margin <- 50
scales <- list(
x = linear_scale(c(1, 5), c(margin, 600 - margin)),
y = linear_scale(c(1, 10), c(400 - margin, margin)),
r = linear_scale(c(1, 10), c(10, 50))
)
# Initialisation
draw_circles(stage_1, scales)
current <- 1
document$querySelector("body")$onclick <- function() {
if (current == 1) {
draw_circles(stage_2, scales)
current <<- 2 # careful with super-assign
} else {
draw_circles(stage_1, scales)
current <<- 1 # careful with super-assign
}
}2.4 JQuery Quiz
helper.R
# Create a new DOM element with id
dom <- function(tag, id) {
declare (el)
el <- document$createElement(tag)
el$id <- id
return(el)
}
insert_into <- function(x, y) {
document$querySelector(y)$appendChild(x)
}simple_interface.R
#! load_script("helper.R")
start_button <- dom("a", "quiz-start-btn")
start_button$href <- "#"
start_button$class <- "quiz-button"
start_button$innerText <- "Start"
insert_into(dom("div", "quiz-header"), "#new_sketch")
insert_into(dom("div", "quiz-start-screen"), "#new_sketch")
insert_into(start_button, "#new_sketch")main.R
Note that since the symbol $ has special meaning in R, the JavaScript
$ is mapped to the symbol
jQuery.
#! load_script("https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js")
# Source: https://github.com/jchamill/jquery-quiz
#! load_script("./jquery.quiz.css")
#! load_script("./jquery.quiz.js")
#! load_script("simple_interface.R")
quiz_db <- list(
counterFormat = 'Question %current of %total',
questions = c(
list(
q= 'A sample question?',
options= c('Answer 1', 'Answer 2', 'Answer 3', 'Answer 4'),
correctIndex = 1,
correctResponse = 'Custom correct response.',
incorrectResponse = 'Custom incorrect response.'
),
list(
q= 'A new question?',
options= c('Answer 1', 'Answer 2'),
correctIndex = 0,
correctResponse = 'Custom correct response.',
incorrectResponse = 'Custom incorrect response.'
)
)
)
jQuery('#new_sketch')$quiz(quiz_db)Note that the element #new_sketch is always there in a sketch file
(but this may be subject to removal in the future).
3. Notes
3.1 Special operators
#! load_script()
Use #! together with load_script to load any JavaScript library, CSV
/ JSON data file or an other sketch R file.
-
If the data file is named as
my_file.json, then it will be available in the variablemy_file_json. The data variable is not supposed to be modified (and is assigned theconsttag). If one needs to change the data, please proceed by making a copy. -
The package does not resolve circular referencing, i.e. file A and file B source each other at the same time. Please avoid that!
declare() / let()
declare() / let() are empty R functions that act as a placeholder to
facilitate conversion into JavaScript. If you want to assign a value to
a variable, you may proceed as:
declare (x)
x <- 3
# alternative
let (x = 3)Note that declare and let are 100% interchangable, e.g. declare (x = 3) is also valid. I prefer using declare for top-level variable
declaration and let for local use.
If one uses variables without declaring them, JavaScript will declare it for you and place them in the global namespace.
%=>% and %+%
-
%=>%maps to the arrow function=>in JavaScript. -
%+%maps to+in JavaScript. The reason for creating this operator is that R does not perform string addition, while JS does. One may think of%+%as the semantic equivalent ofpaste0. Indeed,`%+%` <- paste0 "a" %+% "b" %+% "c" # gives "abc"
lambda
The lambda function offers a convenient way to define anonymous
function, which works similar to pryr::f. For instance, function(x, y) { return(x + y) } can be rewritten as lambda(x, y, x + y).
3.2 Common pitfalls (of writing JS in R)
i. JavaScript uses 0-based indexing
// Web-browser console
x = [1,2,3]
x[0] // 1ii. JavaScript function reads argument by position / order
// Web-browser console
first = function(x, y) { console.log(x) }
first(y = 3, x = 1) // 3Possible workaround: Use destructuring to mimic named arguments.
iii. return in JavaScript function must be explicit
add = function(x, y) { x + y }
add(3, 4) // undefined
add2 = function(x, y) { return(x + y) }
add(3, 4) // 7iv. JavaScript passes objects by reference (think R6 in R)
// Web-browser console
x = [1,2,3] // Array(3) [ 1, 2, 3 ]
y = x // Array(3) [ 1, 2, 3 ]
y[0] = 999
y // Array(3) [ 999, 2, 3 ]
x // Array(3) [ 999, 2, 3 ]Workaround: Use y = x.slice() to make a copy of array.
v. A browser session has a pre-populated namespace
-
e.g. the variables
document,window,location,var,stop,open,focusare taken already. Avoid assigning values to these variables! -
The full list can be found by typing in the console of a browser:
// Web-browser console
for (let b in window) {
if (window.hasOwnProperty(b)) console.log(b);
}




