top of page
  • Writer's picturehelenmakesmaps5

How-to: Make your first interactive map in R

Updated: Aug 9, 2022

Interactive maps are such an empowering form of cartography, and have such wide-ranging application that knowing how to create one is an important skill for any cartographer to have.

While there are some great solutions out there for creating interactive maps without needing any coding skills at all, I think it's great to be able to know how to code your own interactive map. It gives you that bit more flexibility in terms of how you display your data, but also in terms of sharing your map without license restrictions

There are lots of options for doing this, and for this tutorial we're going to use R. Coding up a map in R is surprisingly straightforward, and at its most simple it only really needs a few lines of code. I think lots of people shy away from coding web maps as it looks really complicated, but once you understand the different building blocks you need it starts to make a lot more sense. And the thing about coding is you only need to start from scratch once, then you can re-use and adapt your code as often as you need!

When I'm writing out coding tutorials, I try to share code examples a mixture of images and text snippets to encourage you to write out the code rather than just copy+paste - I feel like it's the best way to learn what you're doing.

The end goal

This is the map we're going to be making today! It gives users the options to:

  • Add multiple layers

  • Turn layers on and off

  • Switch between basemaps

  • Adding hover effects

  • Configure pop-ups

  • Zoom to the world extent

  • Centre on their current location

Want to skip ahead to a certain section? I got you.



You'll need

You aren't going to need any coding skills for this - I'm pitching this at beginner level. However, a decent understanding of mapping, cartography and GIS will be helpful.

In terms of software, I'm going to be using the following:

  • QGIS for a quick bit of pre-processing (you could use any desktop GIS or translator which can write to geojson for this). You could do this in R but I'm assuming most readers will be more GUI GIS users.

  • RStudio to code the map. You'll need to download and install both R and RStudio for this.

That's it! In terms of data, you can use anything you want - as long as it's GIS ready! This tutorial is going to show you how to create a choropleth interactive map using polygons, as well as a stations point layer so whatever you want to use should be covered.

What am I using? I've recently started the process of trying to buy my first house in London and it SO depressing and I'm obsessed with it all, so I'm going to create a map of house prices across London. If you want to use the same data, you can download the house price data by LSOA from the London Datastore here and the LSOA feature layer from the ONS geoportal here. Before using this data I extracted the values I'm interested in, tagged "no data" values and finally joined the two together. The stations data can be extracted from Ordnance Survey Zoomstack here.


1 Prepping the data

Normally I would bang on and on about how you should always map in a projected coordinate system to avoid distortions and errors. I stand by that for most static maps, however we're creating an interactive map which has scope for the user to pan and zoom around the entire world. This means we need to convert our data into the geographic coordinate system WGS84. We also want to convert it into a geojson format. While R can read most common GIS data formats, I find geojson is the most straightforward to work with and therefore most suitable for beginners.

Handily, in QGIS we can achieve both of these things in just one process. In your Layers panel right click on your layer and choose Save features as... Change the file format to Geojson and name your layer something snappy - I'm calling mine LSOA_MeanHousePrices2017.geojson. At this point make sure you change the coordinate system to WGS 84 (ESPG:4326).


2 Loading your data into R

Now our data is prepped, we can get coding! Open R Studio.

  1. First we need to install the R packages we need. Type in install.packages("leaflet") and install.packages("rgdal"). Run this code. Leaflet is the package we'll be using for our web map, while Rgdal has some great general GIS functionality, and we'll be using it here to import our data.

  2. Clear this code, then load these two packages up with the code library(leaflet) and library(rgdal). Run this code.

  3. I'm going to create a layer called LSOA (by writing "LSOA <- "), but you can use a name more appropriate to your data. Next use the readOGR() function to read the LSOA_MeanHousePrices2017.geojson layer we've created earlier. Make sure to use forward - rather than back - slashes in your link. I spent so long working out that this was causing me problems!

  4. Now use plot() on the LSOA layer to create a really simple plot of your data to check it's imported correctly.

