Legend position based on coordinates in ggplot2 - r

Using ggplot2's legend.position (and legend.justification), the two available parameters indicate the relative position of the legend, but what if I want to position the legend based on the coordinates of the plot?
I can't find a way to do it.
This is strange as annotate gives an x and y argument that allows such things.
Here is some toy data
library(ggplot2)
ggplot(data = mtcars, aes(x = mpg,y = disp,color = factor(cyl))) +
geom_point() +
theme(legend.position = c(0.01,0.01),
legend.justification = c(0,0))
Which gives:
What about if I want the bottom-left corner of the legend to be on coordinates (10,100)?

I don't think there is an easy way to do it. The only approach i could think of is to build the plot object to extract the ranges of the axes in order to convert (10, 100) into a relative coordinate that can be used with legend position. Admittedly, this is very hacky...
library(tidyverse)
p <- ggplot(data = mtcars, aes(x = mpg, y = disp, color = factor(cyl))) +
geom_point()
ranges <- ggplot_build(p) %>%
pluck("layout", "panel_params", 1) %>%
`[`(c("x.range", "y.range"))
x <- (10 - ranges$x.range[1]) / (ranges$x.range[2] - ranges$x.range[1])
y <- (100 - ranges$y.range[1]) / (ranges$y.range[2] - ranges$y.range[1])
p + theme(legend.position = c(x, y), legend.justification = c(0, 0))
Created on 2021-07-21 by the reprex package (v1.0.0)

Related

How to stack (overlap) legend items in ggplot2

I was wondering if it is possible to "stack" the items of the legend on a ggplot2 map (or any package that allows to produce the same result).
For spatial (sf, etc.) objects there is at least one package, mapsf, that produces the desired output, but I would like to produce this type of legend also for non-spatial objects (dataframes/tibble, etc.).
See here a reprex:
# A regular plot with ggplot2
library(ggplot2)
# A plot
ggplot(mtcars, aes(wt, mpg)) +
geom_point(aes(size = drat))
# A map with the circles of the legend "stacked" (overlapping points)
# Need to conver to sf
mtcars_sf <- sf::st_as_sf(mtcars, coords = c("wt", "mpg"))
library(mapsf)
# See the legend on the top-right corner (blue arrow)
mf_map(x = mtcars_sf)
mf_map(x = mtcars_sf, var = "drat", type = "prop",
inches = .2)
As Allan said, there is (to my knowledge) no general way to do this. But you can hack this together for your custom graph. Thomas Pedersen's awesome packages ggforce and patchwork are your friend.
I am essentially faking a legend. The challenge is to get the right dimensions between main plot and legend, and to stitch the legend in a reasonable way to the main plot. This is achieved by using carefully chosen radii on geom_ellipse, setting the coordinate ratio accordingly, and adjusting the coordinate limits from both plots. More comments in the code.
library(ggplot2)
library(ggforce)
library(patchwork)
## we cannot just use geom_point,
## because the size of the circles need to correspond to the legend later
## you will need to play around with this constant
r_constant <- 25
mtcars$r <- mtcars$drat / r_constant
## this is to make the points round - it will have an effect on the panel dimension
ellipse_fac <- .1
p <-
ggplot(mtcars) +
geom_ellipse(aes(x0 = wt, y0 = mpg, a = r, b = r / ellipse_fac, angle = 0), fill = "darkred") +
theme(legend.position = "none") +
coord_equal(ellipse_fac)
## for the legend, chose rounded values from the radius range
unique_r <- unique(plyr::round_any(mtcars$r, .1))
y_circles <- floor(min(mtcars$mpg))
## I'm sorting the radii decreasingly, so that the circles overlap correctly
circles <- data.frame(x = 0, r = sort(unique_r, decreasing = TRUE))
## the segment / label poistion is also arbitrary
x_lab <- max(circles$r) + .1
y_seg <- y_circles + 2 * circles$r / ellipse_fac
p_leg <-
ggplot(circles) +
geom_ellipse(aes(x0 = x, y0 = y_circles + r / ellipse_fac, a = r, b = r / ellipse_fac, angle = 0), fill = "darkred", alpha = .5) +
geom_segment(aes(x = x, xend = x_lab, y = y_seg, yend = y_seg)) +
geom_text(aes(x = x_lab, y = y_seg, label = r), hjust = 0) +
## you need to set the ylimits similar to the main plot for control of legend position
coord_equal(ratio = ellipse_fac, ylim = range(mtcars$mpg), clip = "off") +
theme_void() +
## also need to set a margin
theme(plot.margin = margin(r = .2, unit = "in"))
p + p_leg
I've added an alpha just for aesthetic reasons

