r-wasm / webr

The statistical language R compiled to WebAssembly via Emscripten, for use in web browsers and Node.
https://docs.r-wasm.org/webr/latest/
Other
805 stars 54 forks source link

Setting image height/width under new capture output #390

Closed coatless closed 3 months ago

coatless commented 3 months ago

With webR v0.3.0+, the graphics for shelter.captureR() were moved into a new .images slot.

However, it doesn't seem like there is a way to constrain image height and/or image width with a pre-call to webr::canvas() since the shelter makes its own call.

https://github.com/r-wasm/webr/blob/cc95727742fb9324bfb35fed78480ced9648be49/src/webR/webr-worker.ts#L683-L688

So, pre-calling the webr::canvas(width=, height=) before shelter.captureR() seems to have no effect. However, if I move the canvas call into the shelter evaluation, then the graphs are no longer stored inside of .images, but can be retrieved through reading messages after flushing, e.g. the old way from webR v0.2.x.

For a quick example, please consider the Capturing plots documentation entry with only a slight modification to webr::canvas(height=400, width=320):

<html>
  <head>
    <title>WebR Test Graph with Dimensions</title>
  </head>
  <body>
    <div id="plot-output"></div>
    <div>
      <pre><code id="out">Loading webR, please wait...</code></pre>
    </div>

    <script type="module">
      import { WebR } from 'https://webr.r-wasm.org/latest/webr.mjs';
      const webR = new WebR();
      await webR.init();

      const shelter = await new webR.Shelter();

      // <---
      // Attempt to constrain graphics output
      webR.evalRVoid('webr::canvas(width=320, height=400)');
      // ---->

      const capture = await shelter.captureR("demo(graphics)");
      capture.images.forEach((img) => {
        const canvas = document.createElement("canvas");
        // set canvas size
        canvas.width = img.width;
        canvas.height = img.height;

        // Draw image
        const ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0, img.width, img.height);
        document.getElementById("plot-output").appendChild(canvas);
      });

      shelter.purge();
    </script>
  </body>
</html>

Note this yields a series of canvas objects with a width and height of 1008 x 1008 instead of the desired 320 x 400 per image

Screenshot of the modified example test to set width and height of image generated

Thus, I think the .images slot contains img objects that do not respect the user supplied width and height values.

georgestagg commented 3 months ago

Hi James,

Try this:

<html>

<head>
  <title>WebR Test Graph with Dimensions</title>
</head>

<body>
  <div id="plot-output"></div>
  <div>
    <pre><code id="out">Loading webR, please wait...</code></pre>
  </div>

  <script type="module">
    import { WebR } from 'https://webr.r-wasm.org/latest/webr.mjs';
    const webR = new WebR();
    await webR.init();

    const shelter = await new webR.Shelter();

    // Capture graphics with custom width/height
    const capture = await shelter.captureR(`
        webr::canvas(width = 320, height = 400, capture = TRUE)
        .webr_cvs_id <- dev.cur()
        demo(graphics)
        invisible(dev.off(.webr_cvs_id))
    `);
    capture.images.forEach((img) => {
      const canvas = document.createElement("canvas");
      // set canvas size
      canvas.width = img.width;
      canvas.height = img.height;

      // Draw image
      const ctx = canvas.getContext("2d");
      ctx.drawImage(img, 0, 0, img.width, img.height);
      document.getElementById("plot-output").appendChild(canvas);
    });
    shelter.purge();
  </script>
</body>

</html>

If sufficient, I am happy for this to be added either in src/examples/plot-capture or explicitly in the docs.


I am also currently reconsidering the default width and height for the canvas device, perhaps setting it to look at a session-wide option like getOption("webr.fig.width", default = 504). That would avoid the dance of creating an extra device on the stack inside captureR().

What are your thoughts on such a scheme?

coatless commented 3 months ago

Excellent! Just needed to add the capture = TRUE parameter. Thinking out loud, I don't think this should need a closing statement of dev.off() right?

Regarding a session-wide option, I'm in favor of adding one; however, I do not believe that will be a panacea. There is a need to be able to set/customize image width and height on each call since there are already a few folks tweaking cell-level figure output in surprising ways... 😱

georgestagg commented 3 months ago

I don't think this should need a closing statement of dev.off() right?

It will work without dev.off(), but including it does no harm and stops the graphics stack from growing over time if there is a long-running session..

I believe there is a limit to the number of devices on the stack, so I would actually recommend adding it. I'll edit the example to add dev.off() now.


There is a need to be able to set/customize image width and height on each call

OK. I will keep thinking. Another option would be something like:

    const capture = await shelter.captureR("demo(graphics)",
     {
        captureGraphicsOptions: { width: 100, height: 200 },
     });

This requires thought to work well, but I needed to get 0.3.x out for other reasons. Hopefully, just opening another device will work for now. I do think we can improve this for the 0.4.x series, though.

coatless commented 3 months ago

@georgestagg I would enclose the dev.off() in invisible(), e.g. invisible(dev.off()), to suppress the print statement associated with the driver.

No worries regarding the image settings. Something to have on the roadmap for 0.4.z.

One trick that could also be used in the interim with the session option is potentially resetting before each call, e.g.

webR.evalRVoid(`
options("webr.fig.width" = 320)
options("webr.fig.height" = 400)
`)

const capture = await shelter.captureR("demo(graphics)");

However, this would only work if shelter.captureR() checked option contents on each call.

georgestagg commented 3 months ago

I would enclose the dev.off() in invisible()

Thanks, one should probably also make an attempt to close the right device, e.g.:

webr::canvas(width = 320, height = 400, capture = TRUE)
.webr_cvs_id <- dev.cur()
demo(graphics)
invisible(dev.off(.webr_cvs_id))