Designing aesthetic maps in R with ggplot2 & svglite

ggplot2
R
Geospatial
svglite
SVG
JavaScript
rspatial
tidyverse
30DayMapChallenge
A method to create beautiful lightweight interactive maps in R using ggplot2 and SVG (Scalar Vector Graphics).
Author

Vincent Clemson

Published

October 29, 2023

Modified

October 30, 2023

Figure 1: A choropleth map visualizing the population within each county of the continental United States in 2015.

The map was designed using ggplot2. Saving the map using svglite and editing the output has the potential to create beautiful interactive & lightweight maps.

TLDR

I show off how to use R packages in the #rspatial & tidyverse ecosystems to build both aesthetic & interactive maps. I also use some slightly advanced web dev & scraping techniques to create the finished product.

Interact with it yourself: Figure 4.

Intro

At the end of November, I plan on packing up my belongings and trekking 6,500 miles across the continental United States from Virginia out to the West coast (🗣️ roadtrip 🛣️).

Being who I am, I figured it’d be a great idea to plot out the journey using R. Additionally, the #30DayMapChallenge is approaching, so I figured that it was a great time to create a new thing in preparation for the challenge. Most of my time went into development over the past week, so the next steps for me are the trip planning, but I think the proof of concept is a great start!

Thoughts on Data Driven JavaScript Visuals

Plots & maps are cool, and interactive plots can be even cooler, but the bloat of adding external JavaScript libraries can sometimes create a performance hit to a user’s experience on the web. A typically approach within the R user community to embed data driven interactive graphics in web content involves using the incredible htmlwidgets package. There are several HTML widget R packages. These packages work by using htmlwidgets to bind & format data as JSON input to their respective JavaScript visualization libraries and then to output the results as an HTML element.

My biggest gripe with HTML widgets is that they embed the entire JavaScript library passed to them. So, if you say want 2 unique HTML widgets on your web page that are created from the same htmlwidgets ‘binding’ R package, then you’ll have 2 identical copies of the same JavaScript library downloaded to your web browser. Having the option to embed the library in say your web pages <head> element could help cut down on some of this bloat.

Aside from this, I think that being able to create data driven interactive graphics without the need for external JavaScript libraries has a lot of potential when it comes to creating beautiful plots in the browser. Building plots with SVG has been conquered by D3.js as well as many others that have taken significant influence from D3, such as plotly & highcharter. Additionally, Observable has changed the game when it comes to quickly prototyping & sharing new data visuals in the browser. Making it easier than ever before to collaborate & maximize one’s reach when developing a new dataviz powered by web technologies. In particular, its lead by Mike Bostock, one who I would call a super human with eons of data visualization expertise. At Observable, they’re building the Observable Plot library to help folks efficiently visualize tabluar data in the browser.

Plot is inspired by the grammar of graphics style, which is the foundation that ggplot2 was built upon. I think that the ability to turn a ggplot into a an interactive graphic can be extremely powerful and can open up many new doors into the world of dataviz. Using plotly and the incredible plotly::ggplotly designed by Carson Sievert is an htmlwidgets approach to doing this. The approach to use svglite to save ggplot2 created plots and then post process these graphics allows you to keep the design portion of the dataviz mostly outside of the web browser and within the Plot pane of your IDE (i.e. RStudio). The choice to post process the SVG output within R allows one to minimize JavaScript library dependencies, but presents the challenge of making the SVG interactive.

All in all, I’m excited to see where this goes.

The data

I used the 2015 U.S. Census county population data stored within the usmap package. More updated U.S. Census data from the U.S Census Bureau can be accessed via the tidycencus and censusapi R packages.

As a precursor, I filtered out Alaska & Hawaii because I am not planning to travel that far just yet, i.e. I want the focus of the plot to be on CONUS. I used the Albers equal area conic projection centered on the US. The proj4 string of the projection was highlighted in a neat post by Bob Rudis a few years back.

The key variable that I create here for the visual is the percentage of the population per state for each county. I ultimately display this using a log scale in the color palette of the plot to better visualize large differences in the county population. Additionally, this percentage gets used as hover text in the interactive figure.

usapop-data.R
library(sf)
library(dplyr)
library(usmap)

# data load & format ----

d <- left_join(usmap::us_map(regions='county'), usmap::countypop, by = c('fips', 'abbr', 'county')) |>
  as_tibble()

