Adding a scalebar to an existing ggplot

1,079 views
Skip to first unread message

Paul Hiemstra

unread,
Nov 25, 2010, 3:19:16 PM11/25/10
to ggplot2, h.wi...@gmail.com
Hi ggplot2,

I'm trying to add a scalebar to an existing ggplot. I already have the
appropriate polygonGrob, I only can;t get it to plot correctly. An
example:

library(sp) # may need to install this one
library(ggplot2)

data(meuse.grid)

grid.newpage()
ggplot(aes(x = x, y = y, fill = dist), data = meuse.grid) +
geom_tile()
grid.add("panel", polygonGrob(), grep = TRUE)

I now have plotted a simple polygon over the existing plot. The
challenge now is to plot a polygon that has the coordinates system of
the panel and not in npc coordinates:

p1 = polygonGrob(x = c(181e3, 181e3, 181.5e3, 181.5e3),
y = c(330e3, 331e3, 331e3, 330e3))
grid.newpage()
ggplot(aes(x = x, y = y, fill = dist), data = meuse.grid) +
geom_tile()
grid.add("panel", p1, grep = TRUE)

The problem is that the coordinates in p1 are not in npc. When I try
to convert:

p2 = polygonGrob(x = convertUnit(unit(c(181e3, 181e3, 181.5e3,
181.5e3), "native"), "npc"),
y = convertUnit(unit(c(330e3, 331e3, 331e3, 330e3),
"native"), "npc"))
grid.newpage()
ggplot(aes(x = x, y = y, fill = dist), data = meuse.grid) +
geom_tile()
grid.add("panel", p1, grep = TRUE)

So the conversion fails. This might be because the right viewport is
not used. But I'm sort of stuck here. Does anybody have any
suggestions how to proceed from hereon?

cheers,
Paul
Royal Netherlands Meteorological Institute (KNMI)

Mark Connolly

unread,
Nov 27, 2010, 2:06:45 PM11/27/10
to Paul Hiemstra, ggplot2, h.wi...@gmail.com
This may not be of any help, but the results of the convertUnit look a
little funny.

> convertUnit(unit(c(181e3, 181e3, 181.5e3,
+ 181.5e3), "native"), "npc")
[1] 278.033794162826npc 278.033794162826npc 278.801843317972npc
[4] 278.801843317972npc
>

> convertUnit(unit(c(330e3, 331e3, 331e3, 330e3),
+ "native"), "npc")
[1] 506.912442396313npc 508.448540706605npc 508.448540706605npc
[4] 506.912442396313npc
>


Should you be looking for something in the interval 0 to 1?

hadley wickham

unread,
Nov 27, 2010, 6:13:50 PM11/27/10
to Paul Hiemstra, ggplot2
> The problem is that the coordinates in p1 are not in npc. When I try
> to convert:
>
> p2 = polygonGrob(x = convertUnit(unit(c(181e3, 181e3, 181.5e3,
> 181.5e3), "native"), "npc"),
>            y = convertUnit(unit(c(330e3, 331e3, 331e3, 330e3),
> "native"), "npc"))
> grid.newpage()
> ggplot(aes(x  = x, y = y, fill = dist), data = meuse.grid) +
> geom_tile()
> grid.add("panel", p1, grep = TRUE)
>
> So the conversion fails. This might be because the right viewport is
> not used. But I'm sort of stuck here. Does anybody have any
> suggestions how to proceed from hereon?

Your best bet is to use an existing geom to draw the scale bar -
unfortunately ggplot2 doesn't use native scales because it's not
possible to support arbitrary coordinate systems with them.

Hadley

--
Assistant Professor / Dobelman Family Junior Chair
Department of Statistics / Rice University
http://had.co.nz/

Paul Hiemstra

unread,
Nov 28, 2010, 11:48:48 AM11/28/10
to hadley wickham, ggplot2
Dear Mark and Hadley,

@Mark, this was exactly the problem I was facing.