It should look a bit like this:


LSOA <- readOGR("C:/Your file location here / LSOA_MeanHousePrices2017.geojson" )


3 A basic interactive map

Now let's create a really simple interactive map using leaflet.

Underneath the above code, write out the below snippet. This creates a map object "m", uses addProviderTiles() to add a basemap (I'm using the great all-purpose Carto Positron), and addPolygons() to import the LSOA data. It also applies some simple styling to the polygons using the options stroke, weight, color, opacity etc. Calling "m" will then load this map. Depending on the complexity, this may take a while and is a great moment for a tea break.

When you call m, your simple interactive map should appear and look like the below - and it really is that easy! Now shall we kick it up a gear?

Sidebar: If you're working with point data, skip down to section 8 where I'll walk you through how to add these - you essentially call addCircleMarkers(dataLayer, longitudeField, latitudeField) instead of addPolygons(). If you're adding lines, use the code addPolylines().


4 Choropleth maps

Quick shout-out to the #30daymapchallenge for finally teaching me how to spell choropleth correctly! Choropleth maps are a fancypants way of talking about a map which users colour to convey information - like house prices.

The first thing we need to do is set our data bins, so let's go ahead and have a inspect our data. You can run the code names(LSOA) (or whatever name your layer has) to see what your field names are, and then summary(LSOA$MeanHousePrices_Mean_2017) on our house price field to learn a bit more about how your data is structured. Using a $ allows us to use a specific field from a layer, so your code should be structured "summary(LayerName$FieldName)."

Sidebar: data cleaning

I've got some "no data" values in my data which are tagged as -9999. I want to remove these so they don't skew my data. We'll use a function called subset to get rid of all of our £-9999 houses and put them in a separate layer called No Data. If only.

NoData <-subset(LSOA, LSOA$MeanHousePrices_Mean_2017== -9999)
LSOA <-subset(LSOA, LSOA$MeanHousePrices_Mean_2017> -9999)

With my data cleaned, I can see that the maximum house price is £8,061,053 (that's fine, no biggie) and the mean is £609,026 (also fine, I'm not crying - you're crying). The first and third quartiles are also distributed fairly close to the mean, which means a lot of our data points will be found in the £300,000 - £700,000 region, and the more expensive and cheaper houses being more extreme outliers.

I heard somewhere that you should never use more than 7 colours on one map. I have no idea where, but it's a rule I try to stick to - this means I want a maximum of 7 colour bins. I normally use a manual method of data binning. This helps me to find a balance between using a "scientific" binning method (so equal interval, quantiles, natural jenks etc) to stop my map getting too subjective, whilst balancing this with what users can easily understand (so rounded numbers are key) and what tells an interesting data story. Trial and error is a constant with this.

First, call library(colorbrewer) (you may have to install this first if you don't already have this using install.packages("colorbrewer")). The colorbrewer package gives you access to a range of great colour palettes.

To set up your choropleth map, follow these steps:

  1. Create a vector holding your colour bins called LSOABins: LSOABins <- c(0, 400000,600000,800000,1000000,2000000,5000000,1000000). You can adapt these bins as you choose.

  2. Then create your colour scheme called LSOAPal using colorBin: LSOAPal <- colorBin("YlGnBu", domain = LSOA$MeanHousePrices_Mean_2017, bins = LSOABins). This uses the yellow-green-blue colour ramp which is one of my favourites for creating a solid intuitive map. Check out more colour schemes available in the ColorBrewer package here - or experiment with other great colour packages like Viridis, Wes Anderson... or get creative and make your own!

  3. Set the fillColor of your layer to use the colour scheme you've just created fillColor = ~LSOAPal(MeanHousePrices_Mean_2017).

  4. This is also a good time to add in a smoothFactor = 0.3 in your map code so your fairly complex polygons render better. You could also play around with your opacity levels and stroke colour for a cleaner look - I've gone for a more opaque fill and grey stroke.

Altogether, your code should now look a bit like the below.. less than 32 lines of code for a great choropleth interactive map!

You should now be looking at something like this:

Sidebar: other data binning methods

The above code shows you how to set manual data bins, but it's helpful to know how to use automated data binning too. Below are some examples of this code adapted for automated data binning:

  • Quantile: binning the data so there are an equal number of features in each bin, where n is the number of bins

LSOAPal <- colorQuantile(palette = "YlGnBu", LSOA$ MeanHousePrices_Mean_2017, n=7)

Continuous: interpolates a colour ramp into a continuous ramp with unique values.

LSOAPal <- colorNumeric(
  palette = "YlGnBu",
  domain = LSOA$ MeanHousePrices_Mean_2017)


5 Adding a legend

Obviously we know what our map is showing, but anyone looking at it won't have the foggiest. Let's add a legend.

At the end of your map code block after fillOpacity = 0.8) add the beneath code snippet. This will place a legend which matches your data at the bottom right of your map. You can replace the colours and labels as you choose, just make sure you have the same number of them. Using the code "</br>" allows you to add a line break in your legend so it doesn't go on for DAYS.

addLegend("bottomright",opacity = 1,
          colors =c("#ffffcc","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#0c2c84"),
          title = "Average House Price</br>2017, all house sales",
          labels= c("<£400,000","£400,000 - £599,999","£600,000 - £799,999","£800,000 - £999,999","£1 - 2 million", "£2 - 5 million", "£5 - 9 million")


6 Adding pop-ups

Now what we have is a perfectly functional choropleth map - hooray! But to make it really helpful for users, it would be great to add in some pop-ups so they can interrogate specific data points. Let's do that now - we're going to create a new field in our LSOA layer called "popup" and concatenate the information we want our pop-up to show. For example, I want my pop-up to show my LSOA name in bold to show it's the title. I want to follow this with its LSOA code on the next line. Then on a new line I want to show the mean house price for that area, which I want to be formatted with a thousands separator and currency.

I do this with the below code:

#set pop-up content
LSOA$popup <- paste("<strong>",LSOA$lsoa01nm,"</strong>", "</br>", 
                    LSOA$lsoa01cd, "</br>",
                      "Mean house price (2017): £", prettyNum(LSOA$MeanHousePrices_Mean_2017, big.mark = ","))

I use a combination of field names (remember you can call names(YourLayerName) to check your field names and then call them with the format layername$fieldname), free text and formatting tags to create this pop-up. The use of the <strong> formatting tags show my LSOA in bold, with the </br> signifying when the pop-up should move to a new line. The use of prettyNum() allows me to format my number, here to include a thousands separator. Remember everything except your calls to the data need to be encapsulated in "these."

Finally, we need to include the pop-up in our map code block. In your addPolygons() item, add a line popup = ~popup, then run the map.


7 Hover effects

This pop-up is really helpful, but if you have a lot of intricate features on your map it might be difficult to work out which one you're selecting the pop-up for. Adding a hover effect would be really helpful for the user in this instance. This effect changes the appearance of a feature when a mouse hovers over it.

In your map code block (where you just added popup = ~popup) , add a line of code starting with highlightOptions = and add in the below code. This adds a thin pink line to the outside of features when the mouse hovers over them, as well as making their fill more transparent so it's easier to see the basemap. It's good to choose a style for your hover that contrasts with the style of your data, so it's really clear to see.

highlightOptions = highlightOptions(color = "#E2068A", weight = 1.5,
                                                  bringToFront = TRUE, fillOpacity = 0.5),

With your choropleth layer, popup, hover effect and legend, your map code block should be looking something like the below.


8 Adding a second layer - points

Let's add a second data layer so we can start to tell more of a story with our map. I’m going to add station locations as they’re one of the landmarks people use most in London to orientate themselves. Proximity to them is also seen as one of the common causes of high house prices.

When working with point data, you'll need fields holding the latitude and longitude of your features. I'm going to create a "stations" layer and call my stations geojson: stations <- readOGR(“C:/yourfilelocation/Stations.geojson”). Next I’ll add them to my map by inserting a code section similar to addPolygons(), but as these are points rather than polygons, we’re going to use addCircleMarkers(). I’m also going to add a pop-up for this layer using stations$popup <-paste(stations$Name).

This is the code we'll use to accomplish this, with x and y being the fields holding my latitude and longitude data.

addCircleMarkers(data = stations, ~x,~y, radius = 2, stroke = TRUE, color = "#424242", weight = 1, fillOpacity = 1, fillColor ="#FDFDFD", popup = ~popup)%>% 

We'll also take out the bringtofront = TRUE option from the LSOA layer so it doesn't cover the stations.


9 Switching layers on and off

Now we've got a bit more going on in our map, it's actually harder to see some of the data. For example, around Central London the LSOAs are so small and there are so many stations that you can't really see the data behind them when fully zoomed out. We can fix this by adding the ability to turn layers on and off, and it's actually super simple.

We do this firstly by assigning each of our layers to a group within the addPolygons() and addCirclesMarkers() code blocks. So I'm going to assign LSOAs as group = "Average House Price" and stations as group = "Stations". An example of what this should now look like is below.

Next, beneath the addLegend() code block, add an addLayersControl() block, remembering to add a %>% between the two. We're going to add two lines: overlayGroups specifies the groups we'll include, and options allows us to control the appearance of the legend. Add in the below code block, but replace my overlay groups with the groups you've just created for your layers. Run this code.

    overlayGroups = c("Average House Price", "Stations"),
    options = layersControlOptions(collapsed = TRUE))

You should now see a new icon on your map which you can control the layer visibility in. Change the layer control options to FALSE if you want to be able always see the layer visibility options.

We should now add group = "Average House Price" to our addLegend() block to have the legend toggle on/off when users switch between layers.

Let's say we want one of our layers to not be visible when we open the map. We can easily accomplish that by adding hideGroup() to the end of our code block (i.e. outside the addLayersControl() section). I'm going to hide my station layer because they're just too visually cluttering for me to show initially, so I can add %>% hideGroup("Stations") to do this.


10 Switching between basemaps

That was fun - shall we add an option switch between basemaps? This is great functionality to have as it gives users more flexibility to choose a basemap which works better for them and what they're trying to specifically understand from a map.

To do this, as we did with our overlay layers we want to add a group = function to our addProviderTiles() code. My basemap is currently the excellent Carto Positron, but most of my users won't know what that means, so I'm going to call my group "Basemap - greyscale." Next, I'm going to repeat this but add in two more basemaps, but you can add as many as you want! I like to at least have a light basemap, a dark basemap and an aerial/satellite imagery basemap as I think these will cover most user needs. The follows code achieves this:

  addProviderTiles(providers$Esri.WorldImagery, group = "Basemap - aerial") %>%
  addProviderTiles(providers$CartoDB.Positron, group = "Basemap - greyscale") %>%
  addProviderTiles(providers$CartoDB.DarkMatter, group = "Basemap - dark") %>%

There's a huge range of basemaps available here for you to pick from, and you always have the option to use other options from sources like Mapbox or ESRI. Just like designing static maps, it's important to choose a basemap which compliments the story you're trying to tell. The design and content shouldn't overshadow your data but provide supporting contextual information.

Now let's go back to our addLayersControl() code block. Like we did before, add a line of code baseGroups = c("Basemap - dark","Basemap - greyscale","Basemap - aerial") but exchange my group names for the names you gave your groups. This code block should now look like:

    overlayGroups = c("Average House Price", "Stations"),
    baseGroups = c("Basemap - dark","Basemap - greyscale","Basemap - aerial"),
    options = layersControlOptions(collapsed = TRUE))

Now is a good time to revisit the symbology of your overlay layers to check they work against all basemaps.

Note whichever order you you write your groups in will be the order they appear in your layer visibility box. You should also be able to see that the attribution text which credits the creators of your basemap changes as your switch between basemaps, so you don't need to worry about sorting that.


11 Auto-zooms

This bit is mega straightforward but well worth doing to help your users navigate. Use the below code in your map code block to give the user options to zoom to the world or to their current location.

    icon="fa-globe", title="Zoom to Level 1",
    onClick=JS("function(btn, map){ map.setZoom(1); }"))) %>%
    icon="fa-crosshairs", title="Locate Me",
    onClick=JS("function(btn, map){ map.locate({setView: true}); }")))%>%

