geom_tile border missing at corners

2020-02-13 09:22发布

问题:

I have the following data frame:

# Dummy data frame
df <- expand.grid(x = 1:3, y = 1:3)

I would like to plot it as a geom_tile using ggplot2 like so:

# Tile plot
ggplot(df) + 
  geom_tile(aes(x = x, y = y), 
            fill = NA, colour = "red", size = 3, width = 0.7, height = 0.7)

which gives,

Notice, however, that in the top left corner of each tile there is a notch missing where the border doesn't quite dovetail correctly. I get the same result if I use geom_rect. Is there a workaround to avoid this?


R version 3.5.1 (2018-07-02)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 7 x64 (build 7601) Service Pack 1

Matrix products: default  

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] ggplot2_3.1.0     kitchendraw_0.1.0

loaded via a namespace (and not attached):
 [1] Rcpp_1.0.0       rstudioapi_0.8   bindr_0.1.1      magrittr_1.5     tidyselect_0.2.5 munsell_0.5.0    colorspace_1.3-2
 [8] R6_2.3.0         rlang_0.3.0.1    plyr_1.8.4       dplyr_0.7.7      tools_3.5.1      grid_3.5.1       gtable_0.2.0    
[15] withr_2.1.2      yaml_2.2.0       lazyeval_0.2.1   assertthat_0.2.0 tibble_1.4.2     crayon_1.3.4     bindrcpp_0.2.2  
[22] purrr_0.2.5      glue_1.3.0       labeling_0.3     compiler_3.5.1   pillar_1.3.0     scales_1.0.0     pkgconfig_2.0.2 

Updated figure in response to a comment below

回答1:

As others have noted, this is due to the lineend specification, which can be found in environment(GeomTile$draw_panel)$f:

function (self, data, panel_params, coord) 
{
    if (!coord$is_linear()) {
        ... #omitted for space
    }
    else {
        coords <- coord$transform(data, panel_params)
        ggname("geom_rect", 
               rectGrob(coords$xmin, coords$ymax, 
                        width = coords$xmax - coords$xmin, height = coords$ymax - 
                        coords$ymin, default.units = "native", just = c("left", "top"), 
                        gp = gpar(col = coords$colour, 
                                  fill = alpha(coords$fill, coords$alpha), 
                                  lwd = coords$size * .pt, 
                                  lty = coords$linetype, 
                                  lineend = "butt"))) # look here
    }
}

The creation of a geom_tile layer is powered by rectGrob, with a hard-coded lineend parameter value of "butt". The graphic below (found here) illustrates the difference between the 3 lineend values nicely:

If you feel like digging into the underlying GeomTile's functions and changing the graphics parameters for all geom_tile layers in your code, you can do that. (I answered a similar question recently with that solution.) For a single plot, though, I'd just convert the ggplot to a grob object, & mess with the gp parameters there instead:

library(grid)
gp <- ggplotGrob(p)
grid.draw(gp) 

# this "sharpens" the top left corner
gp$grobs[[which(grepl("panel", gp$layout$name))]]$children[[3]]$gp$lineend <- "square"
grid.draw(gp)

# this further "sharpens" the other three corners
gp$grobs[[which(grepl("panel", gp$layout$name))]]$children[[3]]$gp$linejoin <- "mitre"
grid.draw(gp)

Note: the actual location of the correct grob corresponding to geom_tile is not necessarily going to be gp$grobs[[which(grepl("panel", gp$layout$name))]]$children[[3]]$gp$linejoin. It's children[[3]] here, but having other geom layers in the ggplot object, either under or above the geom_tile layer, can shift its relative position. In that case, you may want to check the output from gp$grobs[[which(grepl("panel", gp$layout$name))]]$children in the console to identify the correct position number.



回答2:

I think this happens because the starting point of each line is just that: a point. And because the size of the line makes it thicker, this starting point makes this blank spaces. This plot uses four geom_segment to make one square and the result shows the same problem you encountered:

ggplot(df) + 
   geom_segment(x = 1, y = 1, xend = 2, yend = 1, size = 3) +
   geom_segment(x = 1, y = 1, xend = 1, yend = 2, size = 3) +
   geom_segment(x = 1, y = 2, xend = 2, yend = 2, size = 3) +
   geom_segment(x = 2, y = 2, xend = 2, yend = 1, size = 3) +
   scale_x_continuous(limits = c(0, 3)) + 
   scale_y_continuous(limits = c(0, 3))

The only solution I can think of is making the starting and end points of one of the x or y axis a little behind (for starting) and ahead (for finishing). This solution is far from ideal, but is the only one I can think of. For a line of size = 3, I found that substracting and adding 0.01 to the starting and finishing points fills the blank space:

ggplot(df) + 
 geom_segment(x = 1-0.01, y = 1, xend = 2+0.01, yend = 1, size = 3) +
 geom_segment(x = 1, y = 1, xend = 1, yend = 2, size = 3) +
 geom_segment(x = 1-0.01, y = 2, xend = 2+0.01, yend = 2, size = 3) +
 geom_segment(x = 2, y = 2, xend = 2, yend = 1, size = 3) +
 scale_x_continuous(limits = c(0, 3)) + 
 scale_y_continuous(limits = c(0, 3))

But again, this solution is not ideal because this value should change according to the size of the line and the scale of the figure you are showing.


EDIT: geom_path() connects the corners of the square without leaving blank spaces, but the problem persist in the point where the line meets it's origin:

 df <- data.frame(
       x = c(1, 1, 2, 2, 1),
       y = c(1, 2, 2, 1, 1)
       )

 ggplot(df, aes(x, y)) + 
       geom_path(size = 3, linejoin = "mitre") +
       scale_x_continuous(limits = c(0, 3)) +
       scale_y_continuous(limits = c(0, 3))



回答3:

The issue is now fixed by this update to ggplot2.



标签: r ggplot2