Lets say I want to have a plot and lose the box in R. But still I would need a scale bar so one can understand the scaling. I didn't find a solution.
plot(1,1, type="n", xlim=c(0,5), ylim=c(0,5))
When I use the scalebar function from the raster package, the scaling is not right:
require(raster)
scalebar(1)
The added scalebar is too short to represent 1 in the x axis.
I tried to find something else, but most scalebar functions are related to maps.
edit:
So what I want is something like this:
plot(1,1, type="n", xlim=c(0,5), ylim=c(0,5)
, yaxt="n",
xaxt="n", frame.plot=F, ann=F
# adding a blank plot without the axes
)
#adding some simple function
x=c(1:5)
y=x*x
lines(x=x, y=y)
#defining where the scale bar should appear
lines(x=c(4,5), y=c(5,5))
#placing the text right under the line
text(x=4.5, y=5, pos=1, label="1 km")
Is there an easier way to do something like this?
There might be a function that does what you want, but you can also create your own function that will hopefully serve well enough. See below for one possibility. You can of course adjust the function settings to get the positioning you want. In particular, I've included yadj as an argument to the function, with a default value of 1.5. You can change this if the scalebar label isn't positioned properly under the scale line.
If the x-axis spans a larger range than the values used below, you'll want to adjust the x-coordinates of the scale line so that it spans 10, 100, etc. x-units, as the case may be. If you want to get fancy, you can have the function itself determine how many x-units to span, based on the x-range of the plot and then use the magnitude of that span in the units label.
# Function to add a scalebar to a base-graphics plot
myScalebar = function(units_label, yadj=1.5) {
# Get plot coordinates
pc = par("usr")
# Position scale line between last two major x-axis tick marks
# and 1/10th of the total y-range above the lower y-axis coordinate
lines(c(floor(pc[2]-1),floor(pc[2])),
rep(pc[3] + 0.1*(pc[4] - pc[3]), 2))
# Place the units label at the midpoint of and just below the scale line
text(x=mean(c(floor(pc[2]-1), floor(pc[2]))),
y=pc[3] + 0.1*(pc[4] - pc[3]),
label=units_label, adj=c(0.5, yadj))
}
# Now redo your plot
# Start with blank plot
plot(1,1, type="n", xlim=c(0,5), ylim=c(0,5),
yaxt="n", xaxt="n", frame.plot=F, ann=F)
# Add a simple function
x=c(1:5)
y=x*x
lines(x=x, y=y)
# Add scalebar
myScalebar("1 km")
I usually use this sort of function that allows for lots of flexibility across plots. I have expanded the variables names to help with debugging. Please note: this is designed to work with raster converted to utms only (don't use geographic projections).
ScaleBar <- function(reference_raster_utm, round_to_nearest_km, width_percent, y_percent_from_bottom, x_percent_from_left, y_text_percent_from_bottom, ...) {
# Round by max to nearest... e.g. 5 km
mround <- function(x,base){
base*round(x/base)
}
# scale bar size adjustment to avoid decimals
scale_size <- ((xmax(reference_raster_utm)-xmin(reference_raster_utm))*width_percent)/1000
scale_size_adj <- mround(scale_size, round_to_nearest_km)
scale_size_adj_plot <- (scale_size_adj*1000)/2
# Horizontal percent position (x) for scale bar
x_position <- ((xmax(reference_raster_utm)-xmin(reference_raster_utm))*x_percent_from_left)+xmin(reference_raster_utm)
# Vertical percent position y for scale bar
y_position <- ((ymax(reference_raster_utm)-ymin(reference_raster_utm))*y_percent_from_bottom)+ymin(reference_raster_utm)
y_position_text <- ((ymax(reference_raster_utm)-ymin(reference_raster_utm))*y_text_percent_from_bottom)+ymin(reference_raster_utm)
# Draw line on plot
library(sp)
x_ends <- c((x_position-scale_size_adj_plot), (x_position+scale_size_adj_plot))
y_ends <- c((y_position), (y_position))
scale_bar_line <- SpatialLines(list(Lines(Line(cbind(x_ends, y_ends)), ID="length")))
projection(scale_bar_line) <- projection(reference_raster_utm)
plot(scale_bar_line, add=TRUE, ...)
text(x_position, y_position_text, paste0(scale_size_adj, "km"))
}
Arguments:
reference_raster_utm: One of your personal raster files to source extent/projection from.
round_to_nearest_km: round to nearest kilometre e.g. max out on 2km, 5km ect.
width_percent: percent of plot width that the scale bar should cover (e.g. big 50% small 10%).
y_percent_from_bottom: vertical position from bottom. 0% at bottom, 100% at top, 50% in the middle.
x_percent_from_left: horizontal position from left. 0% at left, 100% at right, 50% in the middle.
y_text_percent_from_bottom: same as y_percent_from_bottom but for text.
Example:
plot(my_raster)
ScaleBar(reference_raster_utm=my_raster, round_to_nearest_km=5, width_percent=0.25, y_percent_from_bottom=0.10, x_percent_from_left=0.50, y_text_percent_from_bottom=0.07, lwd=2)
Related
I am trying to arrange 3 plots together. All 3 plots have the same y axis scale, but the third plot has a longer x axis than the other two. I would like to arrange the first two plots side by side in the first row and then place the third plot on the second row aligned to the right. Ideally I would like the third plot's x values to align with plot 2 for the full extent of plot 2 and then continue on below plot one. I have seen some other postings about using the layout function to reach this general configuration (Arrange plots in a layout which cannot be achieved by 'par(mfrow ='), but I haven't found anything on fine tuning the plots so that the scales match. Below is a crappy picture that should be able to get the general idea across.
I thought you could do this by using par("plt"), which returns the coordinates of the plot region as a fraction of the total figure region, to programmatically calculate how much horizontal space to allocate to the bottom plot. But even when using this method, manual adjustments are necessary. Here's what I've got for now.
First, set the plot margins to be a bit thinner than the default. Also, las=1 rotates the y-axis labels to be horizontal, and xaxs="i" (default is "r") sets automatic x-axis padding to zero. Instead, we'll set the amount of padding we want when we create the plots.
par(mar=c(3,3,0.5,0.5), las=1, xaxs="i")
Some fake data:
dat1=data.frame(x=seq(-5000,-2500,length=100), y=seq(-0.2,0.6,length=100))
dat2=data.frame(x=seq(-6000,-2500,length=100), y=seq(-0.2,0.6,length=100))
Create a layout matrix:
# Coordinates of plot region as a fraction of the total figure region
# Order c(x1, x2, y1, y2)
pdim = par("plt")
# Constant padding value for left and right ends of x-axis
pad = 0.04*diff(range(dat1$x))
# If total width of the two top plots is 2 units, then the width of the
# bottom right plot is:
p3w = diff(pdim[1:2]) * (diff(range(dat2$x)) + 2*pad)/(diff(range(dat1$x)) + 2*pad) +
2*(1-pdim[2]) + pdim[1]
# Create a layout matrix with 200 "slots"
n=200
# Adjustable parameter for fine tuning to get top and bottom plot lined up
nudge=2
# Number of slots needed for the bottom right plot
l = round(p3w/2 * n) - nudge
# Create layout matrix
layout(matrix(c(rep(1:2, each=0.5*n), rep(4:3,c(n - l, l))), nrow=2, byrow=TRUE))
Now create the graphs: The two calls to abline are just to show us whether the graphs' x-axes line up. If not, we'll change the nudge parameter and run the code again. Once we've got the layout we want, we can run all the code one final time without the calls to abline.
# Plot first two graphs
with(dat1, plot(x,y, xlim=range(dat1$x) + c(-pad,pad)))
with(dat1, plot(x,y, xlim=range(dat1$x) + c(-pad,pad)))
abline(v=-5000, xpd=TRUE, col="red")
# Lower right plot
plot(dat2, xaxt="n", xlim=range(dat2$x) + c(-pad,pad))
abline(v=-5000, xpd=TRUE, col="blue")
axis(1, at=seq(-6000,-2500,500))
Here's what we get with nudge=2. Note the plots are lined up, but this is also affected by the pixel size of the saved plot (for png files), and I adjusted the size to get the upper and lower plots exactly lined up.
I would have thought that casting all the quantities in ratios that are relative to the plot area (by using par("plt")) would have both ensured that the upper and lower plots lined up and that they would stay lined up regardless of the number of pixels in the final image. But I must be missing something about how base graphics work or perhaps I've messed up a calculation (or both). In any case, I hope this helps you get the plot layout you wanted.
Here is shown how to label histogram bars with data values or percents using labels = TRUE. Is it also possible to rotate those labels? My goal is to rotate them to 90 degrees because now the labels over bars overrides each other and it is unreadable.
PS: please note that my goal is not to rotate y-axis labels as it is shown e.g. here
Using mtcars, here's one brute-force solution (though it isn't very brutish):
h <- hist(mtcars$mpg)
maxh <- max(h$counts)
strh <- strheight('W')
strw <- strwidth(max(h$counts))
hist(mtcars$mpg, ylim=c(0, maxh + strh + strw))
text(h$mids, strh + h$counts, labels=h$counts, adj=c(0, 0.5), srt=90)
The srt=90 is the key here, rotating 90 degrees counter-clockwise (anti-clockwise?).
maxh, strh, and strw are used (1) to determine how much to extend the y-axis so that the text is not clipped to the visible figure, and (2) to provide a small pad between the bar and the start of the rotated text. (The first reason could be mitigated by xpd=TRUE instead, but it might impinge on the main title, and will be a factor if you set the top margin to 0.)
Note: if using density instead of frequency, you should use h$density instead of h$counts.
Edit: changed adj, I always forget the x/y axes on it stay relative to the text regardless of rotation.
Edit #2: changing the first call to hist so the string height/width are calculate-able. Unfortunately, plotting twice is required in order to know the actual height/width.
I'm trying to change the axes in my NMDS plot to zoom into where my sites are plotted. I assume that the space chosen in a product of the species points which I do not have plotted. I have tried adding xlim to my code to no avail and was wondering if I have it in the wrong place or if another action is needed. Below is a copy of my code.
#NMDS on pooled abundance with NA's omitted
NMDS_HPA<-metaMDS(HP_Abundance_omit[,-1],k=2, trymax=1000)
plot(NMDS_HPA, type="n", display="sites", xlim=c(-1.5,1.5))
with(descriptors, levels(T))
colorvec<-c("seagreen4", "tan4", "mediumblue")
plot(NMDS_HPA, type="n", xlim=c(-1.5,1.5))
title(main="NMDS using Abundance with Bray-Curtis", sub="Habitats Pooled")
ordihull(NMDS_HPA, groups=treat, draw="polygon", col="grey90", label=F)
with(descriptors, points(NMDS_HPA, display="sites", col=colorvec[T], pch=21, bg=colorvec[T]))
with(descriptors, legend("topright", legend=levels(T), bty="n", col=colorvec, pch=21, pt.bg=colorvec))
Thanks
If you don't set the ylim too, then vegan has no choice but to show more (or less) of the x-axis than you want because the scaling of the axis must be retained; a unit change along one axis must match the same unit change along the other. Otherwise, how would you know how to represent Euclidean distances (easily) on the figure? As those Euclidean distances are supposed to reflect the rank ordering of the original dissimilarities, maintaining the aspect ratio or relative scaling of the axes to one another is important.
You can see this in action just by using your mouse to rescale the size of the device window on screen. R replots the figure using different axis limits all the time in order to maintain an aspect ratio of 1.
Consider this reproducible example:
library("vegan")
data(dune)
set.seed(56)
sol <- metaMDS(dune)
Choosing a section in both the x and y axes works as expected
## zoom in on the section (-0.5,0.5)(-0.5,0.5)
plot(sol, xlim = c(-0.5, 0.5), ylim = c(-0.5,0.5))
If you want to retain the full y-axis but only show say the middle 50% of the x axis then you have to plot on a device whose width is ~ 50% that of the height (approximately because R's default is to use different sized margins on the top/bottom left/right margins.)
png("~/mds-zoom2.png", height = 700, width = 350, res = 100, pointsize = 16)
plot(sol, xlim = c(-0.5, 0.5))
dev.off()
which produces
This is almost right. You could solve the problem exactly by setting the margins equal around the plot using par(mar = rep(4, 4) + 0.1) and then work out the ratio of the range of the scores on the x and y axes (get the scores(sol) and compute the range() on both columns then compute the ratio of the two ranges), then use that to give you the desired height of the plot for the width you want to state.
If you just plotted the points rather than the NMDS object than xlim works just fine
plot(NMDS_HPA$points, xlim=c(-1.5,1.5))
I'm trying to figure out a way to calculate the height of a legend for a plot prior to setting the margins of the plot. I intend to place the legend below the plot below the x-axis labels and title.
As it is part of a function which plots a range of things the legend can grow and shrink in size to cater for 2 items, up to 15 or more, so I need to figure out how I can do this dynamically rather that hard-coding. So, in the end I need to dynamically set the margin and some other bits and pieces.
The key challenge is to figure out the height of the legend to feed into par(mar) prior to drawing the plot, but after dissecting the base codes for legend however, it seems impossible to get a solid estimate of the height value unless the plot is actually drawn (chicken and egg anyone?)
Here's what I've tried already:
get a height using the legend$rect$h output from the base legend function (which seems to give a height value which is incorrect unless the plot is actually drawn)
calculate the number of rows in the legend (easy) and multiply this by the line height (in order to do this, seems you'd need to translate into inches (the base legend code uses yinch and I've also tried grconvertY but neither of those work unless a plot has been drawn).
Another challenge is to work out the correct y value for placement of the legend - I figure that once I've solved the first challenge, the second will be easy.
EDIT:
After a day of sweating over how this is (not) working. I have a couple of insights and a couple of questions. For the sake of clarity, this is what my function essentially does:
step 1) set the margins
step 2) create the barplot on the left axis
step 3) re-set the usr coordinates - this is necessary to ensure alignment of the right axis otherwise it plots against the x-axis scale. Not good when they are markedly different.
step 4) create the right axis
step 5) create a series of line charts on the right axis
step 6) do some labelling of the two axes and the x-axis
step 7) add in the legend
Here are the questions
Q1) What units are things reported in? I'm interested in margin lines and coordinates (user-coordinates), inches is self explanatory. - I can do some conversions using grconvertY() but I'm not sure what I'm looking at and what I should be converting to - the documentation isn't so great.
Q2) I need to set the margin in step 1 so that there is enough room at the bottom of the chart for the legend. I think I'm getting that right, however I need to set the legend after the right axis and line charts are set, which means that the user coordinates (and the pixel value of an inch, has changed. Because of Q1 above I'm not sure how to translate one system to the other. Any ideas in this regard would be appreciated.
After another day of sweating over this here's what solved it mostly for me.
I pulled apart the code for the core legend function and compiled this:
#calculate legend buffer
cin <- par("cin")
Cex <- par("cex")
yc <- Cex * cin[2L] #cin(inches) * maginfication
yextra <- 0
ymax <- yc * max(1, strheight("Example", units = "inches", cex = Cex)/yc)
ychar <- yextra + ymax #coordinates
legendHeight <- (legendLines * ychar) + yc # in
Which is essentially mimicking the way the core function calculates legend height but returns the height in inches rather than in user coordinates. legendLines is the number of lines in the legend.
After that, it's a doddle to work out how to place the legend, and to set the lower margin correctly. I'm using:
#calculate inches per margin line
inchesPerMarLine<-par("mai")[1]/par("mar")[1]
To calculate the number of inches per margin line, and the following to set the buffers (for the axis labels and title, and the bottom of the chart), and the margin of the plot.
#set buffers
bottomBuffer = 1
buffer=2
#calculate legend buffer
legBuffer <- legendHeight/inchesPerMarLine
#start the new plot
plot.new()
# set margin
bottomMargin <- buffer + legBuffer + bottomBuffer
par(mar=c(bottomMargin,8,3,5))
The plot is made
barplot(data, width=1, col=barCol, names.arg=names, ylab="", las=1 ,axes=F, ylim=c(0,maxL), axis.lty=1)
And then the legend is placed. I've used a different method to extract the legend width which does have some challenges when there is a legend with 1 point, however, it works ok for now. Putting the legend into a variable allows you to access the width of the box like l$rect$w. trace=TRUE and plot=FALSE stop the legend being written to the plot just yet.
ycoord <- -1*(yinch(inchesPerMarLine*buffer)*1.8)
l<-legend(x=par("usr")[1], y=ycoord, inset=c(0,-0.25), legendText, fill=legendColour, horiz=FALSE, bty = "n", ncol=3, trace=TRUE,plot=FALSE)
lx <- mean(par("usr")[1:2]-(l$rect$w/2))
legend(x=lx, y=ycoord, legendText, fill=legendColour, horiz=FALSE, bty = "n", ncol=3)
For completeness, this is how I calculate the number of lines in the legend. Note - the number of columns in the legend is 3. labelSeries is the list of legend labels.
legendLines <- ceiling(nrow(labelSeries)/3)
I have a plot that has data that runs into the area I'd like to use for a legend. Is there a way to have the plot automatically put in something like a header space above the highest data points to fit the legend into?
I can get it to work if I manually enter the ylim() arguments to expand the size and then give the exact coordinates of where I want the legend located, but I'd prefer to have a more flexible means of doing this as it's a front end for a data base query and the data levels could have very different levels.
Edit 2017:
use ggplot and theme(legend.position = ""):
library(ggplot2)
library(reshape2)
set.seed(121)
a=sample(1:100,5)
b=sample(1:100,5)
c=sample(1:100,5)
df = data.frame(number = 1:5,a,b,c)
df_long <- melt(df,id.vars = "number")
ggplot(data=df_long,aes(x = number,y=value, colour=variable)) +geom_line() +
theme(legend.position="bottom")
Original answer 2012:
Put the legend on the bottom:
set.seed(121)
a=sample(1:100,5)
b=sample(1:100,5)
c=sample(1:100,5)
dev.off()
layout(rbind(1,2), heights=c(7,1)) # put legend on bottom 1/8th of the chart
plot(a,type='l',ylim=c(min(c(a,b,c)),max(c(a,b,c))))
lines(b,lty=2)
lines(c,lty=3,col='blue')
# setup for no margins on the legend
par(mar=c(0, 0, 0, 0))
# c(bottom, left, top, right)
plot.new()
legend('center','groups',c("A","B","C"), lty = c(1,2,3),
col=c('black','black','blue'),ncol=3,bty ="n")
You have to add the size of the legend box to the ylim range
#Plot an empty graph and legend to get the size of the legend
x <-1:10
y <-11:20
plot(x,y,type="n", xaxt="n", yaxt="n")
my.legend.size <-legend("topright",c("Series1","Series2","Series3"),plot = FALSE)
#custom ylim. Add the height of legend to upper bound of the range
my.range <- range(y)
my.range[2] <- 1.04*(my.range[2]+my.legend.size$rect$h)
#draw the plot with custom ylim
plot(x,y,ylim=my.range, type="l")
my.legend.size <-legend("topright",c("Series1","Series2","Series3"))
Building on #P-Lapointe solution, but making it extremely easy, you could use the maximum values from your data using max() and then you re-use those maximum values to set the legend xy coordinates. To make sure you don't get beyond the borders, you set up ylim slightly over the maximum values.
a=c(rnorm(1000))
b=c(rnorm(1000))
par(mfrow=c(1,2))
plot(a,ylim=c(0,max(a)+1))
legend(x=max(a)+0.5,legend="a",pch=1)
plot(a,b,ylim=c(0,max(b)+1),pch=2)
legend(x=max(b)-1.5,y=max(b)+1,legend="b",pch=2)
?legend will tell you:
Arguments
x, y
the x and y co-ordinates to be used to position the legend. They can be specified by keyword or in any way which is accepted by xy.coords: See ‘Details’.
Details:
Arguments x, y, legend are interpreted in a non-standard way to allow the coordinates to be specified via one or two arguments. If legend is missing and y is not numeric, it is assumed that the second argument is intended to be legend and that the first argument specifies the coordinates.
The coordinates can be specified in any way which is accepted by xy.coords. If this gives the coordinates of one point, it is used as the top-left coordinate of the rectangle containing the legend. If it gives the coordinates of two points, these specify opposite corners of the rectangle (either pair of corners, in any order).
The location may also be specified by setting x to a single keyword from the list bottomright, bottom, bottomleft, left, topleft, top, topright, right and center. This places the legend on the inside of the plot frame at the given location. Partial argument matching is used. The optional inset argument specifies how far the legend is inset from the plot margins. If a single value is given, it is used for both margins; if two values are given, the first is used for x- distance, the second for y-distance.