12 Exporting, sharing and publishing

There are loads of different ways to export, share and publish your map. My web dev skills aren't what I wish they were, and the method I'm going to share is the one I find suits those skills (or lack of) the best.

  1. In Rstudio, above your web map view there will be an Export drop-down list. Open this and select Save as Web Page...

  2. Change the file name to index.html and click ok. This will convert your script to a html file.

  3. Once exported, navigate in windows explorer to where you just saved your html file and double click it... it'll open in your internet browser as a web map!

You can now share this HTML file as you choose for other users to open it on their internet browser - a really handy way of sharing your web map without publishing it to the whole world!

But maybe you want to share it with the whole world? I got you.

Sharing by GitHub

If you don't already have a GitHub

account, head on over to GitHub to create one. Once you've done that, head on over to your profile and open the Repositories tab. Fill out the Create a new repository page below with a snappy name. I like to use the description to credit my data sources. Click create.

Once done, you'll be taken to the Quick set up page. If you're new to github this can look a little overwhelming, but it's fairly straightforward.

  1. On your repository page,

  2. Under Code<> select add a file > upload file and upload the index.html file which you just created.

  3. Commit the changes - you should now see index.html as an item in your main branch.

  4. Go to Settings > Pages which is available from the panel on the left side of the page

  5. Change the source to main and hit save.

  6. Wait...

  7. It may take a few minutes (another great time for a tea) but after a while you should see a great box appear saying "Your site is published at..."

  8. Click the link...