# must create individual polygons first before entire counties of multiple polygons
d_sf <- d |> st_as_sf(coords = c('x', 'y'), crs = usmap::usmap_crs()) |> st_transform(4326) |>
  group_by(group) |>
  summarise(
    geometry = st_combine(geometry) |> st_cast('POLYGON'),
    across(c(abbr, pop_2015, full, county, fips), unique)
  ) |>
  group_by(fips) |>
  summarise(
    geometry = st_combine(geometry),
    across(c(abbr, pop_2015, full, county), unique),
    l_group = list(group)
  ) |>
  mutate(pop_full = sum(pop_2015, na.rm = T), .by = full) |>
  mutate(
    point = lapply(geometry, function(p) p |> st_cast('POINT') |>
      suppressWarnings()) |> st_sfc() |> st_set_crs(4326),
    county = sub(x = county, pattern = ' County', replacement = ''),
    pop_perc = pop_2015/pop_full
  ) |>
  mutate(
    text = paste0(
      sprintf(paste0(county, ', ', full, ' ', '%.2f%%'), 100*pop_perc),
      # for mapping SVG elements
      '|', lapply(geometry, length) |> unlist()
    )
  )

# ggplot ----
## plot format objects ----
proj <- '+proj=laea +lat_0=45 +lon_0=-100 +x_0=0 +y_0=0 +a=6370997 +b=6370997 +units=m +no_defs'
d_sf_us <- d_sf |> filter(! full %in% c('Hawaii', 'Alaska')) |>
  summarise(geometry = st_combine(geometry)) |>
  mutate(geometry = geometry |> st_make_valid() |> st_union()) |>
  st_transform(proj)
d_sf_states <- d_sf |> group_by(full) |> summarise(geometry = st_combine(geometry)) |>
  mutate(geometry = geometry |> st_make_valid() |> st_union(), .by = full) |>
  filter(! full %in% c('Hawaii', 'Alaska')) |>
  st_transform(proj)
d_sf_plot <- d_sf |>
  filter(! full %in% c('Hawaii', 'Alaska')) |>
  st_transform(proj)

The ggplot

With the showtext package, I used a custom font that I downloaded from Fontsgeek, Black Chancery. Additionally, to give the plot a bit of a 3D effect, I added shadow using ggfx. It adds some good taste IMO.

When it came to trying to save an identical copy of the rendered ggplot graphic within the RStudio Plot pane programmatically, I’ve found this to be very challenging. The ggplot::ggsave function has width, height, unit, & dpi parameters that can be specified. There’s also a great post by Christophe Nicault on setting the text DPI via the showtext package to match the DPI used within ggsave by the PNG graphics device. However on my 14” MBP, these methods all seem to fall short, with the ggsave approach font being larger than the font displayed in the Plot pane. It seems that the Plot pane does some magic when it comes to resizing. You can right click your image & choose “Save image as…”, which allows you to save what you’re looking at to file.

I think that there are benefits & drawbacks to both approaches. Below I provide the ggplot code version of the map. The last line of the snippet includes a ggsave call that builds a plot with text slightly bigger than what’s expected. Additionally, the ggfx shadow seems less prominent in the ggsave version, and the overall width / height of the plot content appear smaller as well.

Even if we switch the dpi to 72, and calculate the same width / height of the RStudio pane version to get the same resolution image in the ggsave version, i.e. both dpi & pixel width/height match amongst both files, we get even smaller text and other distortions.

Thus, I’ve included both versions in Figure 2 for comparison, the version that I saved from the RStudio plot pane, as well as the ggsave version. MacOS Preview app info on the .png tells me that the plot pane version ends up saving with 72 dpi (dots per inch), while the ggsave version has 254 as I’ve specified. Both files have near equivalent pixel size dimensions (3023 × 1889), with the key difference being the dpi.

usapop-ggplot.R
library(sf)
library(ggfx)
library(dplyr)
library(usmap)
library(tibble)
library(ggplot2)
library(showtext)

# mbp 14" dpi / height / width in inches
dpi <- 254
width <- 11.90157
height <- 7.437008

showtext_opts(dpi = dpi)
showtext_auto(enable = TRUE)
font_add("Black Chancery", "BLKCHCRY.TTF")

g <- ggplot(data = d_sf_plot) +
  with_shadow(
    sigma = 5, x_offset = 5, y_offset = 5,
    geom_sf(data = d_sf_states, color = "black", fill = 'white')
  ) +
  geom_sf(mapping = aes(fill = log(pop_2015)), linewidth=0.1) +
  scale_fill_continuous(low = 'yellow', high = 'darkgreen', na.value='yellow', guide = 'none') +
  geom_sf(data = d_sf_states, color = "black", fill = 'transparent') +
  labs(title = 'Population Across America') +
  theme_void() +
  theme(
    plot.background = element_rect(fill = "white", color = "white"),
    text = element_text(family='Black Chancery'),
    plot.title = element_text(vjust = 0.01, hjust = 0.5, size = 75)
  )
