For a while I’ve been trying to find the best slideshow format to create using RMarkdown. My go-to format is ioslides because it has a presenter mode that works in self-contained mode, meaning the entire presentation is in one file. The drawback is that fullscreen images is not a built in feature.
Another popular format is reveal.js. This allows for fullscreen images, but presenter mode is not possible while keeping the file self-contained.
A very promising format is from the xaringan
package which is based on remarkjs. This allows for beautiful, fullscreen images and it has an excellent presenter mode. This can all be done with a self-contained file. The problem is, due to the nature of the format, having all the images URI encoded for self-contained mode causes the file to open very slowly. I gave a talk about web scraping at the MIT Sports Analytics Conference which takes over five minutes to open.
Given all this, I decided to hack my way to a solution using ioslides. After some prep it works quite well.
This is a long post, so if you want it all in one place, skip ahead.
The Problem
RMarkdown slides are initiated using two hash tags (##
). Metadata can be encoded in curly braces ({}
) after the hash tags and any heading text.
## Heading Text {.SlideClass #SlideID0 attr=value0}
Slide Content
The thing is, SlideClass
, SlideID
and the attr
value are applied to an <article
> tag inside a <slide>
, not to the <slide>
.
<slide class=''>
<hgroup>
<h2>Heading Text</h2>
</hgroup>
<article attr="value0" class="SlideClass" id="SlideID0">
<p>Slide Content</p>
</article>
</slide>
This precludes us from setting style="background-url(filename.jpg)"
in the header metadata. Further, doing so would still leave just a link to the file and now URI encode it.
The Solution
The solution is to specify the image in CSS and associate it with a specific ID. However, at this point, it will only be associated with the <article>
.
#SlideID1 {
background-image: url(Cowboy-Lasso.jpg);
}
To keep things looking nice, in a separate CSS block we specify some background image properties that apply to all slide background images.
slide {
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
## Background One {.SlideClass #SlideID1}
Some Text Here
knitr::include_graphics(file.path(imageDir, 'Background-Article.png'))
The trick is to associate the image, in CSS, with the ID of the <slide>
. Even though we assign an ID to the <article>
this does not help us know the ID of the <slide>
. To get around this we use a bit of jQuery. We write some code that take a name
attribute from every <article>
tag and assigns it as an ID for the corresponding <slide>
tags.
$(document).ready(function(){
// for every article tag inside a slide tag
$("slide > article").each(function(){
// copy the article name to the parentNode's (the slide) ID
this.parentNode.id=$(this).attr('name');
});
});
In order for this to work, we also need to load jQuery. The quickest way I have found to do this is to use htmltools
to
```{r deps,include=FALSE}
# this ensures jquery is loaded
dep <- htmltools::htmlDependency(name = "jquery", version = "1.11.3", src = system.file("rmd/h/jquery-1.11.3", package='rmarkdown'), script = "jquery.min.js")
# attach the dependency to an empty span
htmltools::attachDependencies(htmltools::tags$span(''), dep)
```
The order matters, and jQuery must be loaded first. So rather than deal with this code every time we write a presentation let’s be good coders and save this to an Rmd
file called ‘LoadJQuery.Rmd’.
---
title: "Load JQuery"
author: "Jared P. Lander"
date: "July 1, 2017"
output: ioslides_presentation
---
```{r deps,include=FALSE}
# this ensures jquery is loaded
dep <- htmltools::htmlDependency(name = "jquery", version = "1.11.3", src = system.file("rmd/h/jquery-1.11.3", package='rmarkdown'), script = "jquery.min.js")
htmltools::attachDependencies(htmltools::tags$span(''), dep)
```
```{js move-id-background-color}
$(document).ready(function(){
// for every article tag inside a slide tag
$("slide > article").each(function(){
// copy the article name to the parentNode's (the slide) ID
this.parentNode.id=$(this).attr('name');
});
});
```
Then in the presentation Rmd file, toward the top, this file is loaded as a chunk child.
```{r load-jquery,child='LoadJQuery.Rmd'}
```
Now, any time a name
attribute is included in heading metadata, that name
will become the id
for the enclosing <slide>
.
## Background Two {.SlideClass #SlideID2 name=Slide2}
Some more text here
becomes
<slide class="current" data-slide-num="4" data-total-slides="4" id="Slide2">
<hgroup>
<h2>Background Two</h2>
</hgroup>
<article id="SlideID2" class="SlideClass">
<p>Some more text here</p>
</article>
</slide>
Then there is the matter of writing the CSS so that the image file is URI encoded. This can become laborious so we write a function to do it.
```{r background-function,include=FALSE}
makeBG <- function(id, file)
{
cat(
sprintf('<style type="text/css">\n#%s {\nbackground-image: url(%s);\n}\n</style>',
id, knitr::image_uri(file))
)
}
```
Anywhere in the document, though most logically in the intended slide, that function is called, specifying the slide ID and the image.
## Background Three {.SlideClass #SlideID3}
Full screen background
```{r results='asis',echo=FALSE}
makeBG(id='Slide3', 'Cowboy-Lasso.jpg')
```
This results in the following slide.
Of course, we must use images with the proper resolution.
Putting it All Together
Putting it all together yields a presentation with fullscreen background images.
---
title: "Fullscreen Backgrounds"
author: Jared P. Lander
output:
ioslides_presentation:
self_contained: yes
widescreen: yes
---
```{css}
slide {
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
```
```{r deps,include=FALSE}
# this ensures jquery is loaded
dep <- htmltools::htmlDependency(name = "jquery", version = "1.11.3", src = system.file("rmd/h/jquery-1.11.3", package='rmarkdown'), script = "jquery.min.js")
htmltools::attachDependencies(htmltools::tags$span(''), dep)
```
```{js move-id-background-color}
$(document).ready(function(){
// for every article tag inside a slide tag
$("slide > article").each(function(){
// copy the article name to the parentNode's (the slide) ID
this.parentNode.id=$(this).attr('name');
});
});
```
```{r background-function,include=FALSE}
makeBG <- function(id, file)
{
cat(
sprintf('<style type="text/css">\n#%s {\nbackground-image: url(%s);\n}\n</style>',
id, knitr::image_uri(file))
)
}
```
## Slide with Background {.SlideClass #SlideID name=ThisSlide}
```{r results='asis',echo=FALSE}
makeBG(id='ThisSlide', 'Cowboy-Lasso.jpg')
```
Slide with a nice background.
The full code and final presentation are posted so we can see the whole thing.
In case typing in the block of code to make the background becomes too tedious we can make an RStudio snippet.
snippet back
```{r results='asis'}
makeBG('${1}', file.path(imageDir, '${2}'))
```
${0}
Since this is RMarkdown, in order to complete a snippet we have to use shift+tab
rather than just tab
as in R scripts.
I posted the snippets I use on GitHub.
Other RStudio tips and tricks can be seen in Sean Lopp’s talk at this year’s New York R Conference.
Jared Lander is the Chief Data Scientist of Lander Analytics a New York data science firm, Adjunct Professor at Columbia University, Organizer of the New York Open Statistical Programming meetup and the New York and Washington DC R Conferences and author of R for Everyone.