@Hadley, I tried to use an existing geom, geom_polygon. The problem is that if I want to use scale_fill_* to change the color of the polygon (alternating black and white) that could conflict with other geoms (e.g. geom_tile) that already defined the fill. I could add a separate geom_polygon for each subpolygon of the scalebar (e.g. 5), but do you have a better suggestion to circumvent the problem of defining scale_fill_* twice?

Paul

2010/11/28 hadley wickham <h.wi...@gmail.com>

baptiste auguie

unread,
Nov 28, 2010, 2:29:29 PM11/28/10
to Paul Hiemstra, ggplot2
Hi,

If the scalebar is black and white, you should only need two calls to
geom_rect (or rather geom_path i guess). Something like this perhaps,

library(ggplot2)

p = qplot(1:10,1:10)
## p

makeScaleBar <- function(x=8,y=2,width=2,N=4, ...){

dlong = data.frame(x=x - width/2,
xend = x + width/2,
y=y, yend=y)

dshort = data.frame(x=x - width/2 + seq(0, N-1,by=2)*width/N,
xend = x - width/2 + (1 + seq(0, N-1,by=2))*width/N,
y=y, yend=y)

list(geom_segment(data=dlong, aes(x=x,y=y,xend=xend,yend=yend),
colour="black", ...),
geom_segment(data=dshort, aes(x=x,y=y,xend=xend,yend=yend),
colour="white", ...))

}

p + makeScaleBar(N=5, size=5)

HTH,

baptiste

> --
> You received this message because you are subscribed to the ggplot2 mailing
> list.
> Please provide a reproducible example: http://gist.github.com/270442
>
> To post: email ggp...@googlegroups.com
> To unsubscribe: email ggplot2+u...@googlegroups.com
> More options: http://groups.google.com/group/ggplot2
>

Paul Hiemstra

unread,
Nov 28, 2010, 3:04:23 PM11/28/10
to baptiste auguie, ggplot2
Dear Baptiste,

Thanks for the input. There is one problem however with your example,
sticking geom's in a list and trying to add them to an existing ggplot does not work.
The function is probably going to look more like (in pseudo code):


addScalebar = function(ggplot_obj, data_ggplot_was_based on) {
    extract coordinates for scale bar from data_ggplot_was_based on
    create list of geom's (path and text)
    for(geom in geoms) ggplot_obj = ggplot_obj + geom
    return(ggplot_obj)
}

bla = ggplot(aes(), bla_data)
bla_withscalebar = addScalebar(bla, bla_data)

When I'm done with the function, I'll report back to the list. Is there some kind
of contributed code section for ggplot?

Paul

2010/11/28 baptiste auguie <bapt...@googlemail.com>

baptiste auguie

unread,
Nov 28, 2010, 3:11:51 PM11/28/10
to Paul Hiemstra, ggplot2
Hi,

I'm sorry but I don't understand what you mean. Would you have an
example of what you're trying to achieve?


On Sun, Nov 28, 2010 at 9:04 PM, Paul Hiemstra <p.h.hi...@gmail.com> wrote:
> Dear Baptiste,
>
> Thanks for the input. There is one problem however with your example,
> sticking geom's in a list and trying to add them to an existing ggplot does
> not work.

Works for me, admittedly on a dummy example.

> The function is probably going to look more like (in pseudo code):
>
>
> addScalebar = function(ggplot_obj, data_ggplot_was_based on) {
>     extract coordinates for scale bar from data_ggplot_was_based on
>     create list of geom's (path and text)
>     for(geom in geoms) ggplot_obj = ggplot_obj + geom
>     return(ggplot_obj)
> }
>
> bla = ggplot(aes(), bla_data)
> bla_withscalebar = addScalebar(bla, bla_data)
>
> When I'm done with the function, I'll report back to the list. Is there some
> kind
> of contributed code section for ggplot?
>


Either the wiki or the ggExtra package could be good places for
contributed code, I think.

Paul Hiemstra

unread,
Nov 28, 2010, 3:19:34 PM11/28/10
to baptiste auguie, ggplot2
Hmmm, I thought it did not work. I'll look into it and report back.

Thanks again for the input,

Mark Connolly

unread,
Nov 28, 2010, 4:18:13 PM11/28/10
to baptiste auguie, Paul Hiemstra, ggplot2
Baptiste,