gb <- ggplot_build(g)

g <- g +
  annotate(
    "text", label = "© Vincent Clemson", family='Black Chancery', size = 14/.pt,
    x = gb$layout$panel_params[[1]]$x_range[1]+1100000,
    y = gb$layout$panel_params[[1]]$y_range[1]+500000,
  )
ggsave(plot = g, width = width, height = height, unit = 'in', dpi = dpi, filename = 'usapop-ggplot-ggsave.png')
(a) RStudio Plot Pane Version
(b) ggsave Version
Figure 2: Two ggplot versions of the county population map to compare save methods.

SVG & XML Editting

SVG (Scalar Vector Graphics) is itself its own markup language, based in XML and similar to HTML. The key difference between HTML & SVG being that HTML specifies how text is displayed to the browser, while SVG describes how graphics are displayed.

Because SVG files are written as XML, they can be loaded as objects and edited in scripting languages, similar to how commonly used web scraping tools work. Using the xml2 & rvest packages in combination from the tidyverse can help us run query selectors on the XML markup to add and remove XML element nodes as necessary.

Using the svglite package/graphics device within ggsave function calls, we can convert & save ggplots to SVG elements, and even use our custom fonts within them.

Note

showtext_auto(FALSE) should be ran within interactive R sessions that have set showtext_auto(enable = TRUE) before saving ggplots via ggsave to .svg.

Adding Iteractivity

Using vanilla JavaScript, I add a tooltip that reads the title attribute of each county boundary SVG <path> within the created choropleth group (<g>) element, e.g <g class="tooltip"><rect/><text></text></g>. The SVG tooltip element itself is a single <rect/> & <text> pair rapped in a group. When creating the SVG/XML document in R, I add it as the last graphic element in the <svg> container so that it displays on top of everything else. JavaScript handles updating the content of the <text> using event handlers on mouse movement. The tooltip was inspired by Lee Mason’s Basic SVG Tooltip Observable notebook. It differs in that it’s implemented as native SVG, rather than using a <div> wrapped in a <foreignObject>, which is incompatible in all browsers.

Code
svgChoro = document.querySelector('svg.svglite > g.choro')
svgBords = document.querySelector('svg.svglite > g.borders')
svgBordE = document.querySelector('svg.svglite > g.borders > path.end')
tooltipG = document.querySelector('svg.svglite g.tooltip')
tooltipR = document.querySelector('svg.svglite g.tooltip rect')
tooltipT = document.querySelector('svg.svglite g.tooltip text')
tooltipT.innerHTML = 'A'
const pad = 4
const xR = 0.5 // for width of path
const yR = 0.5
let tooltipTBox = tooltipT.getBBox()
let hT = tooltipTBox.height
let wT = tooltipTBox.width
let hR = hT+pad*3+xR
let wR = wT+pad*2.5+yR
let xT = wR/2
let yT = hR/2
tooltipR.setAttribute('x', xR)
tooltipR.setAttribute('y', yR)
tooltipT.setAttribute('x', xT)
tooltipT.setAttribute('y', yT)
tooltipR.setAttribute('width', `${wR}px`)
tooltipR.setAttribute('height', `${hR}px`)
// right & bottom pixel offseft
const mouseOffset = [0,0]
const svgBox = document.querySelector('svg.svglite').getBBox()

// element to be replaced by hovered element
svgBords.insertAdjacentHTML('beforeend', '<path class="end"></path>')

svgChoro.onmouseover = function(e) {
  if (e.target.nodeName == 'path') {
    const bbox = e.target.getBBox()
    e.target.classList.add('hover', 'end')
    bordersGEnd = document.querySelector('svg.svglite > g.borders > path.end')
    // replacement of elements
    const dummy = document.createComment('')
    bordersGEnd.replaceWith(dummy)
    e.target.replaceWith(bordersGEnd)
    dummy.replaceWith(e.target)
    showTooltip(bbox.x, bbox.y, bbox.width, bbox.height, e.target.getAttribute('title'))
    // remove hover and put path elements back in original groups
    e.target.onmouseout = function(e) {
      e.target.classList.remove('hover', 'end')
      svgChoro.appendChild(e.target)
      svgBords.appendChild(bordersGEnd)
    }
  }
}

