This example requires the servr package and the latest development version of xfun, which you may install via:
install.packages('servr')
install.packages('xfun', repos = 'https://yihui.r-universe.dev')
Restart R after installation.
Make sure you have some R scripts under your current working directory. Then run:
servr::httr()
You can click on any R script and see its output as a web page.
.R
file
that is time-consuming to run. Try a simple R script first.You can edit the R script in your favorite editor (e.g., RStudio). As soon as you save it, the web page will be automatically refreshed (with new output).
You can click on a code block to toggle its output. Or Alt + Click
to
toggle all output.
The option options(xfun.record.verbose = 2)
will let you see verbose
output, i.e., all values of all code expressions.
The code is a little complicated here and I don’t mean to make you fully understand it, but just want to show you a possibility.
You say “plot something”, and the words will be sent to R. We use R to split words into letters, and draw a barplot to show the frequencies of letters. If you say “plot nothing”, the page will be emptied.
library(httpuv)
p = if (servr:::port_available(43210)) 43210 else randomPort()
s = startServer(
host = '127.0.0.1',
port = p,
app = list(
call = function(req) {
list(
status = 200,
headers = list('Content-Type' = 'text/html'),
body = "
<html>
<head>
<title>Test speech recognition</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/annyang/2.6.0/annyang.min.js'></script>
</head>
<body>
<p><img id='barplot' /></p>
<p id='speech'></p>
<script>
const ws = new WebSocket(location.href.replace(/^http/, 'ws').replace(/\\/?$/, '/websocket/'));
ws.onmessage = e => {
document.getElementById('barplot').src = e.data;
};
annyang.addCommands({'plot *plot': plot => {
document.getElementById('speech').innerText += ('... ' + plot);
ws.send(plot);
}});
annyang.start();
</script>
</body>
</html>
"
)
},
onWSOpen = function(ws) {
d = character()
ws$onMessage(function(binary, message) {
if (message == 'nothing') return(ws$send(''))
m = toupper(unlist(strsplit(message, '')))
m = m[m %in% LETTERS]
d <<- c(d, m)
freqs = table(factor(d, levels = LETTERS))
f = tempfile(fileext = '.png'); on.exit(file.remove(f))
png(f, width = 800)
barplot(
freqs, main = message, ylim = c(0, ceiling(max(freqs)/5) * 5),
col = palette.colors(26, 'Alphabet')
)
dev.off()
ws$send(xfun::base64_uri(f))
})
}
)
)
browseURL(sprintf('http://127.0.0.1:%d', p))
s$stop()
The answer lies in the httpuv package (it’s not the only option for creating web applications with R; more on this later).
library(httpuv)
p = if (servr:::port_available(43210)) 43210 else randomPort()
s = startServer(
host = '127.0.0.1', port = p, app = list(
call = function(req) {
list(status = 200, body = paste('Hello', rnorm(1)))
}
)
)
browseURL(sprintf('http://127.0.0.1:%d', p))
# stop the server when you are done
s$stop()
library(httpuv)
p = if (servr:::port_available(43210)) 43210 else randomPort()
s = startServer(
host = '127.0.0.1', port = p, app = list(
call = function(req) {
params = sub('^[?]', '', req$QUERY_STRING)
params = gsub('&', ',', params)
params = eval(parse(text = c('list(', params, ')')))
if (is.null(params$n)) params$n = 30
res = do.call(rnorm, params)
list(status = 200, body = jsonlite::toJSON(res))
}
)
)
browseURL(sprintf('http://127.0.0.1:%d?n=10&mean=1', p))
s$stop()
Draw a QQ plot in R with half of the browser window width:
library(httpuv)
p = if (servr:::port_available(43210)) 43210 else randomPort()
s = startServer(
host = '127.0.0.1', port = p, app = list(
call = function(req) {
list(
status = 200,
headers = list('Content-Type' = 'text/html'),
body = "
<html>
<head>
<title>Test websocket</title>
</head>
<body>
<p><img id='qqplot' /></p>
<button id='generate'>New QQ plot</button>
<script>
const ws = new WebSocket(location.href.replace(/^http/, 'ws').replace(/\\/?$/, '/websocket/'));
ws.onmessage = e => {
console.log('Data sent from R: ' + e.data.substr(0, 200));
document.getElementById('qqplot').src = e.data;
};
document.getElementById('generate').onclick = e => {
ws.send(window.innerWidth);
};
</script>
</body>
</html>
"
)
},
onWSOpen = function(ws) {
ws$onMessage(function(binary, message) {
cat('The window width from JavaScript is', message, 'px.\n')
# draw a plot with half of the width of the browser window
f = tempfile(fileext = '.png'); on.exit(file.remove(f))
png(f, width = as.numeric(message)/2)
x = rnorm(30)
qqnorm(x, xlim = c(-3, 3), ylim = c(-3, 3))
qqline(x)
dev.off()
ws$send(xfun::base64_uri(f))
})
}
)
)
browseURL(sprintf('http://127.0.0.1:%d', p))
s$stop()
Again, the code may look complicated in your eyes, but essentially the idea is the following:
ws = new WebSocket();
// Message received from R
ws.onmessage = function(event) {
event.data;
};
// Message to R
ws.send('...');
ws$onMessage(function(binary, message) {
# `message` is from browser via JavaScript
# After processing the message, send another message to browser
ws$send('...')
})
I won’t cover this package in this talk (actually I haven’t used it much), but just FYI: https://cran.r-project.org/package=Rserve
In theory, you can register your app handler in tools:::.httpd.handlers.env
but this is just for fun (:::
is a danger sign). Note that base R’s HTTP
server doesn’t support WebSocket.
new_app = function(name, handler) {
port = tools::startDynamicHelp(NA)
url = sprintf('http://127.0.0.1:%d/custom/%s', port, name)
assign(name, handler, envir = tools:::.httpd.handlers.env)
browseURL(url)
}
new_app('rnorm', handler = function(path, ...) {
list(payload = sprintf('<h1>N(0, 1) = %f</h1>', rnorm(1)))
})
If you know CSS (to style web elements) and JavaScript (to program web pages):
new_app('rnorm', handler = function(path, ...) {
x = rnorm(1)
color = ifelse(x > 0, 'blue', 'red')
list(payload = paste0(
sprintf('<h1>N(0, 1) = <i style="color: %s;">%f</i></h1>', color, x),
'<script>setTimeout(() => location.reload(), 1000);</script>'
))
})
Questions?
Welcome to file Github issues related to the topic of this talk: https://github.com/yihui/servr/issues
If you want my public and general help, use the discussion board: https://github.com/yihui/yihui.org/discussions
If your question involves privacy and you must reach me privately: https://yihui.org/en/about/#contact-me