Skip to content

Commit

Permalink
Pass arguments to webr::canvas() graphics device during captureR() (
Browse files Browse the repository at this point in the history
#399)

* Allow captureGraphics to set canvas() arguments

Additionally move cleanup `dev.off()` statements to `finally` block
to ensure graphics device cleanup even on evaluation error.

* Update NEWS.md

* Update plotting.qmd
  • Loading branch information
georgestagg committed Apr 3, 2024
1 parent cc95727 commit a06b0bb
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 26 deletions.
10 changes: 10 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# webR (development version)

## New features

* The `captureGraphics` option in `EvalROptions` now allows the caller to set the arguments to be passed to the capturing `webr::canvas()` device.

## Breaking changes

## Bug Fixes

* When capturing graphics with `captureR()`, clean-up now occurs even when the evaluated R code throws an error. This avoids leaking graphics devices on the device stack.

# webR 0.3.1

Hotfix release to manage incompatible WebAssembly binary R packages due to ABI changes in Emscripten.
Expand Down
13 changes: 13 additions & 0 deletions src/docs/plotting.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,19 @@ In the following example, a set of demo plots are captured and then displayed on
</html>
```

Arguments for the capturing `webr::canvas()` graphics device that's used during evaluation, such as setting a custom width or height, can be included as part of the optional [`EvalROptions`](api/js/interfaces/WebRChan.EvalROptions.md) argument to `captureR()`:

``` javascript
const shelter = await new webR.Shelter();
const capture = await shelter.captureR("hist(rnorm(1000))", {
captureGraphics: {
width: 504,
height: 252,
bg: "cornsilk",
}
});
```

## Plotting from the console

The `Console` class includes callbacks that are used for handling image rendering. This example builds off the [interactive webR REPL Console](examples.qmd#creating-an-interactive-webr-repl-console). In addition to the console, there is a `<canvas>` element to which plots will be drawn. The callbacks `canvasImage` and `canvasNewPage` are used to draw plots.
Expand Down
11 changes: 10 additions & 1 deletion src/webR/webr-chan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,17 @@ export interface EvalROptions {
captureConditions?: boolean;
/**
* Should a new canvas graphics device configured to capture plots be started?
* Either a boolean value, or an object with properties corresponding to
* `webr::canvas()` graphics device arguments.
* Default: `true`.
*/
captureGraphics?: boolean;
captureGraphics?: boolean | {
width: number;
height: number;
pointsize?: number;
bg?: string;
capture?: true;
};
/**
* Should the code automatically print output as if it were written at an R console?
* Default: `false`.
Expand Down
59 changes: 34 additions & 25 deletions src/webR/webr-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -646,32 +646,33 @@ function captureR(expr: string | RObject, options: EvalROptions = {}): {
output: RList,
images: ImageBitmap[],
} {
const _options: Required<EvalROptions> = Object.assign(
{
env: objs.globalEnv,
captureStreams: true,
captureConditions: true,
captureGraphics: typeof OffscreenCanvas !== 'undefined',
withAutoprint: false,
throwJsException: true,
withHandlers: true,
},
replaceInObject(options, isWebRPayloadPtr, (t: WebRPayloadPtr) =>
RObject.wrap(t.obj.ptr)
)
);

const prot = { n: 0 };
try {
const _options: Required<EvalROptions> = Object.assign(
{
env: objs.globalEnv,
captureStreams: true,
captureConditions: true,
captureGraphics: typeof OffscreenCanvas !== 'undefined',
withAutoprint: false,
throwJsException: true,
withHandlers: true,
},
replaceInObject(options, isWebRPayloadPtr, (t: WebRPayloadPtr) =>
RObject.wrap(t.obj.ptr)
)
);
const devEnvObj = new REnvironment({});
protectInc(devEnvObj, prot);

try {
const envObj = new REnvironment(_options.env);
protectInc(envObj, prot);
if (envObj.type() !== 'environment') {
throw new Error('Attempted to evaluate R code with invalid environment object');
}

// Start a capturing canvas graphics device, if required
const devEnvObj = new REnvironment({});
protectInc(devEnvObj, prot);
if (_options.captureGraphics) {
if (typeof OffscreenCanvas === 'undefined') {
throw new Error(
Expand All @@ -680,11 +681,17 @@ function captureR(expr: string | RObject, options: EvalROptions = {}): {
);
}

// User supplied canvas arguments, if any. Default: `capture = TRUE`
devEnvObj.bind('canvas_options', Object.assign({
capture: true
}, _options.captureGraphics));

parseEvalBare(`{
old_dev <- dev.cur()
webr::canvas(capture = TRUE)
do.call(webr::canvas, canvas_options)
new_dev <- dev.cur()
old_cache <- webr::canvas_cache()
plots <- numeric()
}`, devEnvObj);
}

Expand Down Expand Up @@ -739,13 +746,6 @@ function captureR(expr: string | RObject, options: EvalROptions = {}): {
images = plots.toArray().map((idx) => {
return Module.webr.canvas[idx!].offscreen.transferToImageBitmap();
});

// Close the device and destroy newly created canvas cache entries
parseEvalBare(`{
dev.off(new_dev)
dev.set(old_dev)
webr::canvas_destroy(plots)
}`, devEnvObj);
}

// Build the capture object to be returned to the caller
Expand All @@ -755,6 +755,15 @@ function captureR(expr: string | RObject, options: EvalROptions = {}): {
images,
};
} finally {
// Close the device and destroy newly created canvas cache entries
const newDev = devEnvObj.get('new_dev');
if (_options.captureGraphics && newDev.type() !== "null") {
parseEvalBare(`{
dev.off(new_dev)
dev.set(old_dev)
webr::canvas_destroy(plots)
}`, devEnvObj);
}
unprotect(prot.n);
}
}
Expand Down

0 comments on commit a06b0bb

Please sign in to comment.