Make a Custom Art Map... Using only R!

I work primarily in R every day doing data-science-type stuff, but I love making maps and I love graphic design. When I make the maps that I sell, I always use Adobe illustrator and I never use R. But since it's pretty easy to plot maps in R, I thought it would be fun to make a map in R that's nice enough to print out and hang on your wall.

For this map, I imagined a couple who live in Philly and want to document some of the places that are meaningful to them. Here's the final result:

All of the code that you need, as well as a list of sources are on my github. Note that not all of the data is in the project. You'll have to download the streetlines shapefile and the font yourself. Otherwise, everything you need to recreate the map is on github.

Getting Started

After starting R and loading the necessary libraries you'll need to load the shapefile of all of the street centerlines in Philadelphia. If you've never worked with shapefiles before, note that even though we only use the .shp file, you need to keep every file that came with the shapefile, or you won't be able to load the .shp file into R.

Next we'll convert the shapefile to a data table using fortify(). This is done so that we can use ggplot2, by far the best R plotting library (imho).

After fortifying the data we want to determine two points that we can use to crop, i.e. subset, the data. This step isn't necessary, but if you dont' subset you'll have a very dense, hard to view map. You can see what the plot of the whole city would look like by simply calling plot() on the shapefile datat that you read in.

While you're getting the locations of your bounding box, you can also find the data points that you want to add to the map. The tricky thing here is that you need to convert the data points to the right coordinate system from the standard latitude / longitude coordinates that you may be used to. I used this website to upload a csv of latitude and longitude and convert them to state plane zone 3772 - PA South. This page will convert a single latitude / longitude point.

At this point, we can make a plot of the cropped map with the points added. It will look like this:


ggplot(phila.streets.plot[lat < lat.n & lat > lat.s & long < lon.e & long > lon.w],
       aes(x = long, y = lat, group = id)) +
  geom_line() +
  geom_point(data = map.points, aes(x = long, y = lat, group = NULL)) +
  coord_equal()
          

Not great. Definitely not something that you would look to for aesthetic pleasure. So we'll have to work some magic.

Remove annotation completely from the map (i.e. axes, labels, grids)

To remove all annotation, we'll just add the custom theme defined in the script theme_empty.R. Now the plot looks like this:


ggplot(phila.streets.plot[lat < lat.n & lat > lat.s & long < lon.e & long > lon.w],
       aes(x = long, y = lat, group = id)) +
  geom_line() +
  geom_point(data = map.points, aes(x = long, y = lat, group = NULL)) +
  coord_equal() +
  new_theme_empty
          

Use hearts instead of points for the place markers.

To do this, instead of using geom_point, we'll use geom_text and use the unicode character for heart as the text label. You could use any unicode character here, it doesn't have to be a heart. In this case our unicode character is '\u2665'.


ggplot(phila.streets.plot[lat < lat.n & lat > lat.s & long < lon.e & long > lon.w],
       aes(x = long, y = lat, group = id)) +
  geom_line() +
  geom_text(data = map.points, 
            aes(x = long, y = lat, group = NULL, label = rep('\u2665', nrow(map.points))),
            size = 18, color = "#FADBDB") +
  coord_equal() +
  new_theme_empty
          

Add a title with a custom font.

To add the title, we'll use geom_text again. We could add an actual title, but with geom_text we'll have more control over the location of the title. To use a custom font, we'll use the R package extrafont which let's you load in fonts on your computer for use in R. The font I chose is designed to look like writing on a chalkboard, you can get it here. Once you've imported the fonts on your computer, just specify family in the geom_text title line. You'll also need to increase the y-axis limits to make room for the title above the map.


ggplot(phila.streets.plot[lat < lat.n & lat > lat.s & long < lon.e & long > lon.w],
       aes(x = long, y = lat, group = id)) +
  geom_text(data = data.table(x = lon.w, y = lat.n + 1200, group = "a"),
            aes(x = x, y = y, group = group, label = "Blake and Chloe take on Philly"),
            family = "KGMakesYouStronger", size = 16, hjust = 0) +
  scale_y_continuous(limits = c(lat.s, (lat.n+2000))) +
  geom_line() +
  coord_equal() + #very imprtant so that your map doesn't look "streched"
  geom_text(data = map.points, 
            aes(x = long, y = lat, group = NULL, label = rep('\u2665', nrow(map.points))),
            size = 18, color = "#FADBDB") +
  new_theme_empty
          

Add a pretty background & do some more general styling.

Finally we'll add the background and make the lines and text white with a little transparency. To add the background we use the libraries jpg and grid. The only thing you want to keep in mind about the background is that R will stretch the image to fit the plot. That's not a big deal here, but with other images it could lead to a very funny looking plot.


ggplot(phila.streets.plot[lat < lat.n & lat > lat.s & long < lon.e & long > lon.w],
                   aes(x = long, y = lat, group = id)) +
  annotation_custom(rasterGrob(img, width=unit(1,"npc"), height=unit(1,"npc")),
                    -Inf, Inf, -Inf, Inf) +
  geom_text(data = data.table(x = lon.w, y = lat.n + 1200, group = "a"),
            aes(x = x, y = y, group = group, label = "Blake and Chloe take on Philly"),
            color = "grey90", family = "KGMakesYouStronger", 
            size = 16, alpha = 0.9, hjust = 0) +
  scale_y_continuous(limits = c(lat.s, (lat.n+2000))) +
  geom_line(color = "grey90", alpha = 0.75, size = 1.1) +
  coord_equal() + #very important so that your map doesn't look "stretched"
  geom_text(data = map.points, 
            aes(x = long, y = lat, group = NULL, label = rep('\u2665', nrow(map.points))),
            size = 18, color = "#FADBDB") +
  new_theme_empty
          

That's it!

I hope you liked the tutorial! Since this isn't technically a blog, there's no space for comments, but please let me know what you think by emailing me (angela at philamapco dot com) or tweet at me @philamaco. If you try this for yourself, I'd love to see how it works out!