svgBords.onmouseleave = hideTooltip
// Chrome compatibility
svgChoro.onmouseleave = hideTooltip

function hideTooltip(e) {
  tooltipG.style.visibility = 'hidden'
}
function showTooltip(x, y, w, h, text) {
  tooltipT.innerHTML = text
  tooltipTBox = tooltipT.getBBox()
  hT = tooltipTBox.height
  wT = tooltipTBox.width
  hR = hT+pad*2
  wR = wT+pad*2
  xT = wR/2
  yT = hR/2
  tooltipT.setAttribute('x', xT)
  tooltipT.setAttribute('y', yT)
  tooltipR.setAttribute('width', `${wR}px`)
  tooltipR.setAttribute('height', `${hR}px`)
  const bottom_target = [x-w/2-mouseOffset[0], y+h+mouseOffset[1]]
  let xG = bottom_target[0]
  let yG = bottom_target[1]
  const tooltipRBox = tooltipR.getBBox()
  // uses max tooltip size to not going over right edge
  if (xG > svgBox.width - tooltipRBox.width) { xG = x - (tooltipRBox.width < 150 ? tooltipRBox.width : 150) }
  // keeps from going over left edge
  if (xG < 0) { xG = 0 }
  // keeps from going over bottom
  if (yG > svgBox.height - tooltipRBox.height) { yG = y - tooltipRBox.height - mouseOffset[1] }
  // tooltips will always be under top
  tooltipG.setAttribute('transform', `translate(${xG},${yG})`)
  tooltipG.style.visibility = 'visible'
}

Styling for the tooltip:

Code
g.borders > path.hover {
  stroke-width: 0.5 !important;
  stroke: rgb(226, 226, 226) !important;
}
g.tooltip {
  visibility: hidden;
}
g.tooltip rect {
  fill: rgba(255, 255, 255, 0.7);
  rx: 2px;
  ry: 2px;
}
g.tooltip text {
  font-size: 16px;
  dominant-baseline: middle;
  text-anchor: middle;
}

The Whole Game

Combining all of these techniques involves quite a bit of prototyping. There’s a lot to pick apart, especially with mapping elements of the ggplot to the svg output correctly. Figure 3 displays an outline of the core components of the script. You can dive deep into the full script below. I originally built this to add within the about page/path of my site, so this is why paths are relative to the about/ directory.

When it comes to prototyping CSS/JavaScript, I created 2 methods to add styles and scripts. For testing purposes, I setup the CSS/JavaScript to source external scripts. This allows you to set breakpoints on script files sourced by the SVG in Developer Tools, as well as edit stylesheets natively in the Sources/Styling Editor. For ‘production’, I keep the graphic entirely self-contained, e.g. not sourcing any external scripts. This means that the SVG element can be downloaded and used in any browser context, without the need for the external .css/.js dependencies. Additionally, I embed the external .ttf font as base64 encoded data.

usapop-svg.R
library(sf)
library(ggfx)
library(xml2)
library(dplyr)
library(purrr)
library(rvest)
library(usmap)
library(tibble)
library(tictoc)
library(ggplot2)
library(svglite)
library(showtext)
library(base64enc)

tic()

# data load & format ----
d <- left_join(usmap::us_map(regions='county'), usmap::countypop, by = c('fips', 'abbr', 'county')) |>
  as_tibble()
# must create individual polygons first before entire counties of multiple polygons
d_sf <- d |> st_as_sf(coords = c('x', 'y'), crs = usmap::usmap_crs()) |> st_transform(4326) |>
  group_by(group) |>
  summarise(
    geometry = st_combine(geometry) |> st_cast('POLYGON'),
    across(c(abbr, pop_2015, full, county, fips), unique)
  ) |>
  group_by(fips) |>
  summarise(
    geometry = st_combine(geometry),
    across(c(abbr, pop_2015, full, county), unique),
    l_group = list(group)
  ) |>
  mutate(pop_full = sum(pop_2015, na.rm = T), .by = full) |>
  mutate(
    point = lapply(geometry, function(p) p |> st_cast('POINT') |>
      suppressWarnings()) |> st_sfc() |> st_set_crs(4326),
    county = sub(x = county, pattern = ' County', replacement = ''),
    pop_perc = pop_2015/pop_full
  ) |>
  mutate(
    text = paste0(
      sprintf(paste0(county, ', ', full, ' ', '%.2f%%'), 100*pop_perc),
      # for mapping SVG elements
      '|', lapply(geometry, length) |> unlist()
    )
  )

