After some research on the Internet, I gave up trying to find an R function to add a scale bar and a North arrow on a map, using ggplot().
So, I would like to present you what I have come up with.

The idea is really basic : we create two polygons for the scale bar, we add some text above, and we draw an arrow. That’s it. The only tricky thing here, is the way to obtain the coordinates of each element. An easy solution is to use the gcDestination function, from the maptools package.

The functions are available on github. To install the package:


devtools::install_github("3wen/legendMap")

From the user’s point of vue, there is only one function to use (scaleBar), and a few arguments to pass. However, this function has some dependencies.

First, let us load some packages:

library(maps)
library(maptools)
library(ggplot2)
library(grid)

Then, we need a function to get the scale bar coordinates:

#
# Result #
#--------#
# Return a list whose elements are :
# 	- rectangle : a data.frame containing the coordinates to draw the first rectangle ;
# 	- rectangle2 : a data.frame containing the coordinates to draw the second rectangle ;
# 	- legend : a data.frame containing the coordinates of the legend texts, and the texts as well.
#
# Arguments : #
#-------------#
# lon, lat : longitude and latitude of the bottom left point of the first rectangle to draw ;
# distance_lon : length of each rectangle ;
# distance_lat : width of each rectangle ;
# distance_legend : distance between rectangles and legend texts ;
# dist_units : units of distance "km" (kilometers) (default), "nm" (nautical miles), "mi" (statute miles).
create_scale_bar <- function(lon,lat,distance_lon,distance_lat,distance_legend, dist_units = "km"){
	# First rectangle
	bottom_right <- gcDestination(lon = lon, lat = lat, bearing = 90, dist = distance_lon, dist.units = dist_units, model = "WGS84")
	
	topLeft <- gcDestination(lon = lon, lat = lat, bearing = 0, dist = distance_lat, dist.units = dist_units, model = "WGS84")
	rectangle <- cbind(lon=c(lon, lon, bottom_right[1,"long"], bottom_right[1,"long"], lon),
	lat = c(lat, topLeft[1,"lat"], topLeft[1,"lat"],lat, lat))
	rectangle <- data.frame(rectangle, stringsAsFactors = FALSE)
	
	# Second rectangle t right of the first rectangle
	bottom_right2 <- gcDestination(lon = lon, lat = lat, bearing = 90, dist = distance_lon*2, dist.units = dist_units, model = "WGS84")
	rectangle2 <- cbind(lon = c(bottom_right[1,"long"], bottom_right[1,"long"], bottom_right2[1,"long"], bottom_right2[1,"long"], bottom_right[1,"long"]),
	lat=c(lat, topLeft[1,"lat"], topLeft[1,"lat"], lat, lat))
	rectangle2 <- data.frame(rectangle2, stringsAsFactors = FALSE)
	
	# Now let's deal with the text
	on_top <- gcDestination(lon = lon, lat = lat, bearing = 0, dist = distance_legend, dist.units = dist_units, model = "WGS84")
	on_top2 <- on_top3 <- on_top
	on_top2[1,"long"] <- bottom_right[1,"long"]
	on_top3[1,"long"] <- bottom_right2[1,"long"]
	
	legend <- rbind(on_top, on_top2, on_top3)
	legend <- data.frame(cbind(legend, text = c(0, distance_lon, distance_lon*2)), stringsAsFactors = FALSE, row.names = NULL)
	return(list(rectangle = rectangle, rectangle2 = rectangle2, legend = legend))
}

We also need a function to obtain the coordinates of the North arrow:

