How can I use a non-standard graphics device interactively?
Asked Answered
H

1

9

The problem

When running R outside of RStudio, plots will by default be shown in a pop-up window, e.g. provided by the quartz device on macOS, the X11 device on Unix, the windows device on Windows.

A special feature of these 'interactive' devices is that manually resizing the plot window causes the plots to be redrawn to fit the new dimensions. This feature is really useful!

A downside of the default interactive devices is that they're relatively slow. The {ragg} package provides alternative graphics devices like ragg::agg_png(), which render noticeably faster than the default devices, and often look better too. Unfortunately, these devices aren't responsive to resizing - you have to manually specify the dimensions of the plot before rendering.

In RStudio it's possible to use {ragg} as a backend interactively. In this case, the plot preview is rendered by {ragg}, and resizing the pane causes the preview to be re-rendered. I assume this is powered by RStudio magic behind the scenes, not by R.

What I want

I would like to achieve automatic resizing with a custom device outside of RStudio. I want my plots to be rendered/drawn by {ragg}, to appear in a floating window, and to be re-drawn when I resize this window.

What I've tried

I was hoping the default quartz device would allow me to specify a backend to draw the plot itself, but I don't think this is possible.

I was able to create an imitation of an interactive device powered by {ragg} using the system's default png viewer - the major downside of this approach is that there's no redrawing of the plot when I resize the window:

options(device = function() {
  file <- tempfile("last_plot_", fileext = ".png")

  ragg::agg_png(file, height = 480 * 5, width = 480 * 5, scaling = 5)

  withr::defer(
    {
      dev.off()
      browseURL(file)
    }, 
    sys.frame(1)
  )
})
Homeopathist answered 10/4, 2024 at 10:22 Comment(6)
I’ve never used it but the ‘httpgd’ package might be a building block to achieve this.Consideration
Thanks! This looks way better than anything I've got currently, so will definitely give it a try.Homeopathist
Update: been playing with this a bit but struggling to get anything working reliably enough to not be frustrating in day-to-day use. Will post an edit if that changes.Homeopathist
github.com/rstudio/rstudio/blob/…Gorged
Mike FC's rstudio::conf(2022) talk on R graphics has been helping a bit with this. Tl;dr is that X11 (available for mac but not until you install it) is really fast.Homeopathist
Did you see this coolbutuseless.github.io/2019/10/03/…? Haven't studied it, but it looks it allows to create your custom device.Delacourt
S
3

My current setup is with VS Code, but I infer from your question that you want to use a more lightweight editor/REPL. The R Editor Support extension integrates nicely with httpgd, see this support page. They offer this code snippet for your .Rprofile:

if (interactive() && Sys.getenv("TERM_PROGRAM") == "vscode") {
  if ("httpgd" %in% .packages(all.available = TRUE)) {
    options(vsc.plot = FALSE)
    options(device = function(...) {
      httpgd::hgd(silent = TRUE)
      .vsc.browser(httpgd::hgd_url(history = FALSE), viewer = "Beside")
    })
  }
}

Which, when changed to the following, gives me a nice clickable (in iTerm at least) URL:

if (interactive() && Sys.getenv("TERM_PROGRAM") == "iTerm.app") {
  if (requireNamespace("httpgd")) {
    options(device = function(...) {
      httpgd::hgd(silent = TRUE)
      message("httpgd running at: ", httpgd::hgd_url(history = FALSE))
    })
  }
}

Sample session:

> plot(1:10, 1:10*3)
httpgd running at: http://127.0.0.1:58930/live?history=FALSE&token=zsHkpKzo
> plot(1:10, 1:10*3)
> httpgd::hgd_close()
null device
          1
> plot(1:10, 1:10*3)
httpgd running at: http://127.0.0.1:58948/live?history=FALSE&token=CEQRWBS1

ps: The radian REPL is a nice substitute for the bare R REPL also relied on by the VS Code extension. Worth having a look at.

Stinkpot answered 23/4, 2024 at 9:7 Comment(10)
It’s worth noting that the check "httpgd" %in% .packages(all.available = TRUE) is really slow (several seconds on my machine with many packages installed), and is also completely unnecessary: just try loading the package. See https://mcmap.net/q/1319536/-how-to-check-if-a-package-is-installed-and-if-not-install-and-load-it-duplicate.Consideration
Or just requireNamespace("httpg", quietly = TRUE)?Homeopathist
This is a good answer - thank you! Unfortunately, while useful, it doesn't give the specific information I'm looking for regarding R devices, internals and so on. But I'm sure many will find this helpful.Homeopathist
@KonradRudolph - thanks for the hint; my machine (renv w/ 161 packages, MBP M3 Pro) runs it in 0.001s, so I didn't think to change it.Stinkpot
@Homeopathist - Ah, I think I see now - you'd want something like httpgd, but with the unigd backend swapped out and ragg plugged in. Since the httpgd/unigd decoupling isn't quite complete yet (e.g.) I think you'd have to do a PR with them...Stinkpot
@KonradRudolph I was in a bit of a hurry last time and couldn't fix it; I tend to avoid loading any namespaces for my personal dev tooling, so I propose doing it with a try on find.package. My machine prints out 0 elapsed time for that code block on startup, could you benchmark?Stinkpot
@Stinkpot requireNamespace("httpgd", quietly = TRUE): 1.2s — if ("httpgd" %in% .packages(all.available = TRUE)) loadNamespace("httpgd"): 8.3s — if (! inherits(try(find.package("httpgd"), silent = TRUE), "try-error")) loadNamespace("httpgd") is much faster than that (1.5s) but, as mentioned, it’s also completely unnecessary (doubly so: you don’t need the try(), but you also don’t need the rest). (All benchmarks performed via time Rscript -e '…'; the numbers above are from when the package exists, but for non-existing packages the numbers are highly similar, just minimally lower.)Consideration
Oh: these are with a cold system; repeating the measures makes them much faster due to NFS caching, but the relative differences are pretty similar.Consideration
@Stinkpot In addition, I don’t understand what you mean by “I tend to avoid loading any namespaces for my personal dev tooling”. Could you explain?Consideration
Sorry, I meant attach a namespace. Indeed, I had forgotten that requireNamespace does not attach it. I'll change it to that.Stinkpot

© 2022 - 2025 — McMap. All rights reserved.