Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wishlist: Will tinyplot ever support returning a plot object as ggplot2 does? #121

Open
arnejohannesholmin opened this issue Feb 11, 2024 · 11 comments

Comments

@arnejohannesholmin
Copy link

I am looking for a tiny alternative to ggplot2 that allows saving a plot object that can be modified (like using the "+" operator of ggplot2) and plotted (like using print() on a ggplot object) later.

@harrelfe
Copy link

This raises a side question: instead of basing the new package on base graphics, basing it on grid (as ggplot2 does) may solve that problem, and others.

@vincentarelbundock
Copy link
Collaborator

Out of curiosity, what would be the benefits of that? I imagine it would involve a complete re-write of the current package, so it would be nice to get a sense of the tangible gains before considering.

@arnejohannesholmin
Copy link
Author

Out of curiosity, what would be the benefits of that? I imagine it would involve a complete re-write of the current package, so it would be nice to get a sense of the tangible gains before considering.

Without going into detail, we (https://github.com/StoXProject) have one framework package responsible for producing output files based on results from other packages. We currently rely on ggplot objects but would like to explore lightweight alternatives.

@grantmcdermott
Copy link
Owner

grantmcdermott commented Feb 12, 2024

To add to what's already been said: Unlike the grid-based plotting packages (ggplot2, lattice, etc.), the tinyplot package relies on the original graphics system. Unfortunately, graphics draws directly on the graphics device and does not yield a (first-class) R object for re-use/saving later.

(To follow up on Vincent's point, tinyplot has already hitched itself to graphics—this was a deliberate decision right at the start of the project, despite the tradeoffs—and I'm afraid we won't be switching to grid now, or any time in the future.)

Nonetheless, here are a few additional points that may be be useful:

  1. While graphics doesn't provide the same object-orientated approach of grid, you can still recall/redraw earlier plots using recordPlot() and replayPlot() (see: docs). Quick example:
library(tinyplot)
plt(Sepal.Length ~ Petal.Length | Species, iris)
p = recordPlot()
dev.off() ## optional: remove plot to show that we're really redrawing it below
replayPlot(p)
  1. We do support tinyplot(..., add = TRUE). So if it's just a matter of adding layers to an existing plot, then that should be relatively easy to do. (Same for adding "vanilla" graphics elements like points(), lines(), etc.)
  2. If these options don't work for, but you still need lightweight alternative to ggplot2, then definitely give lattice a look. In a sense, it's even more lightweight than tinyplot since it comes bundled with the "Recommended" group of packages in the normal base R installation. I personally think that lattice is underrated; it's an incredibly versatile and powerful plotting package. (The downside is that it is very function heavy and you need to learn a lot of different calls for different plot types.)

@zeileis
Copy link
Collaborator

zeileis commented Feb 12, 2024

💯

One further point: An additional handy aspect of grid is that you can place a grid viewport in all sorts of positions within a graphics device (e.g., for facets but also as panels within a tree etc.). But using gridBase you can also draw base graphics plots within such a grid display.

@b-rodrigues
Copy link

I was going to open a very similar issue. In my case, I would find it useful because it would make working with {targets} pipelines easier. But I didn’t know about recordPlot(), so maybe this is not an issue. I’ll have to try and see how/what works.

@grantmcdermott
Copy link
Owner

@b-rodrigues Doesn't targets require that you explicitly make your plot exporting/saving step a separate function? (At least, I remember this being a minor annoyance for me when I tried it with ggplot2 a while back.) In that case, couldn't you just do something like:

save_mtcars_plot = function(filename, width, height) {
  png(filename, width = width, height = height) 
  plt(mpg ~ wt | am, mtcars)
  dev.off()
}

?

But your question does prompt to wonder if we should perhaps add a file-writing argument to tinyplot()/plt(), which would make the appropriate calls to, say, png() and dev,off() internally. Let me raise it as a separate issue.

@b-rodrigues
Copy link

You can define a target that simply saves the ggplot into a variable, say x, and then do tar_read(x) to read the plot, so no need to save it to pdf for example

@arnejohannesholmin
Copy link
Author

@grantmcdermott Thanks for the useful info about the recordPlot() and replayPlot(), which I did not know about, and the return object from lattice functions. Surely, many of our users are used to the style of ggplot2, so using lattice would either require some persuasion or some work to make plots look like ggplot2. I will stay tuned on the developments of tinyplot and explore whether recordPlot() can be used for our needs.

@harrelfe
Copy link

I make frequent use of ggplot2 objects being created inside a function that creates other output, then returns the mixed output as a big list(). It helps to defer graphics rendering until later in a Quarto report, for example.

@grantmcdermott
Copy link
Owner

Thinking about this some more, I wonder whether we couldn't just call recordPlot() internally, right at the end of the tinyplot() function? Some quick testing suggests that this adds minimal overhead i.t.o. of execution time. But I might be missing some side-effects.

Mock-up example on a fairly large dataset.

library(tinyplot)

myplot = function(...) {
  tinyplot(...)
  grDevices::recordPlot()
}

data("diamonds", package = "ggplot2")

dp = myplot(
  price ~ depth | carat, diamonds,
  palette = "agSunset",
  pch = 16
)

We still can't get away from displaying the initial plot on the user's device (unless we did something clever with a dummy device file location at the same time...) But the "saved" plot is easily re-callable.

dev.off()
#> null device 
#>           1
dp

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants