-
Notifications
You must be signed in to change notification settings - Fork 20
Description
Adapted from my original comment in the "bubble'" PR here.
Relevant for #31 (#343), #210 and #455, since we'd need to enable multi-part legends irrespective of whether we decide to support (nested and cross) grouping by other graphical parameters, let alone settle on an API for doing so.
Background (dual legends)
One way to create dual (n = 2) legends in vanilla base plot is by using an inset of 0.5 plus an adjustment term. Assuming we don't want a gap between the two legends (i.e., they can be directly adjacent), we have:
- For the 'top' legend this adjustment term is zero, since the 0.5 inset already forces the (bottom of the) legend rectangle relative to the middle of the plot
- For the 'bottom' legend, the adjustment term is the top height of the rectangle, adjusted in terms of normalized plot coordinates (
"npc"), since we want to push this second legend down by its entire height.
A simple example to illustrate:
plot(0:10, type = 'n')
abline(h = 5, lty = 2)
legend("bottomright", "a", pch=1, title= "Legend 1",inset = c(0, 0.5))
l2 = legend("bottomright", c("b", 'c'), pch=1, title= "Legend 2", plot = FALSE)
# l2h = l2$rect$h ## use top, rather than h
l2t = l2$rect$top
l2h = grconvertY(l2t, to = 'npc')
legend("bottomright", c("b", 'c'), pch=1, title= "Legend 2",inset = c(0, 0.5-l2h))Created on 2025-09-05 with reprex v2.1.1
The problem... and an alternative workaround
Unfortunately, try as I might, I have not been able to make this simple approach work for tinyplot. I honestly don't know why; it could be because of recordGraphics adjustments, our bespoke margin tweaks, or something else. Update: After some further experimentation, I think it's because of (overlayed/recursive) recordGraphics calls. If correct, fixing this would require some bespoke logic in the underlying draw_legend/tinylegend functions.
Luckily, there's another approach that we can use for dual axes. Specifically, we use the same 0.5 inset trick, but coming from opposite edges of the plot (e.g., "bottomright" vs "topright"). In this case, the solution is even simpler since you don't need to calculate an additional adjustment term:
plot(0:10, type = 'n')
abline(h = 5, lty = 2)
legend("bottomright", "a", pch=1, title= "Legend 1",inset = c(0, 0.5))
legend("topright", c("b", 'c'), pch=1, title= "Legend 2", inset = c(0, 0.5))Created on 2025-09-05 with reprex v2.1.1
The good news is that this latter approach does work with tinyplot and this is what my PR here does underneath the hood.
So what (n > 2 legends)?
The second approach is simpler and gives exactly the same result for this dual legend case. So, why worry? The problem is that it can't scale for multi-legends (> dual legends), b/c it fully depends on the "opposite edges" logic... and thus limits you to two (opposite) legends.
If we'd like to support, say, three-part legends down the road, then we'd have to figure out why the first approach isn't working. And then you could do something in spirit of:
plot(0:10, type = 'n')
abline(h = 5, lty = 2)
l1 = legend("bottomright", "a", pch=1, title= "Legend 1", plot = FALSE)
l2 = legend("bottomright", c("b", 'c'), pch=1, title= "Legend 2", plot = FALSE)
l3 = legend("bottomright", "d", pch=2, title= "Legend 3", plot = FALSE)
l1h = grconvertY(l1$rect$top, to = 'npc')
l2h = grconvertY(l2$rect$top, to = 'npc')
l3h = grconvertY(l3$rect$top, to = 'npc')
lh_tot = l1h + l2h + l3h
adj = 0.5+lh_tot/2
l1 = legend("bottomright", "a", pch=1, title= "Legend 1", inset = c(0, adj-l1h))
l2 = legend("bottomright", c("b", 'c'), pch=1, title= "Legend 2", inset = c(0, adj-l1h-l2h))
l3 = legend("bottomright", "d", pch=2, title= "Legend 3", inset = c(0, adj-l1h-l2h-l3h))Created on 2025-09-05 with reprex v2.1.1
Until then, we're stuck with dual legends at a maximum.