Cohesively combine and alter the dimensions of two figures on the same y axis in ggplot()

I am attempting to accomplish two tasks:
Cohesively combine two figures that share the same y axis, but one which has a categorical x axis variable and the other that has a continuous x axis variable. I would like to display them as contiguous, only separated by a solid black line (i.e. the right edge of the left plot and the left edge of the right plot).
Modify freely the dimensions of the figures, so that I can i. extend the x axis on the left figure to better demonstrate the spread of the data, and to ii. idealize the ratio of the size of the two figures.
Below is my attempt:
#libraries used:
library(ggplot2)
library(dplyr)
#Pulling in example dataset:
data_1 <- iris
#Building my left figure, which has a continuous x and y axis, and establishing y axis limits to match between the two figures:
object_1 <- ggplot(data_1, aes(x = Sepal.Width, y = Sepal.Length)) + geom_point() + ylim(0, 10)
#Building my second data table:
data_2 <- iris %>% group_by(Species) %>% summarize(av_petal_length = mean(Petal.Length))
#Building my right hand figure, with empty y axis titles and text to provide space to combine the two figures on the left y axis:
object_2 <- ggplot(data_2, aes(x = Species, y = av_petal_length)) + geom_point() + ylim(0, 10) +
theme(axis.title.y = element_blank(),
axis.text.y = element_blank())
#Attempt to grid.arrange:
grid.arrange(object_1, object_2, nrow = 1)
As you can see, a simple grid.arrange does not combine them completely. I have attempted to modify the panel margins in the two figures by tinkering with plot.margin() under theme(), but this requires a lot of tinkering and if the figures get resized at all the relationship between the two figures can become distorted. Is it possible to cleanly, simply combine these two figures into one cohesive rectangle, separated by a line, and to manually modify the dimensions of the figures?
Below, we're using seperate themes for the left and right plots that delete the relevant plot margins and the y-axis of the right plot.
I'm sure you can do it with grid.arrange() too, but {patchwork} allows you to set figure widths as well.
library(ggplot2)
library(dplyr)
library(patchwork)
# As before
data_1 <- iris
object_1 <- ggplot(data_1, aes(x = Sepal.Width, y = Sepal.Length)) + geom_point() + ylim(0, 10)
data_2 <- iris %>% group_by(Species) %>% summarize(av_petal_length = mean(Petal.Length))
object_2 <- ggplot(data_2, aes(x = Species, y = av_petal_length)) + geom_point() + ylim(0, 10)
# Remove relevant margins from theme, including y-axis elements on the right
theme_left <- theme(plot.margin = margin(5.5, 0, 5.5, 5.5))
theme_right <- theme(plot.margin = margin(5.5, 5.5, 5.5, 0),
axis.ticks.length.y = unit(0, "pt"),
axis.title.y = element_blank(),
axis.text.y = element_blank())
black_line <- annotate("segment", x = Inf, xend = Inf, y = -Inf, yend = Inf, size = 2)
# Patchwork everything together
(object_1 + theme_left + black_line) +
(object_2 + theme_right) +
plot_layout(widths = c(2, 1))
Created on 2022-02-01 by the reprex package (v2.0.1)

Is there any alternative to ggtern in R?

