I would like to use facet_zoom() to zoom in on part of an axis that has limits explicitly set. However, using scale_*(limits = *) and coord_cartesian(xlim = *) overrides the zoomed facet's scales as well such that both have the same limits. Is there a way around this? Maybe I could add some data points near the limits and then set their alpha = 0... Any other ideas?
library(ggplot2)
library(ggforce)
# works with no limits specified
ggplot(mpg, aes(x = hwy, y = cyl)) +
geom_point() +
facet_zoom(xlim = c(20, 25))
# fails with limits specified
ggplot(mpg, aes(x = hwy, y = cyl)) +
scale_x_continuous(limits = c(0, 50)) +
geom_point() +
facet_zoom(xlim = c(20, 25))
# fails with coord_cartesian()
ggplot(mpg, aes(x = hwy, y = cyl)) +
scale_x_continuous() +
coord_cartesian(xlim = c(0, 50)) +
geom_point() +
facet_zoom(xlim = c(20, 25))
I don't have enough knowledge of the underlying intricacies in FacetZoom, but you can check if the following workarounds provide a reasonable starting point.
Plot for demonstration
One of the key differences between setting limits in scales_* vs. coord_* is the clipping effect (screenshot taken from the ggplot2 cheatsheet found here). Since this effect isn't really clear in a scatterplot, I added a geom_line layer and adjusted the specified limits so that the limits extend beyond the data range on one end of the x-axis, & clips the data on the other end.
p <- ggplot(mpg, aes(x = hwy, y = cyl)) +
geom_point() +
geom_line(aes(colour = fl), size = 2) +
facet_zoom(xlim = c(20, 25)) +
theme_bw()
# normal zoomed plot / zoomed plot with limits set in scale / coord
p0 <- p
p1 <- p + scale_x_continuous(limits = c(0, 35))
p2 <- p + coord_cartesian(xlim = c(0, 35))
We can see that while p0 behaves as expected, both p1 & p2 show both the original facet (top) & the zoomed facet (bottom) with the same range of c(0, 35).
In p1's case, the shaded box also expanded to cover the entire top facet. In p2's case, the zoom box stayed in exactly the same position as p0, & as a result no longer covers the zoomed range of c(20, 25).
Workaround for limits set in scale_*
# convert ggplot objects to form suitable for rendering
gp0 <- ggplot_build(p0)
gp1 <- ggplot_build(p1)
# re-set zoomed facet's limits to match zoomed range
k <- gp1$layout$layout$SCALE_X[gp1$layout$layout$name == "x"]
gp1$layout$panel_scales_x[[k]]$limits <- gp1$layout$panel_scales_x[[k]]$range$range
# re-set zoomed facet's panel parameters based on original version p0
k <- gp1$layout$layout$PANEL[gp1$layout$layout$name == "x"]
gp1$layout$panel_params[[k]] <- gp0$layout$panel_params[[k]]
# convert built ggplot object to gtable of grobs as usual & print result
gt1 <- ggplot_gtable(gp1)
grid::grid.draw(gt1)
The zoomed facet now shows the zoomed range c(20, 25) correctly, while the shaded box shrinks to cover the correct range in the original facet. Since this method removes unseen data points, all lines in the original facet stay within the confines of the facet.
Workaround for limits set in coord_*
# convert ggplot objects to form suitable for rendering
gp0 <- ggplot_build(p0)
gp1 <- ggplot_build(p1)
# apply coord limits to original facet's scale limits
k <- gp2$layout$layout$SCALE_X[gp2$layout$layout$name == "orig"]
gp2$layout$panel_scales_x[[k]]$limits <- gp2$layout$coord$limits$x
# re-set zoomed facet's panel parameters based on original version without setting
# limits in scale
k <- gp1$layout$layout$PANEL[gp1$layout$layout$name == "x"]
gp2$layout$panel_params[[k]] <- gp0$layout$panel_params[[k]]
# convert built ggplot object to gtable of grobs as usual,
# & print result
gt2 <- ggplot_gtable(gp2)
grid::grid.draw(gt2)
The zoomed facet now shows the zoomed range c(20, 25) correctly, while the shaded box shifts to cover the correct range in the original facet. Since this method includes unseen data points, some lines in the original facet extend beyond the facet's confines.
Note: These workarounds should work with zoom in y + limits set in y-axis as well, as long as all references to "x" / panel_scales_x / SCALE_X above are changed to "y" / panel_scales_y / SCALE_Y. I haven't tested this for other combinations such as zoom in both x & y, but the broad principle ought to be similar.
Related
I am trying to make a scatterplot using ggplot2 in which the diameter of the points is of the same dimensions as the variables on the axes and should have the same scale. This problem is laid out well in this question as well as this one, which was resolved by drawing ellipses on the graph (nowadays done with geom_circle from ggforce. However, for my application I need to draw thousands of points, which is quick using geom_point but very slow using geom_circle. Is there a way to scale geom_point to the scale of the axes?
As an example of the problem, this graph shows the discrepancy in scales using scale_radius:
x <- runif(20, 0, 20)
y <- runif(20, 0, 20)
radius <- runif(20, 0, 4)
df <- data.frame(x = x, y = y, size = radius)
library(ggplot2)
p <- ggplot(
data = df,
mapping = aes(
x = x,
y = y,
size = radius
)
) +
geom_point() +
coord_fixed() + xlim(0, 20) + ylim(0, 20) +
scale_radius(range = c(min(radius), max(radius)))
p
I have tried using scale_radius and scale_continuous, but both use a scale that is arbitrary with relation to the axis scales (scale_radius also does not scale such that a point of size 0 displays with size 0). I had the idea of accessing the plot size using ggplot_build and scaling the point sizes accordingly. I can access the plot range using ggplot_buil(p)$layout$get_scales(i=1) or layer_scales(p), but no variables appear to correspond to the size of the plot in the units that scale_radius uses.
Using 2000 circles and max radius of 1 (ie max diameter 2), I get a ~5-7x speedup using a lower poly resolution per circle. You might also look at your output device and try ragg, which is faster than cairo still offers nice anti-aliasing.
ggplot(df, aes(x0 = x, y0 = y, r = radius)) +
ggforce::geom_circle(n = 20) + #5-7x faster than default
coord_fixed()
Still looks pretty good, with 1.5 sec render time on my system.
(You might also consider defining your plot window range using coord_fixed(xlim = c(0,20), ylim = c(0,20)) since that will have the effect of zooming in on that viewing window instead of cropping out all data points out outside it, as your xlim() and ylim() (shortcuts for scale_x_continuous(limits = ...) and scale_y_continuous(limits = ...). It's not an issue for geom_point but for geom_circle your approach will result in cut-off circles.)
I'm trying to force the grid of a scatter plot to be composed of squares, with x and y values that have different ranges.
I tried to force a square shape of the whole plot (aspect.ratio=1), but this does not solve the problem of different ranges. Then I tried to change limits of values of my axes.
1)Here is what I tried first:
p + theme(aspect.ratio = 1) +
coord_fixed(ratio=1, xlim = c(-0.050,0.050),ylim = c(-0.03,0.03))
2) I changed the ratio by using the range of the values for each axis:
p + coord_fixed(ratio=0.06/0.10, xlim = c(-0.050,0.050), ylim = c(-0.03,0.03))
3)Then I changed the limits of y to match those of x:
p + theme(aspect.ratio = 1) +
coord_fixed(ratio=1, xlim = c(-0.050,0.050),ylim = c(-0.05,0.05))
1) The grid on the background is composed by rectangles.
2) I would expect this to change the position of the tick marks automatically in order to give me a grid composed of squares. Still triangles.
3) It obviously worked 'cause I matched the ranges of x and y. But there was a lot of empty space in the graph.
Is there something else I should try?
Thanks in advance.
If you want the plot to be square and you want the grid to be square you can do this by rescaling the y variable to be on the same scale as the x variable (or vice versa) for plotting, and then inverting the rescaling to generate the correct axis value labels for the rescaled axis.
Here's an example using the mtcars data frame, and we'll use the rescale function from the scales package.
First let's create a plot of mpg vs. hp but with the hp values rescaled to be on the same scale as mpg:
library(tidyverse)
library(scales)
theme_set(theme_bw())
p = mtcars %>%
mutate(hp.scaled = rescale(hp, to=range(mpg))) %>%
ggplot(aes(mpg, hp.scaled)) +
geom_point() +
coord_fixed() +
labs(x="mpg", y="hp")
Now we can invert the rescaling to generate the correct value labels for hp. We do that below by supplying the inverting function to the labels argument of scale_y_continuous:
p + scale_y_continuous(labels=function(x) rescale(x, to=range(mtcars$hp)))
But note that rescaling back to the original hp scale results in non-pretty breaks. We can fix that by generating pretty breaks on the hp scale, rescaling those to the mpg scale to get the locations where we want the tick marks and then inverting that to get the label values. However, in that case we won't get a square grid if we want to keep the overall plot panel square:
p + scale_y_continuous(breaks = rescale(pretty_breaks(n=5)(mtcars$hp),
from=range(mtcars$hp),
to=range(mtcars$mpg)),
labels = function(x) rescale(x, from=range(mtcars$mpg), to=range(mtcars$hp)))
I'm not sure what code you are using, it is missing in block 1 and 3. But using the mtcars data set the following works:
library(ggplot2)
ggplot(mtcars, aes(mpg, wt)) +
geom_point() +
coord_fixed(ratio = 1) +
scale_x_continuous(breaks = seq(10, 35, 1)) +
scale_y_continuous(breaks = seq(1, 6, 1))
The last two lines make it clear that 1 point on the x-axis is equal to 1 point on the y-axis.
In the documention you will further find the following advise:
ensures that the ranges of axes are equal to the specified ratio by
adjusting the plot aspect ratio
Making a plot with ggplot, I wish to set my axis exactly. I am aware that I can set the plot range (e.g. for the x-axis I specified limits from 2 to 4) with coord_cartesian() but that leaves a bit of space to the left and right of the range I specify:
Code for the MWE above:
library(ggplot2)
data.mwe = cbind.data.frame(x = 1:5, y = 2:6)
plot.mwe = ggplot(data = data.mwe, aes(x=x,y=y)) + geom_line() + coord_cartesian(xlim = c(2,4))
print(plot.mwe)
My desired result is a plot where the displayed area is exactly between the limits I specify.
I am aware of
How to set limits for axes in ggplot2 R plots?
but it does not answer my question, as it produces the undesired result above, or cuts out observations (with the limits argument to scale_x_continuous). I know I could tinker with setting a smaller range for limits, but I am looking for a clean result. At the least I would like to know by how much the actual range differs from the one I specify, so that I could adapt my limits accordingly.
Add expand = FALSE:
library(ggplot2)
data.mwe = data.frame(x = 1:5,
y = 2:6)
ggplot(data.mwe, aes(x, y)) +
geom_line() +
coord_cartesian(xlim = c(2, 4),
expand = FALSE)
Is there any way to line up the points of a line plot with the bars of a bar graph using ggplot when they have the same x-axis? Here is the sample data I'm trying to do it with.
library(ggplot2)
library(gridExtra)
data=data.frame(x=rep(1:27, each=5), y = rep(1:5, times = 27))
yes <- ggplot(data, aes(x = x, y = y))
yes <- yes + geom_point() + geom_line()
other_data = data.frame(x = 1:27, y = 50:76 )
no <- ggplot(other_data, aes(x=x, y=y))
no <- no + geom_bar(stat = "identity")
grid.arrange(no, yes)
Here is the output:
The first point of the line plot is to the left of the first bar, and the last point of the line plot is to the right of the last bar.
Thank you for your time.
Extending #Stibu's post a little: To align the plots, use gtable (Or see answers to your earlier question)
library(ggplot2)
library(gtable)
data=data.frame(x=rep(1:27, each=5), y = rep(1:5, times = 27))
yes <- ggplot(data, aes(x = x, y = y))
yes <- yes + geom_point() + geom_line() +
scale_x_continuous(limits = c(0,28), expand = c(0,0))
other_data = data.frame(x = 1:27, y = 50:76 )
no <- ggplot(other_data, aes(x=x, y=y))
no <- no + geom_bar(stat = "identity") +
scale_x_continuous(limits = c(0,28), expand = c(0,0))
gYes = ggplotGrob(yes) # get the ggplot grobs
gNo = ggplotGrob(no)
plot(rbind(gNo, gYes, size = "first")) # Arrange and plot the grobs
Edit To change heights of plots:
g = rbind(gNo, gYes, size = "first") # Combine the plots
panels <- g$layout$t[grepl("panel", g$layout$name)] # Get the positions for plot panels
g$heights[panels] <- unit(c(0.7, 0.3), "null") # Replace heights with your relative heights
plot(g)
I can think of (at least) two ways to align the x-axes in the two plots:
The two axis do not align because in the bar plot, the geoms cover the x-axis from 0.5 to 27.5, while in the other plot, the data only ranges from 1 to 27. The reason is that the bars have a width and the points don't. You can force the axex to align by explicitly specifying an x-axis range. Using the definitions from your plot, this can be achieved by
yes <- yes + scale_x_continuous(limits=c(0,28))
no <- no + scale_x_continuous(limits=c(0,28))
grid.arrange(no, yes)
limits sets the range of the x-axis. Note, though, that the alginment is still not quite perfect. The y-axis labels take up a little more space in the upper plot, because the numbers have two digits. The plot looks as follows:
The other solution is a bit more complicated but it has the advantage that the x-axis is drawn only once and that ggplot makes sure that the alignment is perfect. It makes use of faceting and the trick described in this answer. First, the data must be combined into a single data frame by
all <- rbind(data.frame(other_data,type="other"),data.frame(data,type="data"))
and then the plot can be created as follows:
ggplot(all,aes(x=x,y=y)) + facet_grid(type~.,scales = "free_y") +
geom_bar(data=subset(all,type=="other"),stat="identity") +
geom_point(data=subset(all,type=="data")) +
geom_line(data=subset(all,type=="data"))
The trick is to let the facets be constructed by the variable type which was used before to label the two data sets. But then each geom only gets the subset of the data that should be drawn with that specific geom. In facet_grid, I also used scales = "free_y" because the two y-axes should be independent. This plot looks as follows:
You can change the labels of the facets by giving other names when you define the data frame all. If you want to remove them alltogether, then add the following to your plot:
+ theme(strip.background = element_blank(), strip.text = element_blank())
When I compile the following MWE I observe that the maximum point (3,5) is significantly cut/cropped by the margins.
The following example is drastically reduced for simplicity.
In my actual data the following are all impacted by limiting my coord_cartesian manually if the coresponding x-axis aesthetic is on the max x value.
Point symbol
Error bars
Statistical symbols inserted by text annotation
MWE
library(ggplot2)
library("grid")
print("Program started")
n = c(0.1,2, 3, 5)
s = c(0,1, 2, 3)
df = data.frame(n, s)
gg <- ggplot(df, aes(x=s, y=n))
gg <- gg + geom_point(position=position_dodge(width=NULL), size = 1.5)
gg <- gg + geom_line(position=position_dodge(width=NULL))
gg <- gg + coord_cartesian( ylim = c(0, 5), xlim = c((-0.05)*3, 3));
print(gg)
print("Program complete - a graph should be visible.")
To show my data appropriately I would consider using any of the following that are possible (influenced by the observation that the x-axis labels themselves are never cut):
Make the margin transparent so the point isn't cut
unless the point is cut by the plot area and not the margin
Bring the panel with the plot area to the front
unless the point is cut by the plot area and not the margin so order is independent
Use xlim = c((-0.05)*3, (3*0.05)) to extend the axis range but implement some hack to not show the overhanging axis bar after the maximum point of 3?
this is how I had it originally but I was told to remove the overhang after the 3 as it was unacceptable.
Is this what you mean by option 1:
gg <- ggplot(df, aes(x=s, y=n)) +
geom_point(position=position_dodge(width=NULL), size = 3) +
geom_line(position=position_dodge(width=NULL)) +
coord_cartesian(xlim=c(0,3), ylim=c(0,5))
# Turn of clipping, so that point at (3,5) is not clipped by the panel grob
gg1 <- ggplot_gtable(ggplot_build(gg))
gg1$layout$clip[gg1$layout$name=="panel"] <- "off"
grid.draw(gg1)