#
# Result #
#--------#
# Result #
#--------#
# Returns a list containing :
#	- res : coordinates to draw an arrow ;
#	- coordinates of the middle of the arrow (where the "N" will be plotted).
#
# Arguments : #
#-------------#
# scale_bar : result of create_scale_bar() ;
# length : desired length of the arrow ;
# distance : distance between legend rectangles and the bottom of the arrow ;
# dist_units : units of distance "km" (kilometers) (default), "nm" (nautical miles), "mi" (statute miles).
create_orientation_arrow <- function(scale_bar, length, distance = 1, dist_units = "km"){
	lon <- scale_bar$rectangle2[1,1]
	lat <- scale_bar$rectangle2[1,2]
	
	# Bottom point of the arrow
	beg_point <- gcDestination(lon = lon, lat = lat, bearing = 0, dist = distance, dist.units = dist_units, model = "WGS84")
	lon <- beg_point[1,"long"]
	lat <- beg_point[1,"lat"]
	
	# Let us create the endpoint
	on_top <- gcDestination(lon = lon, lat = lat, bearing = 0, dist = length, dist.units = dist_units, model = "WGS84")
	
	left_arrow <- gcDestination(lon = on_top[1,"long"], lat = on_top[1,"lat"], bearing = 225, dist = length/5, dist.units = dist_units, model = "WGS84")
	
	right_arrow <- gcDestination(lon = on_top[1,"long"], lat = on_top[1,"lat"], bearing = 135, dist = length/5, dist.units = dist_units, model = "WGS84")
	
	res <- rbind(
			cbind(x = lon, y = lat, xend = on_top[1,"long"], yend = on_top[1,"lat"]),
			cbind(x = left_arrow[1,"long"], y = left_arrow[1,"lat"], xend = on_top[1,"long"], yend = on_top[1,"lat"]),
			cbind(x = right_arrow[1,"long"], y = right_arrow[1,"lat"], xend = on_top[1,"long"], yend = on_top[1,"lat"]))
	
	res <- as.data.frame(res, stringsAsFactors = FALSE)
	
	# Coordinates from which "N" will be plotted
	coords_n <- cbind(x = lon, y = (lat + on_top[1,"lat"])/2)
	
	return(list(res = res, coords_n = coords_n))
}

The last function enables the user to draw the elements:

#
# Result #
#--------#
# This function enables to draw a scale bar on a ggplot object, and optionally an orientation arrow #
# Arguments : #
#-------------#
# lon, lat : longitude and latitude of the bottom left point of the first rectangle to draw ;
# distance_lon : length of each rectangle ;
# distance_lat : width of each rectangle ;
# distance_legend : distance between rectangles and legend texts ;
# dist_units : units of distance "km" (kilometers) (by default), "nm" (nautical miles), "mi" (statute miles) ;
# rec_fill, rec2_fill : filling colour of the rectangles (default to white, and black, resp.);
# rec_colour, rec2_colour : colour of the rectangles (default to black for both);
# legend_colour : legend colour (default to black);
# legend_size : legend size (default to 3);
# orientation : (boolean) if TRUE (default), adds an orientation arrow to the plot ;
# arrow_length : length of the arrow (default to 500 km) ;
# arrow_distance : distance between the scale bar and the bottom of the arrow (default to 300 km) ;
# arrow_north_size : size of the "N" letter (default to 6).
scale_bar <- function(lon, lat, distance_lon, distance_lat, distance_legend, dist_unit = "km", rec_fill = "white", rec_colour = "black", rec2_fill = "black", rec2_colour = "black", legend_colour = "black", legend_size = 3, orientation = TRUE, arrow_length = 500, arrow_distance = 300, arrow_north_size = 6){
	the_scale_bar <- create_scale_bar(lon = lon, lat = lat, distance_lon = distance_lon, distance_lat = distance_lat, distance_legend = distance_legend, dist_unit = dist_unit)
	# First rectangle
	rectangle1 <- geom_polygon(data = the_scale_bar$rectangle, aes(x = lon, y = lat), fill = rec_fill, colour = rec_colour)
	
	# Second rectangle
	rectangle2 <- geom_polygon(data = the_scale_bar$rectangle2, aes(x = lon, y = lat), fill = rec2_fill, colour = rec2_colour)
	
	# Legend
	scale_bar_legend <- annotate("text", label = paste(the_scale_bar$legend[,"text"], dist_unit, sep=""), x = the_scale_bar$legend[,"long"], y = the_scale_bar$legend[,"lat"], size = legend_size, colour = legend_colour)
	
	res <- list(rectangle1, rectangle2, scale_bar_legend)
	
	if(orientation){# Add an arrow pointing North
		coords_arrow <- create_orientation_arrow(scale_bar = the_scale_bar, length = arrow_length, distance = arrow_distance, dist_unit = dist_unit)
		arrow <- list(geom_segment(data = coords_arrow$res, aes(x = x, y = y, xend = xend, yend = yend)), annotate("text", label = "N", x = coords_arrow$coords_n[1,"x"], y = coords_arrow$coords_n[1,"y"], size = arrow_north_size, colour = "black"))
		res <- c(res, arrow)
	}
	return(res)
}