I think the idea is to create a segmented scalebar that is in proportion
to the scaled real-world coordinates of the map and in arbitrary
scalebar units (ideally). Think in terms of a map of Italy with a scale
showing the measure of a kilometer or 100km.

The scalebar and the map should not interfere with each other (the
scalebar colors should not show up on a legend, for example). The
scalebar should also rescale if the data are zoomed (changed
presentation or data limits) and ideally change position and number of
scalebar segments.

The geom_polygon seems like it is a likely existing geom to start with.
It would have to be constructed according to the coordinates of the data
and the limits of the presentation. The polygon segments would be
adjusted according to the units of measure. The scalebar segments would
alternate color. The scalebar segments would not show up in the
legend. The scalebar would be anchored in space according to a
positioning specification, with a possible default given the extents of
the data on the map.

It seems that multiple viewports might separate the scalebar from the
map and get rid of conflicts. This has two problems: 1. not everyone
wants to deal with a plot with multiple viewports. 2. It may not be
possible to scale the scalebar viewport so that it has the scalebar at
the right proportions.

There seems to be a significant impedance mismatch with the notion of a
scalebar mixed with a gplot map. Maybe someone has some ideas, though.
It would be a worthwhile thing to accomplish.

I wonder if a graticule might be a more feasible option?

Mark

Hadley Wickham

unread,
Nov 28, 2010, 5:50:16 PM11/28/10
to Mark Connolly, baptiste auguie, Paul Hiemstra, ggplot2
> I think the idea is to create a segmented scalebar that is in proportion to
> the scaled real-world coordinates of the map and in arbitrary scalebar units
> (ideally).  Think in terms of a map of Italy with a scale showing the
> measure of a kilometer or 100km.

Could you explain in more detail how the scale bar is created? It's
my understanding that the length of (e.g.) 1km is not constant across
most maps.

> The scalebar and the map should not interfere with each other (the scalebar
> colors should not show up on a legend, for example). The scalebar should
> also rescale if the data are zoomed (changed presentation or data limits)

That should be trivial with geom_rect or geom_polygon.

> and ideally change position and number of scalebar segments.

That's a bit harder.

Mark Connolly

unread,
Nov 28, 2010, 6:26:20 PM11/28/10
to Hadley Wickham, baptiste auguie, Paul Hiemstra, ggplot2
I think Paul probably has a much better handle on the consistency of the
scale. I think that it is on the map-maker to select an appropriate
coordinate projection. With a distance scale, a projection that
preserves distance (as much as possible). Of course, I wouldn't use a
flat map for making military decisions.

Paul Hiemstra

unread,
Nov 29, 2010, 3:54:04 AM11/29/10
to Mark Connolly, Hadley Wickham, baptiste auguie, ggplot2
Hi all,

The idea would be to estimate the length of the scalebar from the data. A first guess would be 20% (e.g. 121.34 km) and this number would be rounded to a 'nice' number, e.g. 120 km. Drawing is done with geom_polygon. I'm already quite far with this, I'll post an example to the list when it's done.

Paul

2010/11/29 Mark Connolly <mark_c...@acm.org>

Paul Hiemstra

unread,
Nov 29, 2010, 7:49:48 AM11/29/10
to Mark Connolly, Hadley Wickham, baptiste auguie, ggplot2
Hi,

A preliminary version of the addScaleBar() function is given below, with an example using a dataset from the sp-package. Any comments are appreciated.

Paul

makeNiceNumber = function(num, num.pretty = 1) {
  # Rounding provided by code from Maarten Plieger
  return((round(num/10^(round(log10(num))-1))*(10^(round(log10(num))-1))))
}

createBoxPolygon = function(llcorner, width, height) {
  relativeCoords = data.frame(c(0, 0, width, width, 0), c(0, height, height, 0, 0))
  names(relativeCoords) = names(llcorner)
  return(t(apply(relativeCoords, 1, function(x) llcorner + x)))
}

