grantmcdermott / tinyplot

Lightweight extension of the base R graphics system
https://grantmcdermott.com/tinyplot
Apache License 2.0
261 stars 8 forks source link

Adding `abline`s to faceted plots #236

Closed grantmcdermott closed 2 weeks ago

grantmcdermott commented 1 month ago

Adding an abline to a (simple) tinyplot works much the same as base plot().

library(tinyplot)
library(marginaleffects)

fit = lm(Sepal.Length ~ (Sepal.Width + Petal.Length + Petal.Width) * Species, iris)

mfx = avg_slopes(fit, variables = c("Sepal.Width", "Petal.Length", "Petal.Width"))
with(
  mfx,
  plt(
    x = term, y = estimate, ymin = conf.low, ymax = conf.high,
    type = "errorbar", pch = 19
  )
)
abline(h = 0, lty = 2, col = "hotpink")

So far so good. But unfortunately this approach doesn't work for faceted plots:

mfx2 = avg_slopes(fit, variables = c("Sepal.Width", "Petal.Length", "Petal.Width"), by = "Species")
with(
  mfx2,
  plt(
    x = term, y = estimate, ymin = conf.low, ymax = conf.high,
    type = "errorbar", pch = 19,
    facet = Species, facet.args = list(bg = "gray90"), frame = FALSE
  )
)
abline(h = 0, lty = 2, col = "hotpink")

I don't see a good way to support the standard way of adding of ablines to faceted plots post hoc. (Too many complications.) But we could potentially support an abline argument that would draw the required line during each facet, and similarly for single facet plots too. Something like:

tinyplot(..., abline = list(h = 0))

Thoughts

vincentarelbundock commented 1 month ago

Is this too weird?

plt(type=type_abline(), add=TRUE)

Maybe...

zeileis commented 1 month ago

I wouldn't add another specialized argument like abline to tinyplot().

Either we should go via the route that Vincent suggested, i.e., a separate tinyplot(..., add = TRUE).

And/or, alternatively, we could add a panel = ... argument that can draw stuff within each facet of a plot. The crucial question is then which information this panel function has access to and what arguments it can take. Also, this might have to control what is drawn before and what after the type_draw.

grantmcdermott commented 1 month ago

Is this too weird? plt(type=type_abline(), add=TRUE)

Not too weird. But it will require some adjustment to the logic here https://github.com/grantmcdermott/tinyplot/blob/068b431302e08a55afedae9bff40d7cf03937db6/R/tinyplot.R#L589-L592 since we we need to restore the previous faceting par settings even if the user hasn't explicitly specified the facets. (Basically, you'd have to make an additional adjustment for the abline type.)

(digression)

Digressing slightly, on a quasi-related note I've been wondering whether it makes sense to support an update_tinyplot() / update_plt() function. The idea is that, similar to update.<model> we would capture the preceding tinyplot call and save it in an environment somewhere. Then users could simply pass the specific arguments that need to be added/changed without having to retype everything again. These updated arguments would be spliced into the saved previous call and fed to tinyplot(..., add = TRUE)

tinyplot(Temp ~ Day | Month, data = airquality, facet = "by")
# This next line...
update_tinyplot(type = "lm")
# ... would be equivalent to:
# tinyplot(Temp ~ Day | Month, data = airquality, facet = "by", type = "lm", add = TRUE)

I can raise this update_tinyplot() idea in a separate issue if either of you think that it is worth discussing further...

(end digression)

And/or, alternatively, we could add a panel = ... argument that can draw stuff within each facet of a plot. The crucial question is then which information this panel function has access to and what arguments it can take. Also, this might have to control what is drawn before and what after the type_draw.

I'm open to this too, although I worry that "panel" might get confusing with the existing "facet" arg. OTOH your comments makes me wonder if we couldn't support a "draw" argument in facet.args. Something like:

tinyplot(Temp ~ Day | Month, data = airquality, facet = "by", facet.args = list(draw = abline(h = 0)))

?

grantmcdermott commented 1 month ago

Sorry, one more thought. Whatever we decide here would probably pertain to other additively "drawn" (function-based?) elements that require an existing plotting window to be open, like rug.

vincentarelbundock commented 1 month ago

Don't want you to wait for my feedback here. TBH, I don't really have a view on there issues

zeileis commented 1 month ago

I was indeed thinking about something that is function-based. And for abline(h = 0) and the like, this function would not need to have access to any actual data. But for rug() and other decorations like this, the panel function would also need access to the data.

Recording the entire plot and adding something sounds cool but would likely be a more challenging extension, I guess.