Jekyll and HTML Widgets
Published:
I’m currently compiling a list of university-affiliated programs designed to help prepare students for graduate study in political science and assist them in the process of applying to graduate school (a labyrinthine and opaque process in many regards). Since travel costs can be a deciding factor for some students when deciding whether to apply to these programs, I thought it would be nice to also put them on a map.
While just plotting them on a map is easy, since it will be on a web page, I figured why not also embed links to each program in the map as well. In theory this is easy thanks to R packages like leaflet, which leverages the (unsurprisingly named) leaflet JavaScript library for interactive webmaps. However, because I use Jekyll instead of Hugo for my site, I can’t just use the blogdown R package and have everything magically work.
Steven Miller’s tutorial on integrating R Markdown and Jekyll is the starting point my own use of R Markdown and Jekyll, so check that out first for a quick primer on how to use R Markdown to render .Rmd
files into the .md
files that Jekyll uses to render your website. This approach works fantastically well for static images, and requires just a little tweaking to make interactive widgets like leaflet maps work.
Leaflet
We’ll use three packages to create our map. The tidyverse is pretty well-documented at this point, but I use it to write efficient and readable code. tidygeocoder
is a geocoder that can use a variety of geocoding services and works well with data frames and tibbles. Finally, leaflet
is what we’ll use to create our actual map widget.
library(tidyverse)
library(tidygeocoder)
library(leaflet)
First, we need to load our data. This is a CSV file of program information that I’ve compiled myself.
## read in data
predoc <- read_csv('predoc.csv')
## inspect the data
predoc
## # A tibble: 9 x 4
## Institution Name Location URL
## <chr> <chr> <chr> <chr>
## 1 University of South… POIR Predoctoral Summer… Los Angeles,… https://dornsife.usc.edu/poir/predoct…
## 2 Duke University Ralph Bunche Summer Ins… Durham, NC, … https://www.apsanet.org/rbsi
## 3 UC San Diego START La Jolla, CA… https://grad.ucsd.edu/diversity/progr…
## 4 MIT MSRP Cambridge, M… https://oge.mit.edu/graddiversity/msr…
## 5 UC Irvine SURF Irvine, CA, … https://grad.uci.edu/about-us/diversi…
## 6 University of Washi… NSF REU: Spatial Models… Tacoma, WA, … https://www.tacoma.uw.edu/smed/nsf-re…
## 7 University of North… NSF REU: Civil Conflict… Denton, TX, … https://untconflictmgmtreu.wordpress.…
## 8 Princeton University Emerging Scholars in Po… Princeton, N… https://politics.princeton.edu/gradua…
## 9 Harvard University PS-Prep Cambridge, M… https://projects.iq.harvard.edu/ps-pr…
First, we need to get latitude and longitude coordinates from our place names to plot them on a map. We’ll use the geocode()
function, where the first argument is a data frame containing a column with the location information we want to use. The second argument is address
, which tells the geocoder to use the information stored in the Address column of our data frame, and then method = 'osm'
dispatches it to the Open Street Map geocoder, Nominatim.
Next, we’ll use mutate()
to create a new variable to hold the popup text a user will see when they click on a point. I want to provide the university name, the program’s name, and then a link to the program’s information page. I use the str_c()
function to combine the Institution and Name columns, and then I use another call to str_c()
to format the URL. This second call looks like str_c('<a href="', URL, '" target="_PARENT">Program Info</a>')
, where URL is the name of the URL field. It combines the standard start of an HTML anchor tag (<a href="
) with the URL itself, adds the link text of “Program Info”, and then closes the tag. The one unusual element is target="_PARENT"
in the anchor tag. This is necessary to make any links a user clicks open normally, instead of within the frame used to embed it into the page (more on that later).
Once we’ve prepped our popup text, we just pass the data frame to leaflet()
, add a background map (I’ve used a styled map, but you can also get the default map with addTiles()
), and then the markers themselves. The one tricky part of addMarkers()
is that it expects its arguments as one-sided formulas, not just variable names like tidyverse functions. geocode()
has created lat and long columns, so pass those through as well as our label column, and we’re good to go.
Map it
Putting all the above code together in a pipeline looks like this:
## prep and plot
predoc %>%
geocode(address = Location, method = 'osm') %>% ## gecode locations
mutate(lab = str_c(Institution, Name,
str_c('<a href="', URL, '" target="_PARENT">Program Info</a>'),
sep = '<br>')) %>% # paste fields into popup text
leaflet() %>% # create leaflet map widget
addProviderTiles(providers$CartoDB.Positron) %>% # add muted palette basemap
addMarkers(lng = ~ long, lat = ~ lat, popup = ~ lab) # add markers with popup text
Unfortunately this code produces an error that stops R Markdown dead in its tracks; like, the-error = T
-knitr-chunk-option-won’t-even-save-you dead in its tracks. What gives? R Markdown is supposed to be able to render interactive widgets no problem. The issue is that R Markdown can render those widgets for HTML output, but since we’re creating a GitHub Flavored Markdown document that Jekyll then turns into HTML, R Markdown chokes. It can’t embed an HTML widget into a plain text markdown document. Luckily there is a way around this, but it involves an extra step and dealing with some file paths.
R Markdown, HTML widgets, and Jekyll
To make things work, we have to manually save the HTML from our widget, and then embed it into our resulting markdown document. Then, when Jekyll renders the markdown to HTML, it will be visible in the final HTML files that comprise your website. This involves telling R where to save the HTML, then referencing it using raw HTML code in our markdown document. We’re going to do this with the htmlwidgets R package.
## load htmlwidgets to save map widget
library(htmlwidgets)
## prep and plot
predoc %>%
geocode(address = Location, method = 'osm') %>% ## gecode locations
mutate(lab = str_c(Institution, Name,
str_c('<a href="', URL, '" target="_PARENT">Program Info</a>'),
sep = '<br>')) %>% # paste fields into popup text
leaflet() %>% # create leaflet map widget
addProviderTiles(providers$CartoDB.Positron) %>% # add muted palette basemap
addMarkers(lng = ~ long, lat = ~ lat, popup = ~ lab) %>% # add markers with popup text
saveWidget(here::here('/files/html/posts', 'predoc_map.html')) # save map widget
The code is identical to that above, with the addition of the file line that saves the map widget as an HTML file called predoc_map.html
in /files/html/posts
using the saveWidget()
function. You’ll notice I use the here()
function from the here R package to supply the file
argument to saveWidget()
. here
is great because it very intelligently finds the top level of whatever project you’re working on and then constructs file paths from there. It has a number of ways to determine where a project ‘starts’, but for us it works because our website is a git repo and contains a .git
directory.
Frame it
All that’s left to do is embed the map widget in the page using an iframe. iframes allow you to embed an HTML page inside of another HTML page. Since saveWidget()
saved our map widget as an HTML file that’s nothing but our map, we can then embed it into our page using an iframe. Jekyll allows raw HTML in markdown files which it ignores and passes through untouched into the final HTML files it produces. Here’s the code I used for the map in this post.
<iframe src="/files/html/posts/predoc_map.html" height="600px" width="100%" style="border:none;"></iframe>
The main argument is src="..."
, which tells the iframe what content it will contain. Notice that this is the same file path I just specified above in saveWidget()
. As long as that directory exists in your website repo, everything will work smoothly. There are three important arguments in addition to the content of the iframe itself:
height
is how tall you want the iframe to be; here I’ve specified it in pixels, but you can also use inches, centimeters, or percentages as you’ll see belowwidth
is how wide you want the iframe to be; I’ve used a percentage here because the AcademicPages template is responsive and will resize itself on smaller screensstyle
is where I tell the iframe not to include a border so it blends seamlessly with the rest of the page
The finished product
Here’s what the final map looks like. If you didn’t know the extra effort it took, it would blend seamlessly into the page. Theoretically this should work for any HTML widget, like those produced by the plotly
R package. If you haven’t checked plotly
out, you really should. It can turn ggplot2
plots into interactive widgets with a single line of code!