Now, let's play with this!

Let us draw the US map:

# United States map
usa_map <- map_data("state")
P <- ggplot() + geom_polygon(data = usa_map, aes(x = long, y = lat, group = group)) + coord_map()

If we want to add the scale bar only:

P + scale_bar(lon = -130, lat = 26, 
              distance_lon = 500, distance_lat = 100, distance_legend = 200, 
              dist_unit = "km", orientation = FALSE)

US map using ggplot2 and scaleBar, without North arrow


Or if we want the scale bar and the North arrow:

P <- P + scale_bar(lon = -130, lat = 26,
                   distance_lon = 500, distance_lat = 100,
                   distance_legend = 200, dist_unit = "km")

To make it look more like a map:

P + theme(panel.grid.minor = element_line(colour = NA), panel.grid.minor = element_line(colour = NA),
          panel.background = element_rect(fill = NA, colour = NA), axis.text.x = element_blank(),
          axis.text.y = element_blank(), axis.ticks.x = element_blank(),
          axis.ticks.y = element_blank(), axis.title = element_blank(),
          rect = element_blank(),
          plot.margin = unit(0 * c(-1.5, -1.5, -1.5, -1.5), "lines"))

US map using ggplot2 and scaleBar

Another example, with France:

france_map <- map_data("france")
P_fr <- ggplot() + geom_polygon(data = france_map, aes(x = long, y = lat, group = group)) + coord_map()

# Let us add the scale bar
P_fr <- P_fr + scale_bar(lon = -5, lat = 42.5, 
                         distance_lon = 100, distance_lat = 20, 
                         distance_legend = 40, dist_unit = "km", 
                         arrow_length = 100, arrow_distance = 60, arrow_north_size = 6)

# Modifying the theme a bit
P_fr + theme(panel.grid.minor = element_line(colour = NA), panel.grid.minor = element_line(colour = NA),
             panel.background = element_rect(fill = NA, colour = NA), axis.text.x = element_blank(),
             axis.text.y = element_blank(), axis.ticks.x = element_blank(),
             axis.ticks.y = element_blank(), axis.title = element_blank(),
             rect = element_blank(),
             plot.margin = unit(0 * c(-1.5, -1.5, -1.5, -1.5), "lines"))

France map using ggplot2 and scaleBar

