oblicubes icon indicating copy to clipboard operation
oblicubes copied to clipboard

Separate legend for `z`

Open EvoLandEco opened this issue 6 months ago • 6 comments

Thank you for the nice extension. I would like to map fill to a different variable, how can I show legends for both fill and z?

I tried something like:

ggplot(df3d, aes(x, -y, z = z, fill = density)) +
  geom_oblicuboids(angle=45, show.legend = TRUE) +
  coord_fixed() +
  scale_fill_nord(palette="lumina",name = "Density", discrete=F) +
  labs(
    title = "3D Oblique Plot: Height = Mahalanobis distance, Color = Density",
    x     = "Grid X index",
    y     = "Grid Y index"
  ) +
  theme(
    panel.grid = element_blank(),
    panel.background = element_blank()
  )

It throws a warning:

Warning message:
`show.legend` must be a logical vector. 

Could you please elaborate on how to set a logical vector in such case?

EvoLandEco avatar Jun 09 '25 13:06 EvoLandEco

Well, perhaps it is very difficult to show a legend for z after all. Please feel free to close this issue if it is not useful.

EvoLandEco avatar Jun 09 '25 13:06 EvoLandEco

  • Do you know of any examples of a custom geom creating a legend for an aesthetic not in base {ggplot2}?
  • Probably not impossible to do but I'm not sure exactly how using the {ggplot2} "guide" system built-in to {ggplot2} or in extension packages like {legendry}
  • #14 is an open issue for a separate z axis which also seems like it would be very difficult to implement

trevorld avatar Jun 09 '25 14:06 trevorld

  • You can use guide_custom() to add arbitrary new z legend elements but you'd need to manually construct your own "grob" for the legend
  • Currently this package does not provide a separate "scale" for the z aesthetic so I don't think you can use normal "guides" with it
  • The z aesthetic for geom_oblicubes() currently needs to be integer valued and often (usually?) will not correspond to the "raw" values you actually care about (e.g. elevation) which is why in the examples we use fill = raw instead of fill = z

trevorld avatar Jun 09 '25 16:06 trevorld

I am not quite familiar with custom geom. At least in my case, mapping fill to a different variable is helpful to interpret my data.

I experimented a bit:

library(grid)

breaks <- c(1, 2, 3, 4)

legend_df <- data.frame(
  x = 1, 
  y = 2 * seq_along(breaks) - 5,
  z = breaks,
  fill = nord:::nord_pal("lumina")(length(breaks))
)

cube_grobs <- lapply(split(legend_df, seq_along(breaks)), function(d) {
  oblicubesGrob(
    x     = d$x, 
    y     = d$y,
    z     = d$z,
    width = unit(0.8, "lines"),
    xo    = unit(0, "lines"),
    yo    = unit(0, "lines"),
    angle = 45,
    scale = 0.5,
    gp    = gpar(fill = d$fill, col = "black")
  )
})

legend_grob <- grid::gTree(children = do.call(gList, cube_grobs), cl = "cube_legend")

p + guides(
  height = guide_custom(
    grob   = legend_grob,
    title  = "Height\n(M-dist)",
    width  = unit(1, "cm"),
    height = unit(2, "cm")
  )
) +
  theme(legend.position = "right")

This is what I got:

Image

Not sure how to get the heights right, they look truncated.

EvoLandEco avatar Jun 09 '25 17:06 EvoLandEco

What about creating stacks of cubes? Something like:

legend_grob <- oblicubesGrob(
  x = 1,
  y = rep.int(seq.int(1, by = 2, length.out = 4L), 1:4),
  z = sequence(1:4, 1),
  gp = gpar(fill = rep.int(palette.colors(4L), 1:4), col = "black"),
  width = unit(0.8, "lines"),
  x0 = unit(0, "lines"),
  y0 = unit(0, "lines"),
  angle = 45,
  scale = 0.5
)

trevorld avatar Jun 09 '25 20:06 trevorld

That's indeed a good idea, stacks clearly indicate differences in magnitudes.

With your suggestion, I tried also the oblicuboids geom:

cube_legend <- oblicuboidsGrob(
  x     = 1,
  y     = rep.int(seq.int(1, by = 2, length.out = 4L), 1:4),
  z     = sequence(1:4, 1) / 4,
  gp    = gpar(fill = "gray", col = "black"),
  width = unit(0.8, "lines"),
  xo    = unit(0, "lines"),
  yo    = unit(0, "lines"),
  angle = 45,
  scale = 0.5
)

stack_ys <- seq.int(1, by = 2, length.out = 4L)
labels  <- as.character(seq.int(1, by = 1, length.out = 4L) / 4)

text_grobs <- mapply(function(lab, ypos) {
  textGrob(
    label = lab,
    x     = unit(3, "lines"),
    y     = unit(ypos, "lines"),
    just  = "left",
    gp    = gpar(fontsize = 10)
  )
}, lab = labels, ypos = stack_ys, SIMPLIFY = FALSE)

cube_stacks <- grobTree(
  cube_legend,
  do.call(gList, text_grobs)
)

p + guides(
  height = guide_custom(
    grob   = cube_stacks,
    title  = "Height\n(M-dist)",
    width  = unit(2, "cm"),
    height = unit(4, "cm")
  )
) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "right")

Yields:

Image

Still a bit difficult for eyeballing. But I guess that's the problem of '3D'.

Will try to wrap it up to something like cube_z_legend() so we can call it by p + cube_z_legend()

EvoLandEco avatar Jun 10 '25 06:06 EvoLandEco