Its looks like that ggtern has not been synchronised with new version of ggplot2.
Therefore we can not use ggtern.
library(ggtern)
set.seed(1)
plot <- ggtern(data = data.frame(x = runif(100),
y = runif(100),
z = runif(100)),
aes(x, y, z))
plot + stat_density_tern(geom = 'polygon',
n = 200,
aes(fill = ..level..,
alpha = ..level..)) +
geom_point() +
theme_rgbw() +
labs(title = "Example Density/Contour Plot") +
scale_fill_gradient(low = "blue",high = "red") +
guides(color = "none", fill = "none", alpha = "none")
Error: geom_point requires the following missing aesthetics: x and y
Does anyone have in find other options for ternary diagrams apart from ggtern in R?
Manually, you could plot the points with a function: (I used the formulas at https://en.wikipedia.org/wiki/Ternary_plot)
I'm not familiar with the output of stat_density_tern so I'm not sure what is expected from that part.
library(tidyverse)
tern <- function(df) {
df %>% mutate(x_pos = 0.5 * (2*y + z) / (x+y+z),
y_pos = sqrt(3) / 2 * z / (x+y+z))
}
tern(plot) %>%
ggplot(aes(x_pos, y_pos)) +
geom_point() +
annotate("path", x = c(0, 0.5, 1, 0), y = c(0,sqrt(3)/2,0,0)) +
coord_equal()
this worked for me! Uninstall ggtern and ggplot2 then
install_version("ggplot2", version = "3.3.0", repos = "http://cran.us.r-project.org")
install.packages("ggtern")
library(ggtern)
I use the following script which also supports making diagrams with 4 or more corners. It also divides the points into colored clusters by cutting a hierarchical clustering at the height where it has 32 subtrees, and it draws a line from each point to its two nearest neighbors.
library(tidyverse)
library(ggforce)
library(colorspace)
t=as.matrix(read.csv("https://pastebin.com/raw/1EDJJtHU",row.names=1,check.names=F))/100
fst=as.matrix(read.csv("https://pastebin.com/raw/6JmN2hRY",row.names=1))
mult=t%*%cmdscale(fst,ncol(fst)-1)
# t=cbind(t[,2]+t[,1],t[,8]+t[,9],rowSums(t[,-c(1,2,8,9)]))
# colnames(t)=c("Baltic + North_Atlantic","Siberian + East_Asian","Other")
# t=cbind(t[,2],t[,1],t[,8]+t[,9],rowSums(t[,-c(1,2,8,9)]))
# colnames(t)=c("Baltic","North_Atlantic","Siberian + East_Asian","Other")
ncorn=ncol(t)
start=ifelse(ncorn==4,.25,0)
corners=sapply(c(sin,cos),\(x)x((start+seq(0,2,,ncorn+1)[-(ncorn+1)])*pi))
corners=corners*min(2/diff(apply(corners,2,range)))
corners[,2]=corners[,2]-mean(range(corners[,2]))
xy=t%*%corners
grid=if(ncorn==3)do.call(rbind.data.frame,apply(simplify=F,rbind(c(1,2,3,2),c(1,3,2,3),c(2,1,3,1)),1,\(x)cbind(
seq(corners[x[1],1],corners[x[2],1],,11),
seq(corners[x[1],2],corners[x[2],2],,11),
seq(corners[x[3],1],corners[x[4],1],,11),
seq(corners[x[3],2],corners[x[4],2],,11)
)))else if(ncorn==4)do.call(rbind.data.frame,apply(simplify=F,rbind(c(1,2,4,3),c(1,4,2,3)),1,\(x)cbind(
seq(corners[x[1],1],corners[x[2],1],,11),
seq(corners[x[1],2],corners[x[2],2],,11),
seq(corners[x[3],1],corners[x[4],1],,11),
seq(corners[x[3],2],corners[x[4],2],,11)
)))else rbind.data.frame(cbind(corners,rbind(corners[-1,],corners[1,])),cbind(corners,matrix(colMeans(corners),ncorn,2,T)))
seg=as.data.frame(cbind(xy[rep(1:nrow(xy),each=2),],xy[apply(as.matrix(dist(mult)),1,\(x)order(x)[2:3]),]))
k=as.factor(cutree(hclust(dist(mult)),32))
set.seed(0)
hue=seq(0,360,,nlevels(k)+1)%>%head(-1)%>%sample()
pal1=hex(colorspace::HSV(hue,.6,1))
pal2=hex(colorspace::HSV(hue,.3,1))
angle=head(seq(360,0,length.out=ncorn+1),-1)
angle=ifelse(angle>90&angle<=270,angle+180,angle)
ggplot(as.data.frame(xy),aes(x=V1,y=V2))+
geom_polygon(data=as.data.frame(corners),fill="gray25")+
(if(ncorn>=5)geom_text(data=as.data.frame(corners),aes(x=1.04*V1,y=1.04*V2),label=colnames(t),size=3.2,angle=angle,color="gray85") # use rotated labels
else geom_text(data=as.data.frame(corners),aes(x=V1,y=1.03*V2),vjust=(1-corners[,2])/2,hjust=(1+corners[,1])/2,label=colnames(t),size=3.2,color="gray85"))+ # don't rotate labels
geom_segment(data=grid,aes(x=V1,y=V2,xend=V3,yend=V4),color="gray30",size=.4)+
ggforce::geom_mark_hull(aes(group=!!k,color=!!k,fill=!!k),concavity=1000,radius=unit(.15,"cm"),expand=unit(.15,"cm"),alpha=.15,size=.1)+
geom_segment(data=seg,aes(x=V1,y=V2,xend=V3,yend=V4),color="gray10",size=.25)+
geom_point(aes(color=k),size=.5)+
geom_text(aes(label=rownames(xy),color=!!k),size=2.2,vjust=-.6)+
coord_fixed(xlim=c(-1,1),ylim=c(-1,1))+
scale_fill_manual(values=pal1)+
scale_color_manual(values=pal2)+
theme(
axis.text=element_blank(),
axis.ticks=element_blank(),
axis.title=element_blank(),
legend.position="none",
panel.background=element_rect(fill="gray20"),
panel.grid=element_blank(),
plot.background=element_rect(fill="gray20",color=NA,size=0),
plot.margin=margin(0,0,0,0)
)
ggsave("1.png",width=7,height=7)
Or here's another version that uses Voronoi tesselation to plot the points (https://ggforce.data-imaginist.com/reference/geom_delvor.html):
t=read.table("https://pastebin.com/raw/CeLAEiAq")
t=distinct(t[,-1]) # geom_voronoi_tile doesn't handle a large number of overlapping points
pop=t[,1]
t=as.matrix(t[,-1])
ncorn=ncol(t)
start=ifelse(ncorn==4,.25,0)
corners=sapply(c(sin,cos),\(x)x((start+seq(0,2,,ncorn+1)[-(ncorn+1)])*pi))
corners=corners*min(2/diff(apply(corners,2,range))) # resize so bigger one of width and height is 2
corners[,2]=corners[,2]-mean(range(corners[,2])) # center vertically
xy=as.data.frame(t%*%corners)
# # use a simple grid with line from each corner to center for a plot with more than 3 corners
# grid=rbind.data.frame(cbind(corners,rbind(corners[-1,],corners[1,])),cbind(corners,matrix(colMeans(corners),ncorn,2,T)))
# use a grid with 10 subdivisions per side for a triangle plot
grid=do.call(rbind.data.frame,apply(simplify=F,rbind(c(1,2,3,2),c(1,3,2,3),c(2,1,3,1)),1,\(x)cbind(
seq(corners[x[1],1],corners[x[2],1],,11),
seq(corners[x[1],2],corners[x[2],2],,11),
seq(corners[x[3],1],corners[x[4],1],,11),
seq(corners[x[3],2],corners[x[4],2],,11)
)))
centers=data.frame(aggregate(xy,list(pop),mean),row.names=1)
set.seed(0)
color=as.factor(sample(length(unique(pop))))
cl=rbind(c(60,80),c(25,95),c(30,70),c(70,50),c(60,100),c(20,50),c(15,40))
hues=max(ceiling(length(color)/nrow(cl)),8)
pal1=as.vector(apply(cl,1,\(x)hcl(seq(15,375,,hues+1)[-(hues+1)],x[1],x[2])))
pal2=as.vector(apply(cl,1,\(x)hcl(seq(15,375,,hues+1)[-(hues+1)],if(x[2]>=60).5*x[1]else .1*x[1],if(x[2]>=60).2*x[2]else 95)))
xy=xy+runif(nrow(xy)*2)/1e3 # add a small random factor to prevent errors because of overlapping points
ggplot(xy,aes(V1,V2))+
geom_segment(data=grid,aes(V1,V2,xend=V3,yend=V4),color="gray85",size=.3)+
ggforce::geom_voronoi_tile(aes(group=0,fill=color[as.factor(pop)],color=color[as.factor(pop)]),size=.07,max.radius=.055)+ # `group=0` is just an arbitrary constant
# ggrepel::geom_label_repel(data=centers,aes(V1,V2,color=color,fill=color),label=rownames(centers),max.overlaps=Inf,point.size=0,size=2.3,alpha=.8,label.r=unit(.1,"lines"),label.padding=unit(.1,"lines"),label.size=.1,box.padding=0,segment.size=.3)+
geom_label(data=centers,aes(V1,V2,color=color,fill=color),label=rownames(centers),size=2.3,alpha=.8,label.r=unit(.1,"lines"),label.padding=unit(.1,"lines"),label.size=.1)+
coord_fixed(xlim=c(-1.08,1.08),ylim=c(-1.08,1.08),expand=F)+
scale_fill_manual(values=pal1)+
scale_color_manual(values=pal2)+
theme(
axis.text=element_blank(),
axis.ticks=element_blank(),
axis.title=element_blank(),
legend.position="none",
panel.background=element_rect(fill="white")
)
ggsave("1.png",width=7,height=7)

Removing ggplot2 legend removes whole data from the plot

Here I have 2-dim numeric array dataset and numeric 1-dim array of labels clustring. Then I plot it with the following code:
s = data.frame(x = dataset[,1], y = dataset[,2])
p = ggplot(s, aes(x, y))
p + geom_point(aes(colour = factor(clustering)))
which displays beautiful picture:
Now I want to remove legend completely, so here I've found possible solution:
# Remove legend for a particular aesthetic (fill)
p + guides(fill=FALSE)
# It can also be done when specifying the scale
p + scale_fill_discrete(guide=FALSE)
# This removes all legends
p + theme(legend.position="none")
but none of such commands wont help. It shows empty plot instead:
So how do I remove the legend from my plot?
Try this:
library(ggplot2)
s = data.frame(x = rnorm(20), y = rnorm(20), clustering = rep(c(1, 2), 10))
p <- ggplot(s, aes(x, y))+
guides(fill=FALSE)+
geom_point(aes(colour = factor(clustering)))+
scale_fill_discrete(guide=FALSE)+
theme(legend.position="none")
p
In your code, you are not saving the plot again after each time you add something to it. You can fix this by changing the lines that add to the plot:
# Remove legend for a particular aesthetic (fill)
p = p + guides(fill=FALSE)
But the way I wrote is is more common R formatting.
Use show.legend = FALSE within geom_point. Here is an example using ggplot2's diamonds dataset.
s <- diamonds
p <- ggplot(data = s, aes(x = depth, y = price))
p + geom_point(aes(colour = factor(cut)), show.legend = FALSE)
Just try this:
p + geom_point(aes(colour = factor(clustering)),show.legend=FALSE)

R plot type "b" with text instead of points - Slope graph with ggplot2

is there a way in ggplot2 to get the plot type "b"? See example:
x <- c(1:5)
y <- x
plot(x,y,type="b")
Ideally, I want to replace the points by their values to have something similar to this famous example:
EDIT:
Here some sample data (I want to plot each "cat" in a facet with plot type "b"):
df <- data.frame(x=rep(1:5,9),y=c(0.02,0.04,0.07,0.09,0.11,0.13,0.16,0.18,0.2,0.22,0.24,0.27,0.29,0.31,0.33,0.36,0.38,0.4,0.42,0.44,0.47,0.49,0.51,0.53,0.56,0.58,0.6,0.62,0.64,0.67,0.69,0.71,0.73,0.76,0.78,0.8,0.82,0.84,0.87,0.89,0.91,0.93,0.96,0.98,1),cat=rep(paste("a",1:9,sep=""),each=5))
Set up the axes by drawing the plot without any content.
plot(x, y, type = "n")
Then use text to make your data points.
text(x, y, labels = y)
You can add line segments with lines.
lines(x, y, col = "grey80")
EDIT: Totally failed to clock the mention of ggplot in the question. Try this.
dfr <- data.frame(x = 1:5, y = 1:5)
p <- ggplot(dfr, aes(x, y)) +
geom_text(aes(x, y, label = y)) +
geom_line(col = "grey80")
p
ANOTHER EDIT: Given your new dataset and request, this is what you need.
ggplot(df, aes(x, y)) + geom_point() + geom_line() + facet_wrap(~cat)
YET ANOTHER EDIT: We're starting to approach a real question. As in 'how do you make the lines not quite reach the points'.
The short answer is that that isn't a standard way to do this in ggplot2. The proper way to do this would be to use geom_segment and interpolate between your data points. This is quite a lot of effort however, so I suggest an easier fudge: draw big white circles around your points. The downside to this is that it makes the gridlines look silly, so you'll have to get rid of those.
ggplot(df, aes(x, y)) +
facet_wrap(~cat) +
geom_line() +
geom_point(size = 5, colour = "white") +
geom_point() +
opts(panel.background = theme_blank())
There's an experimental grob in gridExtra to implement this in Grid graphics,
library(gridExtra)
grid.newpage() ; grid.barbed(pch=5)
This is now easy with ggh4x::geom_pointpath. Set shape = NA and add a geom_text layer.
library(ggh4x)
#> Loading required package: ggplot2
df <- data.frame(x = rep(1:5, each = 5),
y = c(outer(seq(0, .8, .2), seq(0.02, 0.1, 0.02), `+`)),
cat = rep(paste0("a", 1:5)))
ggplot(df, aes(x, y)) +
geom_text(aes(label = cat)) +
geom_pointpath(aes(group = cat, shape = NA))
Created on 2021-11-13 by the reprex package (v2.0.1)
Another way to make great slope graphs is using the package CGPfunctions.
library(CGPfunctions)
newggslopegraph(newcancer, Year, Survival, Type)
You have also many options to choose. You can find a good tutorial here:
https://www.r-bloggers.com/2018/06/creating-slopegraphs-with-r/

Resources