And your map should appear! You can now link people directly to it, or embed it in your website like so!


13 Troubleshooting

Something going wrong in your code? It happens. A lot. Here are a few things that I've learned to check.

  1. Spelling. I swear 95% of my code errors are due to spelling mistakes.

  2. Case sensitivity. Unlike some other languages, R is case sensitive. If you created three layers called case, Case and CASE, they would all be different layers.

  3. Commas, brackets, quote marks and whatever these %>% are called. Making sure you have the right number and locations of these in your code is key to getting it right.

  4. Running code in order. Something it took me ages to work out in R Studio was that clicking "run" only runs the code block which you're currently working on. Any changes to the code blocks before or after won't be ran. You can use ctrl + shift + enter to run the entire code block, although this can be slower and makes it difficult to weed out errors.

  5. Having the right number of things. It may sound simple, but it's easy to miss in a big code block. When you're setting a choropleth map, you need to have the same number of bins as colours. When you're creating a legend, you need to have the same number of colours as labels.

  6. You only need to install packages once, but load them every time. Every time you want to create a map using leaflet, for example, you need to make sure you're loading that package.

  7. Using American English. Colours needs to be "colors!"


That's it!

I hope you enjoyed learning how to make an interactive map in R and that you can see it's not so scary! I've published the code for this map at my github page here so feel free to have a root through that if you get stuck - or drop me a message on twitter @helenmakesmaps and I'll see if I can help! Also please do share any of your creations with me - writing these tutorials can be a lot of work, and it makes me so happy if I hear they help even one person!

Thanks for reading!

Recent Posts

See All


bottom of page