feat(plot): addjgd graphics device integration#1706
Conversation
Embed the JGD (JSON Graphics Device) VS Code extension directly into vscode-R so users only need the `jgd` R package for interactive, Canvas2D-rendered plots with history, navigation, and PNG/SVG export. Add `r.plot.backend` enum setting (auto/standard/httpgd/jgd) with backward compatibility for existing `r.plot.useHttpgd` configurations. The JGD socket server starts eagerly on activation when the backend is set to "jgd", passing JGD_SOCKET to R terminals via env vars. Closes REditorSupport#1679 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In auto mode, the JGD socket server starts eagerly and R-side hooks try jgd > httpgd > standard based on package availability. Users who install the jgd R package get interactive plots with zero config. r.plot.useHttpgd: true still forces httpgd for backward compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
I haven't had a chance to use jgd, will let @renkun-ken review it. Anyway, great work! |
👍
The IMHO the handling of setting |
|
No handling = no tinkering (touching, deleting, etc.) of setting
This actually breaks forward compatibility. What I intend to do: My default {
"r.bracketedPaste": true,
"r.plot.useHttpgd": true,
"r.plot.backend": "auto",
"r.rterm.linux": "/usr/local/bin/radian",
"r.rterm.option": [
"--no-save",
"--no-restore"
],
"r.workspaceViewer.showObjectSize": true,
}This will lead to the following behaviour:
|
|
@grantmcdermott I am just realising that my detailed comments at #1679 have caused more confusion than clarity. Sorry about that. |
eitsupi
left a comment
There was a problem hiding this comment.
I haven't looked at the details, but based on the description, I think it would be better to do it this way.
- Delete old setting
r.plot.useHttpgd - In auto mode, the priority is set to httpgd, then jgd.
This means that in older environments where httpgd is installed, httpgd will continue to be used automatically.
I'm also wondering if we could remove the svglite backend for simplification.
At the very least, I don't think it should be a hard dependency of the sess package.
I believe jgd is lighter than svglite.
|
Thanks for the quick comments. I don't want to respond to everything until @renkun-ken has had a chance to review. But just quickly:
|
If it is implemented as @eitsupi suggests, IMHO everything will continue to work as usual. |
|
My understanding is that only httpgd offers features like thumbnail display for past plots, so it makes sense to prioritize httpgd over jgd in environments where httpgd is installed. Another point that caught my attention is that the tests don't seem to have been ported from the jgd repository. |
|
Looks user-created R session does not respect |
The attach script for user-created R sessions now passes use_httpgd, use_jgd, and JGD_SOCKET to sess::connect(), matching the managed terminal behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The attach script for user-created R sessions now passes use_httpgd, use_jgd, and JGD_SOCKET to sess::connect(), matching the managed terminal behavior. Also fixes terminal test to handle the new SESS_PLOT_BACKEND env var and config default resolution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Thanks. Should be fixed now.
I think these only touch the Deno server, right? Porting them to VS Code is out-of-scope IMO. |
I meant these files. |
svglite usage is already guarded with requireNamespace() checks, so it doesn't need to be a hard dependency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A good catch. I'll port those (second one might be a bit trickier given we changed some things from the original extension, but it should work...) |
Port unit tests from the standalone JGD extension, adapted to the project's mocha/assert test style. Also moves svglite from Imports to Suggests in sess since its usage is already guarded with requireNamespace(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
if From the perspective of current capabilities: httpgd > jgd > svglite > png From the perspective of hard dependencies, footprints, things to be loaded into the user session: png > jgd > svglite > httpgd From a balanced view of both perspectives as well as native extension support, jgd is a good choice I guess? From user-side, if loading a ton of hard dependencies is not a big deal, choosing the currently most capable installed backend might make more sense. From the status quo, I guess a good proportion of existing users use httpgd as a suggested approach to view graphics, with their Considering all these, I think it is fair to make jgd > httpgd as we don't want to introduce a heavier dependency for user workflow by default. The only thing that might be a bit drawback is that do we think jgd has reached a level of stability that could be released/set by default if installed? |
|
In my opinion, if httpgd is removed from CRAN again (which is likely to happen in the not-too-distant future), leaving the |
|
Sometimes when vscode is restarted, |
Thanks @renkun-ken. I think this should be fixed with 26f3bc5. (More details: grantmcdermott/jgd#63) |
remotes::install_local() defaults to dependencies=NA, which installs only Depends/Imports/LinkingTo. After svglite was moved from Imports to Suggests (04ebd7d), the build stopped installing it, so the plot_latest handler fell back to png and the session test's strict svglite assertion failed on CI. Pass dependencies=TRUE to the build's install_local call, matching R CMD check semantics so optional (Suggests) functionality is exercised. Also branch the test assertion on svglite availability so it stays correct whether or not svglite is present.
|
Coming back to this: The three CI tests were failing b/c the runner wasn't installing @renkun-ken @randy3k Can either of you trigger the workflows so that we can confirm? P.S. The tradeoff is now that the CI runs will take slightly longer, since they will install all of the |
Now that the build installs Suggests (dependencies=TRUE), jgd is present on CI runners. The plot test left r.plot.backend unset, so resolveBackend returned 'auto', which prefers jgd when installed — the r.standardPlot webview was never created and the spy timed out. Pin r.plot.backend to 'standard' in the test's config stub so the test exercises a deterministic backend regardless of which optional plot packages are installed.
testthat pulls ~26 transitive packages into the sess Suggests closure, all installed on every build now that install_local uses dependencies=TRUE. tinytest depends only on parallel and utils (both base/recommended), so the migration removes that toolchain while keeping the unit tests. Convert tests/testthat/test-ipc.R to inst/tinytest/test-ipc.R. Because tinytest runs files as flat scripts (no per-test isolation), wrap each former test_that() block in local() so on.exit() state restoration of .sess_env still fires between blocks. Move the socket round-trip test last and guard its environment-sensitive setup in tryCatch, skipping via return() (exit_file does not halt inside local()) when unix sockets are unavailable, so it cannot mask or abort the other blocks. Replace the tests/testthat.R harness with tests/tinytest.R.
|
Hi Grant, Thank you for the excellent work and sorry for the late reply. Obviously I am not very familar with the code for jgd. I have spawned multiple AI agents to review the code. Please review the following comments (and dismiss if you that they are not important / false alarm). Anything below this line was written jointly with Gemini and Claude. Draft Reply to PR #1706Hi @grantmcdermott, Thanks for this — really exciting to see JGD integrated directly. The architecture is clean, the plot history state machine is well thought out (especially the I went through the review above and agree with most of the points raised. Here's my consolidated take with some additional context and a couple of extra findings. Should-fix1. SVG attribute escaping+1 to what was flagged above. // Currently:
let s = ' stroke="' + gc.col + '"';
return ' fill="' + gc.fill + '"';
// Should be:
let s = ' stroke="' + svgEsc(String(gc.col)) + '"';
return ' fill="' + svgEsc(String(gc.fill)) + '"';Same for 2. Large PNG export can crash the webviewIn the PNG export handler: const base64 = btoa(String.fromCharCode(...new Uint8Array(reader.result)));The spread operator on a large function uint8ToBase64(bytes) {
let binary = '';
const chunk = 8192;
for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
}
return btoa(binary);
}Or alternatively use 3. Use a secure temp directory for the Unix socketThe random token ( const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jgd-'));
this.socketPath = path.join(tempDir, 'jgd.sock');Remember to clean up the directory during 4. Add a Content-Security-Policy to the webviewThe <meta http-equiv="Content-Security-Policy"
content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob:;">For what it's worth, 5. Set the panel iconOne-liner for UI consistency with the other plot viewers — add after this.panel.iconPath = new UriIcon('graph');Nice-to-have (can be follow-ups)6. Changing
|
Apply the should-fix items from @randy3k's review of REditorSupport#1706: - Escape gc.col/gc.fill, font-family, text fill, and the raster href through svgEsc() in the SVG export builders; previously only text content was escaped, leaving these attributes injectable. - Encode large PNG exports to base64 in chunks (uint8ToBase64) instead of String.fromCharCode(...bytes), which overflows the call stack at large sizes/DPI. - Place the Unix domain socket in a private 0o700 mkdtemp directory and remove it on stop(), matching the IPC pipe handling from REditorSupport#1705. - Add a Content-Security-Policy meta tag to the JGD webview. - Set the panel icon (UriIcon 'graph') for parity with the httpgd and standard plot viewers.
|
I'm using the grantmcdermott/jgd@ae3283b and the latest PR build. Also, I usually use self-managed R terminals (arf) running in tmux windows. Here's my operation:
|
After a VS Code window reload, the JGD socket server starts on a new socket path and the attach script updates JGD_SOCKET accordingly. But a jgd device opened before the reload remains bound to the old, dead socket: jgd::jgd() only reads JGD_SOCKET at device-creation time, so the stale device never reconnects and subsequent plots silently render to nowhere (reported by @renkun-ken for self-managed arf/tmux sessions). In register_hooks(), when the jgd backend is active, detect an existing jgd device, record its current plot, close it, and reopen jgd::jgd() so it binds to the current socket — replaying the recorded plot so it reappears after reload. No-ops when no jgd device is open. Bump sess to 3.0.1 so the attach script reinstalls the corrected package for sessions that already have 3.0.0.
|
This is great, thanks both. I believe that I've address both sets of feedback. Everything look good on my side. But, again, please trigger the CI when you get a chance to confirm that I haven't introduced some inadvertent bugs. @randy3k Those were all helpful ideas and I've actioned items 1-5. I agree with your agents' assessment that 6-8 are add-ons that should only really be address later, so I left those out. @renkun-ken It took me a bit of back and forth, but I've reproduced your bug locally and have tested a local fix that resolves the issue in c3c6c7b. Specifically, P.S. I ended up going ahead with the |
The previous commit bumped sess to 3.0.1 to force needs_install on reattach, but a test asserts sess DESCRIPTION version equals the base extension version in package.json (currently 3.0.0-rc.0 -> 3.0.0). The standalone bump broke that invariant on all three test runners. Revert sess to 3.0.0. The reconnect fix still ships with this branch's release; version lockstep with the extension is left to the maintainers' release process rather than bumped independently here.
|
One minor test was failing b/c of a |
Would like to mention that for Point 5. If a session is re-attached after detach due to, say, a third party extension gets restarted, "R Plot: Show viewers" actually never worked even in the current official release. I think R plot is currently not session based. It would be better to make it be aware of which R session it attaches to. |
|
Thanks for the quick fix. It works now. 👍 |
|
Great. Are we good to merge, then (perhaps after @randy3k's AI review)? Two closing(?) notes from my side:
|
|
From my usage in the recent months, I think this PR is good enough to merge. Let's see if @randy3k has more review? |
|
let's merge it then~ |
Apply the should-fix items from @randy3k's review of #1706: - Escape gc.col/gc.fill, font-family, text fill, and the raster href through svgEsc() in the SVG export builders; previously only text content was escaped, leaving these attributes injectable. - Encode large PNG exports to base64 in chunks (uint8ToBase64) instead of String.fromCharCode(...bytes), which overflows the call stack at large sizes/DPI. - Place the Unix domain socket in a private 0o700 mkdtemp directory and remove it on stop(), matching the IPC pipe handling from #1705. - Add a Content-Security-Policy meta tag to the JGD webview. - Set the panel icon (UriIcon 'graph') for parity with the httpgd and standard plot viewers.
|
@grantmcdermott When R is launched,
|
|
🎉 @Fred-Wu Good catch. I'll address that in a follow-up up. Let me know if anything else crops up. P.S. I received an email with a reply from @chmzs, saying that no plot was appearing. My first thought is you haven't updated the |
Here's a natural English translation of your message: I applied the update right away. At first, I thought it was an issue with
|
|
Thanks @chmzs. Good to hear you resolved it. I certainly agree that we should update the documentation and was planning to edit the plotting wiki page after this PR landed. But FWIW I am unable to reproduce your error locally. Setting
Perhaps you need to update EDIT: I do think this example probably motivates a warning message (i.e., if the user tries to set |
When r.plot.backend explicitly selects jgd or httpgd (or the legacy r.plot.useHttpgd is true) but the package is not installed, register_hooks silently fell back to the standard viewer. The standard viewer has no plot-cycling UI, so users saw a degraded experience with no explanation (several reports on REditorSupport#1706). Emit a warning from register_hooks naming the requested backend and the remedy, but only when a specific backend was explicitly requested. The auto mode (use_jgd && use_httpgd) still degrades silently by design.
* fix(plot): avoid leaking plot_backend into the global environment profile.R is sourced as the user's R_PROFILE_USER, so the top-level plot_backend assignment created a stray variable in the user's global environment that persisted after startup (reported by @Fred-Wu). Wrap the sess::connect() setup in local() so the temporary plot_backend binding stays scoped, matching the pattern used by the .Rprofile block above it. * feat(plot): warn when a requested plot backend is unavailable When r.plot.backend explicitly selects jgd or httpgd (or the legacy r.plot.useHttpgd is true) but the package is not installed, register_hooks silently fell back to the standard viewer. The standard viewer has no plot-cycling UI, so users saw a degraded experience with no explanation (several reports on #1706). Emit a warning from register_hooks naming the requested backend and the remedy, but only when a specific backend was explicitly requested. The auto mode (use_jgd && use_httpgd) still degrades silently by design.



Closes #1679
Embeds the jgd VS Code extension code directly into
vscode-R. Users only need thejgdR package installed on their system---and there's a graceful fallback otherwise---with no separate extension required.Details
Adds a new
r.plot.backendenum setting (auto|standard|httpgd|jgd). Default isauto, which tries the best available backend at runtime:r.plot.useHttpgdistrue→httpgd(ensures backwards compatability)jgd(if installed) →httpgd(if installed) →standardThis means users who
install.packages("jgd")get interactive plots with zero configuration. (Note: I didn't want to presume that thejgdR package becomes a hard dependency, but am happy to adjust if others feel differently.)When
jgdis active:JGD_SOCKETandSESS_PLOT_BACKENDenv vars are passed to R terminalssess::register_hooks()setsjgd::jgd()as the default deviceMain files
I've added three new files to
src/plotViewer/(ported from from the currently standalonejgdVSC extension):jgdPlotHistory.ts— multi-session plot history with eviction and resize-after-delete protectionjgdSocketServer.ts— socket server managing concurrent R sessions and JSONL routingjgdViewer.ts—JgdManager+JgdViewer(Canvas2D webview with font metrics pipeline)UX
JGD-specific settings:
r.plot.jgd.historyLimitr.plot.jgd.exportWidthr.plot.jgd.exportHeightr.plot.jgd.exportDpi.R-side changes:
sess::connect()andsess::register_hooks()gain ause_jgdparameter. The hook priority isjgd>httpgd>standard, gated on package availability andJGD_SOCKETbeing set.Backward compatibility:
r.plot.useHttpgdis deprecated but still respected. Whenr.plot.backendisauto(default) andr.plot.useHttpgdistrue, httpgd is forced. Existing Docker images and devcontainer configs will continue to work without changes.Screenshots