9

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,

enter image description here

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

enter image description here

Community
  • 1
  • 1
Dan
  • 11,370
  • 4
  • 43
  • 68
  • 1
    I'm not seeing the notches (macOS, ggplot2 3.1.0). Your sessionInfo() might be relevant. – joran Dec 13 '18 at 18:10
  • I see them on win10, ggplot2 3.1.0. – qdread Dec 13 '18 at 18:12
  • @joran I've added my `sessionInfo`. – Dan Dec 13 '18 at 18:12
  • Are you using any `theme()` arguments to draw that plot? Your capture doesn't look like default aesthetics; maybe there's something about line ends hidden somewhere in there? – phalteman Dec 13 '18 at 18:19
  • @phalteman Yeah, that's weird. I restarted my session and reran. While I get a slightly different figure (which I'll add to the question), the problem persists. – Dan Dec 13 '18 at 18:23
  • 1
    For what it's worth, the difference in theme aesthetics between the first & second plot might be due to the presence of the cowplot package? It has been discussed [here](https://stackoverflow.com/questions/41096293/cowplot-made-ggplot2-theme-disappear-how-to-see-current-ggplot2-theme-and-res). – Z.Lin Dec 14 '18 at 13:20
  • @Z.Lin Yes, I think you're right: I *did* have `cowplot` loaded in the first instance. Thanks for pointing that out, as I was a bit baffled by how that happened. – Dan Dec 14 '18 at 13:26

3 Answers3

6

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:

lineend values

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)

plot after changing line end

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

plot after changing line join

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.

Z.Lin
  • 28,055
  • 6
  • 54
  • 94
  • Thanks for the detailed, insightful response! So, for the `ggplot2` team to fix this, they would just need to change `"butt"` to `"mitre"` for `linejoin` in their code? – Dan Dec 14 '18 at 11:58
  • 3
    @Lyngbakr In this case it would be `"square"` for `lineend` and `"mitre"` for `linejoin`. I don't think `linejoin` is currently specified at all, so its value is dependent on the root viewport's setting in the selected graphics device. The ggplot2 team may have reasons for choosing "butt" over "square" for line end (e.g. more precise ending point for line elements), but it may be worth considering exposing `lineend` / `linejoin` as parameters in a `geom_XXX()` layer, even if `lineend = "butt"` remains the default... – Z.Lin Dec 14 '18 at 13:16
4

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))

square with blank spaces

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))

square without blank spaces

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))

enter image description here

David Jorquera
  • 2,046
  • 12
  • 35
  • 1
    Thanks for this. I think you've hit the nail on the head with regards to the cause. A similar workaround is using `geom_polygon`. The corners aren't perfect, but are at least consistent. For example, try `df_poly <- data.frame(x = c(0, 1, 1, 0), y = c(0, 0, 1, 1))` with `ggplot(df_poly) + geom_polygon(aes(x = x, y = y), fill = NA, colour = "blue", size = 3) `. Since this works okay, I'm not sure why the same method isn't being used to plot the border in `geom_tile`. – Dan Dec 13 '18 at 19:29
  • 3
    I think `theme(line=element_line(lineend="square"))` should create square corners (by making each corner point a square of the same size as the linewidth), but for some reason it does not seem to effect the starting point of a path - which is presumably the top left corner in the case of the `geom_tile` examples. Maybe a bug in ggplot2? – Andrew Gustar Dec 13 '18 at 19:37
  • 1
    @AndrewGustar That's good to know. Perhaps I'll create an issue on github and see what the Guardians of the Tidyverse™ have to say. – Dan Dec 13 '18 at 19:44
1

The issue is now fixed by this update to ggplot2.

Dan
  • 11,370
  • 4
  • 43
  • 68