addScaleBar = function(ggplot_obj, spatial_obj, attribute, addParams = list()) {
  addParamsDefaults = list(noBins = 5, xname = "x", yname = "y", unit = "m", placement = "bottomright",
                           sbLengthPct = 0.3, sbHeightvsWidth = 1/14)
  addParams = modifyList(addParamsDefaults, addParams)

  range_x = max(spatial_obj[[addParams[["xname"]]]]) - min(spatial_obj[[addParams[["xname"]]]])
  range_y = max(spatial_obj[[addParams[["yname"]]]]) - min(spatial_obj[[addParams[["yname"]]]])
  lengthScalebar = addParams[["sbLengthPct"]] * range_x
  widthBin = makeNiceNumber(lengthScalebar / addParams[["noBins"]])
  heightBin = lengthScalebar * addParams[["sbHeightvsWidth"]]
  lowerLeftCornerScaleBar = c(x = max(spatial_obj[[addParams[["xname"]]]]) - (widthBin * addParams[["noBins"]]),
                              y = min(spatial_obj[[addParams[["yname"]]]]))
 
  scaleBarPolygon = do.call("rbind", lapply(0:(addParams[["noBins"]] - 1), function(n) {
    dum = data.frame(createBoxPolygon(lowerLeftCornerScaleBar + c((n * widthBin), 0), widthBin, heightBin))
    if(!(n + 1) %% 2 == 0) dum$cat = "odd" else dum$cat = "even"
    return(dum)
  }))
  scaleBarPolygon[[attribute]] = min(spatial_obj[[attribute]])
  textScaleBar = data.frame(x = lowerLeftCornerScaleBar[[addParams[["xname"]]]] + (c(0:(addParams[["noBins"]])) * widthBin),
                            y = lowerLeftCornerScaleBar[[addParams[["yname"]]]],
                            label = as.character(0:(addParams[["noBins"]]) * widthBin))
  textScaleBar[[attribute]] = min(spatial_obj[[attribute]])

  return(ggplot_obj +
    geom_polygon(data = subset(scaleBarPolygon, cat == "odd"), fill = "black", color = "black", legend = FALSE) +
    geom_polygon(data = subset(scaleBarPolygon, cat == "even"), fill = "white", color = "black", legend = FALSE) +
    geom_text(aes(label = label), color = "black", size = 6, data = textScaleBar, hjust = 0.5, vjust = 1.2, legend = FALSE))
}

data(meuse)
data(meuse.grid)
ggobj = ggplot(aes(x = x, y = y, color = zinc), data = meuse) + geom_point()
addScaleBar(ggobj, meuse, "zinc", addParams = list(noBins = 5))


2010/11/29 Paul Hiemstra <p.h.hi...@gmail.com>

Mark Connolly

unread,
Nov 29, 2010, 8:38:55 AM11/29/10
to Paul Hiemstra, Hadley Wickham, baptiste auguie, ggplot2
Looking pretty good. 

An issue: often as not, attribute is liable to be a factor and scaleBarPolygon[[attribute]] = min(spatial_obj[[attribute]]) will fail.


A bug (test setting xname and yname with other than x and y as the coordinate names):


lowerLeftCornerScaleBar =
              c(x = max(spatial_obj[[addParams[["xname"]]]]) - (widthBin * addParams[["noBins"]]),
                 y = min(spatial_obj[[addParams[["yname"]]]]))


...

textScaleBar = data.frame(

                           x = lowerLeftCornerScaleBar[[addParams[["xname"]]]] +
                                  (c(0:(addParams[["noBins"]])) * widthBin),
                           y = lowerLeftCornerScaleBar[[addParams[["yname"]]]],
                           label = as.character(0:(addParams[["noBins"]]) * widthBin))


The attribute names of lowerLeftCornerScaleBar are always x and y, not addParams[["xname"]] and addParams[["yname"]]

Paul Hiemstra

unread,
Nov 29, 2010, 9:01:46 AM11/29/10
to Mark Connolly, Hadley Wickham, baptiste auguie, ggplot2
Sorry, I had already thought of that but not yet implemented it. I should add code that changes the the names of lowerleft cornerscalebar and text scale bar.
Reply all
Reply to author
Forward
0 new messages