# ggplot ----
## plot format objects ----
proj <- '+proj=laea +lat_0=45 +lon_0=-100 +x_0=0 +y_0=0 +a=6370997 +b=6370997 +units=m +no_defs'
d_sf_us <- d_sf |> filter(! full %in% c('Hawaii', 'Alaska')) |>
  summarise(geometry = st_combine(geometry)) |>
  mutate(geometry = geometry |> st_make_valid() |> st_union()) |>
  st_transform(proj)
d_sf_states <- d_sf |> group_by(full) |> summarise(geometry = st_combine(geometry)) |>
  mutate(geometry = geometry |> st_make_valid() |> st_union(), .by = full) |>
  filter(! full %in% c('Hawaii', 'Alaska')) |>
  st_transform(proj)
d_sf_plot <- d_sf |>
  filter(! full %in% c('Hawaii', 'Alaska')) |>
  st_transform(proj)
## plot object ----
g0 <- list(
  geom_sf(mapping = aes(fill = log(pop_2015)), linewidth=0.1),
  scale_fill_continuous(low = 'yellow', high = 'darkgreen', na.value='yellow', guide = 'none'),
  geom_sf(data = d_sf_states, color = "black", fill = 'transparent'),
  geom_sf_text(aes(geometry = point, label = text)),
  labs(title = 'Population Across America'),
  theme_void(),
  theme(
    text = element_text(family='serif'),
    plot.title = element_text(vjust = 0.01, hjust = 0.5, size = 55)#size = 75)
  )
)
g <- ggplot(data = d_sf_plot) + g0
gb <- ggplot_build(g)
g <- g +
  annotate(
    "text", label = "© Vincent Clemson", family='serif', size = 12/.pt,
    x = gb$layout$panel_params[[1]]$x_range[1]+1100000,
    y = gb$layout$panel_params[[1]]$y_range[1]+500000,
  )

gfx <- ggplot(data = d_sf_plot) +
  with_shadow(
    sigma = 3, x_offset = 3, y_offset = 3,
    geom_sf(data = d_sf_us, color = "black", fill = 'white')
  ) +
  g0

## save ggplot to svg ----
# must write to file - uses {svglite}
fonts <- list(serif = list(plain = list(alias = 'Black Chancery Regular', file = 'about/BLKCHCRY.TTF')))
# mbp 14" height / width in inches
width <- 11.88212
height <- 7.465619
ggsave(plot = g, width = width, height = height, filename = 'about/usapop.svg', user_fonts = fonts)
ggsave(plot = gfx, width = width, height = height, filename = 'about/usapopfx.svg')

# svg editting ----
## read svg as xml document for editting ----
doc <- read_xml('about/usapop.svg')
docfx <- read_xml('about/usapopfx.svg')
ns_d1 <- xml_ns(doc) |> as.list() |> pluck('d1')
xml_ns_strip(doc)
xml_ns_strip(docfx)
## gather svg element data & non-dummy data to edit ----
# then move to new node & remove moved elements
xn_g <- doc |> html_elements('g') |> pluck(2)
xn_txt_use <- xn_g |> html_elements('text') |> rev() |> pluck(1)
xn_img <- docfx |> html_elements('image') |> pluck(1)
doc |> html_elements('g') |> pluck(3) |> xml_add_child(xn_txt_use)
doc |> html_elements('g') |> pluck(1) |> xml_add_child(xn_img)
xml_remove(xn_txt_use)
xn_g_path <- xn_g |> html_elements('path')
xn_g_txt <- xn_g |> html_elements('text')
## hover / popup metadata ----
l_txt <- xn_g_txt |> lapply(function(i) strsplit(html_text(i), '\\|') |> unlist())
xby <- l_txt |> lapply(function(i) pluck(i, 2)) |> unlist() |> as.integer()
txt <- l_txt |> lapply(function(i) pluck(i, 1)) |> unlist()
xby_expand <- lapply(xby, function(x) rep(x, x)) |> unlist()
txt_expand <- map2(txt, xby, \(t, x) rep(t, x)) |> unlist()
d_key <- tibble(xby_expand, txt_expand) |>
  mutate(ID = 1:n())
# identify county paths composed of more than 1 polygon
idx_1 <- d_key |> filter(xby_expand > 1) |>
  reframe(ID = ID[1], .by = txt_expand) |> pull(ID)