18 thoughts on “Scale bar and North arrow on a ggplot2 map using R

  1. Hello,

    I just wanted to let you know that this was incredibly helpful – I’ve struggled with making good scalebars for a while now, and this is exactly what I needed! Merci beaucoup!

    Cheers,
    Saki

  2. This looks fantastic, but when I try to plot it I get the message:
    Error in eval(expr, envir, enclos) : object 'group' not found

    Do you know why this happens and how it can be resolved?

    1. Hi,
      I guess this is because there is no « group » column in your data frame.
      P < - ggplot() + geom_polygon(data = usa.map, aes(x = long, y = lat, group = group))
      The argument "group" is here because there are multiple polygons for each state, and since I want to join the points, I need to be careful with these polygons.
      If you are using another map, from maps package, or a shapefile, you might find the function "fortify" from ggplot2 package very useful.

  3. Thanks for your response Ewen. I was using data that had been manipulated using the fortify function, sorry for not making that clear.
    I seem to have resolved my issue. Previously, I had not explicitly stated data = spatial.df when building the layers in ggplot, i.e. I did geom_polygon(spatial.df, aes(...))
    Doing so seems to resolve my problem.


    map <- ggplot() +
    geom_polygon(data = coast.df, aes(long, lat, group = group)) +
    geom_path(data = coast.df, aes(long, lat,group=group), colour = "white") +
    coord_map() #library(mapproj)
    map

  4. Hello!
    First of all, thanks a lot for your code, I’ve been struggling with this problem for a while and I think this might save me! 🙂 However, when I try to add the scale bar, I get this warning:

    Error in data.frame(cbind(legend, text = c(0, distanceLon, distanceLon * :
    arguments imply differing number of rows: 0, 1
    In addition: Warning message:
    In cbind(legend, text = c(0, distanceLon, distanceLon * 2)) :
    number of rows of result is not a multiple of vector length (arg 2)

    Do you know how to solve this or what I might be doing wrong?

  5. I think this is what I need, only, I’m working with GaussKruger data, what should I do (where should I change things to adjust this to my needs?

    Thankyou!

  6. Hi there, I have been looking for a way to do this for a long time! Many thanks. I am very new to R and was wondering, in order to create a scale bar on a map I am drawing, the first three sections where we are creating the scale bar… do I just paste in the code you have given into my console and then change the ‘function’ line (i.e., lon = xx, lon = xx, distancelon = xx… etc. etc.) but leave the rest as you have written? Will it automatically calculate the length from my map?

    Many thanks

    1. Hi! You can copy/paste the functions in your console or your script file and then execute the code. From this moment, you will be able to use these functions. You don’t need to change them. What you need to do, is to change the values of the arguments when you call the function.

      First create your ggplot map, and add the scale bar as an extra layer, using the « + » sign.
      your_ggplot_object + scaleBar(lon = -130, lat = 26, distanceLon = 500, distanceLat = 100, distanceLegend = 200, dist.unit = "km")

      On the example above, I call the « scaleBar » function, and I specify some values for the arguments. For instance, lon = -130, lat = 26 means I want the bottom left point of the rectangle to be at (-130,26). You need to define the other values of each argument according to what you want (there is a description of each argument in the header of the function).

  7. I have tried our code but i get an error Error: 'x' and 'units' must have length > 0

    here is my code

    m <- get_map(location =loc ,color = "color",source = "google",maptype = c("terrain"),zoom = Zoom)

    p<-ggmap(m)#,ylim=YL,xlim=XL))
    #polygon rivers
    p<-p+ geom_path(aes(x=long,y=lat,group=group),fill="grey",size=.5,color="cadetblue",data=riv)
    #Waterways
    p<-p+geom_segment(size=2,data=boxes,aes(y = boxes$LATin, x = boxes$LONin, yend = boxes$LATph, xend = boxes$LONph),colour="black",alpha=0.5)
    #Intakes
    boxes$shape=factor("intake")
    p<-p+geom_point(data=boxes, aes(x=LONin, y=LATin,shape=25), fill="lightblue", size=4)+scale_shape_identity()
    #Powerhouses
    p<-p+ geom_point(data=boxes, aes(x=LONph, y=LATph,shape=22), fill="red", size=3)

    #Labels
    p<-p+ geom_text(size=2.8,fontface="bold",hjust=-0.2,data = boxes, aes(x=LONph,y=LATph,label = paste(mw, "MW",sep="")))
    p<-p+ geom_text(size=2.8,fontface="bold",colour="blue",hjust=-1,angle=90,data = boxes, aes(x=LONin,y=LATin,label = paste("[",IDn,"]n",sep="")))
    print(p)

    #scale bar
    source('~/Documents/DATA/Dev/R/HydroSearch/RAW/LakeGeorge1k/scale.bar.r')
    (q <- p + scaleBar(lon = 29.5, lat = -0.6, distanceLon = 20, distanceLat = 10, distanceLegend = 20, dist.unit = "km")

    1. Hi,

      It’s hard to be sure with your code, since it is not reproducible. But my guess is that you are trying to draw the scale bar outside the range of the Google map coordinates.
      Try changing the lon and lat parameters, and/or the distanceLon and distanceLat. 🙂

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Time limit is exhausted. Please reload CAPTCHA.