GithubHelp home page GithubHelp logo

Plot leaks into knitr document about knitr HOT 9 CLOSED

dmurdoch avatar dmurdoch commented on June 13, 2024 1
Plot leaks into knitr document

from knitr.

Comments (9)

dmurdoch avatar dmurdoch commented on June 13, 2024 3

Thanks, I agree with your analysis. So it's really user error in the document: I shouldn't expect dev.off() to return me to the previous state. I should do something like

prev <- dev.prev()
dev.off()
dev.set(prev)

and indeed when I do that, things are fine. (I don't know why dev.off() doesn't already work that way. But I expect it's been this way for a very long time.)

from knitr.

dmurdoch avatar dmurdoch commented on June 13, 2024 1

Maybe there's a way for knitr to make that plot invisible to evaluate at the start of the run? I think the plot is being captured here:
https://github.com/r-lib/evaluate/blob/00192335c338ac83af200dcee35c56d807b8b28b/R/eval.R#L165-L175

from knitr.

cderv avatar cderv commented on June 13, 2024 1

TL;DR;

This happens because dev.off() is called in a chunk, and it will set dev.cur() to the next device, which happens to be the one opened before knitting. evaluate will then capture plot in this device, which includes the existing one.

Not sure how to prevent this, without a dev.off() for any existing device before knitting.

Details about investigations

Definitely something at the evaluate level

> plot(1, main = "Not in the document")
> res = evaluate::evaluate("dev.new()\ndev.off()\nplot.new()\nplot(1:10, 1:10)", new_device = FALSE)
> str(res, max.level = 2)
List of 9
 $ :List of 1
  ..$ src: chr "dev.new()\n"
  ..- attr(*, "class")= chr "source"
 $ : chr "NULL\n"
 $ :List of 1
  ..$ src: chr "dev.off()\n"
  ..- attr(*, "class")= chr "source"
 $ : chr "RStudioGD \n        2 \n"
 $ :List of 1
  ..$ src: chr "plot.new()\n"
  ..- attr(*, "class")= chr "source"
 $ :List of 2
  ..$ :Dotted pair list of 8
  ..$ : raw [1:35992] 01 00 00 00 ...
  .. ..- attr(*, "pkgName")= chr "graphics"
  ..- attr(*, "engineVersion")= int 16
  ..- attr(*, "pid")= int 53184
  ..- attr(*, "Rversion")=Classes 'R_system_version', 'package_version', 'numeric_version'  hidden list of 1
  ..- attr(*, "load")= chr(0) 
  ..- attr(*, "attach")= chr(0) 
  ..- attr(*, "class")= chr "recordedplot"
 $ :List of 2
  ..$ :Dotted pair list of 2
  ..$ : raw [1:35992] 01 00 00 00 ...
  .. ..- attr(*, "pkgName")= chr "graphics"
  ..- attr(*, "engineVersion")= int 16
  ..- attr(*, "pid")= int 53184
  ..- attr(*, "Rversion")=Classes 'R_system_version', 'package_version', 'numeric_version'  hidden list of 1
  ..- attr(*, "load")= chr(0) 
  ..- attr(*, "attach")= chr(0) 
  ..- attr(*, "class")= chr "recordedplot"
 $ :List of 1
  ..$ src: chr "plot(1:10, 1:10)"
  ..- attr(*, "class")= chr "source"
 $ :List of 2
  ..$ :Dotted pair list of 8
  ..$ : raw [1:35992] 01 00 00 00 ...
  .. ..- attr(*, "pkgName")= chr "graphics"
  ..- attr(*, "engineVersion")= int 16
  ..- attr(*, "pid")= int 53184
  ..- attr(*, "Rversion")=Classes 'R_system_version', 'package_version', 'numeric_version'  hidden list of 1
  ..- attr(*, "load")= chr(0) 
  ..- attr(*, "attach")= chr(0) 
  ..- attr(*, "class")= chr "recordedplot"

It could even be simplified to this

> plot(1, main = "Not in the document")
> res = evaluate::evaluate("# hello", new_device = FALSE)
> str(res, max.level = 2)
List of 2
 $ :List of 1
  ..$ src: chr "# hello"
  ..- attr(*, "class")= chr "source"
 $ :List of 2
  ..$ :Dotted pair list of 8
  ..$ : raw [1:35992] 01 00 00 00 ...
  .. ..- attr(*, "pkgName")= chr "graphics"
  ..- attr(*, "engineVersion")= int 16
  ..- attr(*, "pid")= int 43180
  ..- attr(*, "Rversion")=Classes 'R_system_version', 'package_version', 'numeric_version'  hidden list of 1
  ..- attr(*, "load")= chr(0) 
  ..- attr(*, "attach")= chr(0) 
  ..- attr(*, "class")= chr "recordedplot"

it does not come from the hooks IMO but from handle output: https://github.com/r-lib/evaluate/blob/00192335c338ac83af200dcee35c56d807b8b28b/R/eval.R#L151-L158

This handle_output() is also called on last item (https://github.com/r-lib/evaluate/blob/00192335c338ac83af200dcee35c56d807b8b28b/R/eval.R#L264-L267). evaluate will always capture the content of the graphic device when evaluating the last element. I believe this is expected feature as doc says:

It stores the final result, whether or not it should be visible, and the contents of the current graphics device.

I believe this is what happens:

  • plot() in console opens a new device
  • knitr does open another new device per chunk (unless global.device is set, but this is FALSE by default) and set it current.
  • Then it will evaluate the chunk content using evaluate
  • evaluate will parse each line of code, and evaluate each on its own, each time registering the dev.cur() device, and capturing its content if it has not changed (source here)

So we can simulate with

# no device at start
dev.list()
#> NULL
# creating a plot in console opens the device 
plot(1, main = "Not in the document")
dev.list()
#> RStudioGD       png 
#>        2         3 
dev.cur()
#> RStudioGD 
#>        2

# knitr save the opened device
dv0 = dev.cur(); dv0
#> RStudioGD 
#>        2 
# and creates a new one 
grDevices::png()
dv = dev.cur(); dv
#> png 
#>   4 

# then code is evaluated. 
dev.new() # creating a new device 
dev.cur() # this modifies the dev cur
#> windows 
#>      5 
dev.off() # calling from the chunk 
#> RStudioGD 
#>         2 
dev.cur()
#> RStudioGD 
#>       2 

And we see here that after dev.off(), the current device has been reset to the one from the created plot before knitting.
And so evaluate will capture the plot already in that device, skipping the device opened by knitr.

dev.off() shutdown active device, and setup the current to be the next, where here we would have like dev.prev() instead. From ?dev.off():

dev.off shuts down the specified (by default the current) device. If the current device is shut down and any other devices are open, the next open device is made current.

Issue here is that, calling dev.off() inside a knitr chunk will set the device to the next one, and if one has been opened before knitting happens in will be that one.

I am not sure how we can prevent this for happening exactly, without closing any existing device before rendering. 🤔

from knitr.

cderv avatar cderv commented on June 13, 2024

Thanks for the report. I can obviously reproduce.

This remind me of something similar we've dealt with for child documents already

This was fixed by ff3d767 and amended by 58fc726

So it is probably related to same part of the code

knitr/R/block.R

Lines 205 to 216 in 42f6b3a

# open a device to record plots if not using a global device or no device is
# open, and close this device if we don't want to use a global device
if (!opts_knit$get('global.device') || is.null(dev.list())) {
# reset current device if any is open (#2166)
if (!is.null(dev.list())) {
dv0 = dev.cur(); on.exit(dev.set(dv0), add = TRUE)
}
chunk_device(options, keep != 'none', tmp.fig)
dv = dev.cur()
if (!opts_knit$get('global.device')) on.exit(dev.off(dv), add = TRUE)
showtext(options) # showtext support
}

but it seems to be different here... I'll look into that.

from knitr.

cderv avatar cderv commented on June 13, 2024

Interestingly, more minimal reprex of this

---
title: "Untitled"
output: 
  html_document:
    keep_md: true
---

```{r}
dev.new()
dev.off()
# hello
```

image

So I am thinking something in evaluate rather than knitr directly. This is what we results in evaluate::evaluate() when debugging interactively

> str(res, max.level = 2)
List of 6
 $ :List of 1
  ..$ src: chr "dev.new()\n"
  ..- attr(*, "class")= chr "source"
 $ : chr "debug: options$message\n"
 $ :List of 1
  ..$ src: chr "dev.off()\n"
  ..- attr(*, "class")= chr "source"
 $ : chr "RStudioGD \n        2 \n"
 $ :List of 1
  ..$ src: chr "# hello"
  ..- attr(*, "class")= chr "source"
 $ :List of 2
  ..$ :Dotted pair list of 8
  ..$ : raw [1:35992] 01 00 00 00 ...
  .. ..- attr(*, "pkgName")= chr "graphics"
  ..- attr(*, "engineVersion")= int 16
  ..- attr(*, "pid")= int 51232
  ..- attr(*, "Rversion")=Classes 'R_system_version', 'package_version', 'numeric_version'  hidden list of 1
  ..- attr(*, "load")= chr(0) 
  ..- attr(*, "attach")= chr(0) 
  ..- attr(*, "class")= chr "recordedplot"

So it seems that something evaluated after dev.off will trigger the recordedplot to be among value returned by evaluate::evaluate()

from knitr.

cderv avatar cderv commented on June 13, 2024

Thanks ! I was just looking into those hooks. It seems indeed directly related.

from knitr.

yihui avatar yihui commented on June 13, 2024

Thanks @dmurdoch for the report and @cderv for the investigation! I just pushed a fix in the evaluate package, so it should be safe to use dev.off() in knitr code chunks now.

from knitr.

cderv avatar cderv commented on June 13, 2024

Oh good solution @yihui ! Thank you !

from knitr.

github-actions avatar github-actions commented on June 13, 2024

This old thread has been automatically locked. If you think you have found something related to this, please open a new issue by following the issue guide (https://yihui.org/issue/), and link to this old issue if necessary.

from knitr.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.