for(i in idx_1) {
  for (j in 1:(d_key |> filter(ID == i) |> pull(xby_expand) - 1)) {
    d_at <- xn_g_path[[i]] |> xml_attr('d')
    xn_g_path[[i]] |> xml_attr('d') <- paste(d_at, xn_g_path[[i+j]] |> xml_attr('d'))
  }
}
## removal of dummy layers ----
for(i in idx_1) {
  for (j in 1:(d_key |> filter(ID == i) |> pull(xby_expand) - 1)) {
    xml_remove(xn_g_path[i+j])
  }
}
n_before <- length(xn_g_path)
## create new path list sets with updated nodes ----
xn_g_path <- xn_g |> html_elements('path')
n_after <- length(xn_g_path)
n_removed <- n_before - n_after
n_tobe <- length(xn_g_txt)
n_other <- n_before - n_removed - n_tobe
xn_g_path_1 <- xn_g_path[1:n_tobe]
xn_g_path_2 <- xn_g_path[(n_tobe+1):(n_after)]
## combine individual polygons into group ----
# combines all polygons into same svg path element for counties
map2(xn_g_path_1, txt, \(p, t) xml_attr(p, "title") <- t) |> invisible()
xml_remove(xn_g_txt)
x_tooltip <- read_xml('<g class="tooltip"><rect/><text></text></g>')
## add separate layers to separate nodes ----
xb_g <- read_xml('<g class="borders"></g>')
lapply(xn_g_path_2, function(i) xb_g |> xml_add_child(i)) |> invisible()
xml_remove(xn_g_path_2)
## styling / javascript ----
### gather near equivalent to <head> ----
x_def <- doc |> html_elements('defs') |> pluck(1)
xn_g |> xml_attr('class') <- 'choro'
### css styling ----
x_style <- x_def |> html_element('style')
style_t <- x_style |> html_text()
style <- readLines('about/usapop.css') |> paste0(collapse = '\n')
### fonts ----
fdata <- readBin(fonts$serif$plain$file, what = "raw", n = 1e6)
style_f <- paste0(c(
  'g.tooltip {',
  paste0('font-family:', fonts$serif$plain$alias, ';'),
  '}',
  '@font-face {',
  paste0('font-family:', fonts$serif$plain$alias, ';'),
  paste0('src: url(data:font/ttf;base64,', base64encode(fdata), ') format("truetype");'),
  '}'
), collapse = '\n')
#### javascript ----
x_script <- read_xml('<script></script>')
script <- readLines('about/usapop.js') |> paste0(collapse = '\n')
## add all to xml ----
### uncomment for testing ----
# x_style_l <- read_xml('<link xmlns="http://www.w3.org/1999/xhtml" rel="stylesheet" href="usapop.css"/>')
# doc |> xml_add_child(x_style_l, .where = 0)
# xml_text(x_style) <- paste0(style_t, '\n', style_f)
# x_script <- read_xml('<script xlink:href="usapop.js"></script>')
### for production - self-contained ----
xml_text(x_style) <- paste0(style_t, '\n', style, '\n', style_f)
xml_text(x_script) <- script
doc |> xml_add_child(xb_g)
doc |> xml_add_child(x_tooltip)
doc |> xml_add_child(x_script)
## output & cleanup ----
# re-add namespace & fix attribute unit
xml_attr(doc, 'xmlns') <- ns_d1
# unset to allow element to take full advantage of viewBox
xml_attr(doc, 'width') <-  NULL
xml_attr(doc, 'height') <- NULL
file.remove('about/usapopfx.svg')
write_xml(doc, 'about/usapop.svg')

toc()
Figure 3: Outline within RStudio of the US population SVG graphic R script

Finished Product

Enjoy the viz!

Figure 4: The ggplot converted to SVG. A choropleth of the population of the continental United States in 2015.

Wrap-up

If you have any thoughts or questions, feel free to let me know what you think in the comments below. You’ll need to sign in to your GitHub account to do so. Like my work? Feel free to reach out.

We only have one rock, and it’s a beautiful one. Thanks for reading! ✌️🌍

Back to top

Reuse

© 2024 Vincent Clemson | This post is licensed under <a href='http://creativecommons.org/licenses/by-nc-sa/4.0/' target='_blank'>CC BY-NC-SA 4.0</a>

Citation

For attribution, please cite this work as:
Clemson, Vincent. 2023. “Designing Aesthetic Maps in R with Ggplot2 & Svglite.” October 29, 2023. https://prncevince.io/posts/geo/maps-ggplot-svglite.