Title: JSON Graphics Device
Version: 0.1.0
Description: A graphics device that translates R plotting operations into JSON and streams them over a local connection to an external display application. The device acts as a pure recorder with no rendering dependencies; all rendering occurs in that application (e.g. a 'VS Code' extension or a web browser). Official display applications are available from the project homepage.
License: MIT + file LICENSE
Copyright: file inst/COPYRIGHTS
Encoding: UTF-8
RoxygenNote: 7.3.3
NeedsCompilation: yes
Suggests: callr, ggplot2, jsonlite, processx, testthat (≥ 3.0.0), withr
URL: https://github.com/grantmcdermott/jgd
BugReports: https://github.com/grantmcdermott/jgd/issues
Config/testthat/edition: 3
Packaged: 2026-04-29 04:44:11 UTC; gmcd
Author: Grant McDermott [aut, cre], Tatsuya Shima [aut], Dave Gamble [cph] (cJSON library in src/cjson/), cJSON contributors [cph] (cJSON library in src/cjson/)
Maintainer: Grant McDermott <contact@grantmcdermott.com>
Repository: CRAN
Date/Publication: 2026-04-29 19:00:07 UTC

jgd: JSON Graphics Device

Description

A graphics device that translates R plotting operations into JSON and streams them over a local connection to an external display application. The device acts as a pure recorder with no rendering dependencies; all rendering occurs in that application (e.g. a 'VS Code' extension or a web browser). Official display applications are available from the project homepage.

Author(s)

Maintainer: Grant McDermott contact@grantmcdermott.com

Authors:

Other contributors:

See Also

Useful links:


JSON Graphics Device

Description

Opens a graphics device that streams plot operations as JSON to an external renderer (e.g. VS Code extension or browser) over a Unix domain socket.

Usage

jgd(width = 8, height = 6, dpi = 96, socket = NULL)

Arguments

width

Device width in inches (default 8).

height

Device height in inches (default 6).

dpi

Resolution in dots per inch (default 96).

socket

Socket address for the rendering server. Supports URI formats (⁠tcp://host:port⁠, ⁠unix:///path/to/socket⁠) or raw Unix socket paths. If NULL (default), use the jgd.socket R option, falling back to the JGD_SOCKETenvironment variable. If JGD_SOCKET environment variable is also unset, the device discovers the socket via the discovery file.

Value

Invisible NULL. The device is opened as a side effect.

Displaying plots with jgd

It is important to note that jgd() does not display any plots; it only streams them (i.e., converts them to a format that a JSON renderer understands). To actually display your plots with jgd, you'll need an appropriate frontend. We provide two official renderers, both available for install from the project repository: https://github.com/grantmcdermott/jgd.

Users aren't limited to these two options. The jgd protocol is deliberately frontend-agnostic; you can render plots with any client that reads JSONL (JSON Lines). Again, please see the project repository for full documentation: https://github.com/grantmcdermott/jgd

Debugging

Set options(jgd.debug = TRUE) before opening the device to enable frame-level diagnostic output on stderr (via REprintf). This logs details about newPage, flush_frame, and poll_resize events, which is useful for diagnosing resize/replay issues.

Examples


# Requires a running renderer (e.g., VS Code extension or Deno server).
# See the "Displaying plots" section above.
library(jgd)
jgd()
plot(1:10)
lines(1:10, col = "red", lwd = 3)
hist(rnorm(1000), col = "steelblue")
dev.off()


Begin a drawing group (experimental)

Description

Emits a beginGroup operation into the drawing stream. All subsequent drawing operations until the matching jgd_end_group() are part of this group. The renderer may use the group's extension fields to apply effects to the group as a whole.

Usage

jgd_begin_group(ext = NULL)

Arguments

ext

A single JSON string with extension fields for this group, or NULL for a group without extension fields.

Value

Called for its side effect; returns NULL invisibly.

Lifecycle

Experimental. This API may change in future versions.


Discover a running jgd server

Description

Reads the jgd discovery file from the platform cache directory (⁠~/.cache/jgd⁠ on Linux, ⁠~/Library/Caches/jgd⁠ on macOS, ⁠%LOCALAPPDATA%/jgd⁠ on Windows) and returns its contents. This does not require an open jgd device — it simply reads the file that a running server has written.

Usage

jgd_discover()

Value

A named list with server_name (character), socket_path (character), pid (integer), and server_info (named character vector), or NULL if no discovery file is found.


End a drawing group (experimental)

Description

Emits an endGroup operation into the drawing stream, closing the most recently opened group from jgd_begin_group().

Usage

jgd_end_group()

Value

Called for its side effect; returns NULL invisibly.

Lifecycle

Experimental. This API may change in future versions.


Set extended graphics context (experimental)

Description

Sets extension fields that are included in every subsequent drawing operation's graphics context (gc.ext in the JSON protocol). This is an experimental, low-level API for injecting renderer-specific properties (e.g. blend modes, shadows, opacity) that go beyond R's standard graphics parameters.

Usage

jgd_ext(json = NULL)

Arguments

json

A single JSON string representing the extension object, or NULL to clear. The string must be valid JSON (validated on the C side via cJSON); an error is raised otherwise. Packages built on top of jgd (using e.g. jsonlite) should provide user-friendly wrappers.

Value

Called for its side effect; returns NULL invisibly.

Supported extension fields

The Deno reference server and VS Code renderer currently support:

Field Canvas2D property Example
blendMode globalCompositeOperation "multiply"
opacity globalAlpha 0.5
shadow.blur shadowBlur 10
shadow.color shadowColor "rgba(0,0,0,0.5)"
shadow.offsetX shadowOffsetX 5
shadow.offsetY shadowOffsetY 5
filter filter "blur(3px)"

Custom renderers may support additional fields. Unknown fields are silently ignored, so extensions are forward-compatible.

Design for extension packages

jgd_ext() is intentionally low-level — it accepts a raw JSON string. Higher-level packages built on top of jgd can provide user-friendly wrappers with proper argument checking, e.g.:

jgd_shadow = function(blur = 0, color = "black",
                      offsetX = 0, offsetY = 0) {
  jgd_ext(jsonlite::toJSON(
    list(shadow = list(blur = blur, color = color,
                       offsetX = offsetX, offsetY = offsetY)),
    auto_unbox = TRUE
  ))
}

jgd itself has no dependency on jsonlite or any serialization library; upstream packages choose their own.

Lifecycle

Experimental. This API may change in future versions.

See Also

with_jgd_ext(), jgd_frame_ext(), jgd_begin_group(), jgd_spec

Examples


jgd()

# Drop shadow (scoped -- automatically cleared after the block)
with_jgd_ext(
  '{"shadow":{"blur":15,"color":"rgba(0,0,0,0.5)","offsetX":5,"offsetY":5}}',
  plot(1:10, pch = 19, cex = 3, col = "steelblue")
)

# Semi-transparent overlay
with_jgd_ext('{"opacity":0.3}', {
  plot(1:10, pch = 19, cex = 5, col = "red")
})

# Manual set/clear
jgd_ext('{"blendMode":"multiply"}')
plot(1:10)
jgd_ext(NULL)


Set frame-level extension fields (experimental)

Description

Sets extension fields that are included once per frame in the JSON protocol (at the top level of the frame message, not per drawing operation). This is useful for frame-wide properties such as post-processing effects.

Usage

jgd_frame_ext(json = NULL)

Arguments

json

A single JSON string representing the extension object, or NULL or "" to clear. Non-empty strings must be valid JSON; an error is raised otherwise.

Value

Called for its side effect; returns NULL invisibly.

Lifecycle

Experimental. This API may change in future versions.


Get server information

Description

Returns metadata about the jgd server. When a jgd device is open and connected, returns the welcome message information with connected = TRUE. Otherwise, falls back to reading the discovery file and returns information with connected = FALSE. Returns NULL if no information is available from either source.

Usage

jgd_server_info()

Details

The discovery fallback applies regardless of whether the current device is a jgd device. This means jgd_server_info() can return a non-NULL result even when no jgd device is open, as long as a valid discovery file exists.

Value

A named list, or NULL.

When connected:

When not connected (discovery file fallback):


jgd JSONL Protocol Specification

Description

The jgd device communicates with a rendering server over JSONL (JSON Lines). Messages are exchanged over a persistent connection using one of three transport protocols: Unix domain sockets (Linux/macOS), Windows named pipes, or TCP.

This document specifies the wire protocol so that third-party servers can implement a compatible rendering backend.

Transport protocols

The client connects to the server using one of the following URI schemes:

Raw Unix socket paths (without a URI scheme) are also accepted.

Message format

All messages are single-line JSON objects terminated by ⁠\n⁠ (JSONL). Each message contains a "type" field identifying the message kind. Encoding is always UTF-8.

Receivers should ignore unknown top-level fields in any message (forward-compatible). Unknown "type" values should be silently discarded rather than treated as errors.

Coordinate system

All coordinates in drawing operations are in device pixels (i.e., inches * dpi). The origin ⁠(0, 0)⁠ is the top-left corner of the device surface. The X axis increases to the right and the Y axis increases downward.

Connection handshake

The welcome message is deferred: the server waits until it receives the first message from R before sending it. This avoids a race condition on Windows named pipes where writing before the first read completes can cause data loss.

R -> Server:  {"type":"ping"}
Server -> R:  {"type":"server_info", ...}

The server should also tolerate receiving a frame message before ping (e.g., if a future client skips the ping). The first received message of any type should trigger the deferred welcome.

Discovery file

The discovery file is an optional JSON file that allows the client to find the server without an explicit socket address. It is a hint for auto-connection only; the welcome message is the single source of truth.

Location (platform-specific):

Schema:

{
  "serverName": "jgd-http-server",
  "socketPath": "tcp://127.0.0.1:9000",
  "pid": 12345,
  "serverInfo": {
    "httpUrl": "http://127.0.0.1:8080/"
  }
}

Lifecycle:

server_info message

{
  "type": "server_info",
  "serverName": "jgd-http-server",
  "protocolVersion": 1,
  "transport": "unix",
  "serverInfo": {
    "httpUrl": "http://127.0.0.1:8080/"
  }
}

See jgd_server_info() for how the R client represents this data.

R-to-server messages

ping – Heartbeat; triggers the deferred welcome on first send.

{"type": "ping"}

frame – A complete or incremental set of drawing operations. See the Frame message section for the full schema.

metrics_request – Requests font metrics from the renderer.

{"type": "metrics_request", "id": 1, "kind": "strWidth",
 "str": "Hello",
 "gc": {"font": {"family": "sans", "face": 1,
                  "size": 12}}}
{"type": "metrics_request", "id": 2, "kind": "metricInfo",
 "c": 77,
 "gc": {"font": {"family": "sans", "face": 1,
                  "size": 12}}}

close – Signals device shutdown.

{"type": "close"}

Server-to-R messages

server_info – Welcome message (see above).

resize – Renderer viewport change.

{"type": "resize", "width": 800, "height": 600}

metrics_response – Font metrics from the renderer.

{"type": "metrics_response", "id": 1, "width": 48.5,
 "ascent": 10.2, "descent": 2.8}

Frame message

The frame message carries drawing operations from R to the server.

New plot example:

{
  "type": "frame",
  "incremental": false,
  "newPage": true,
  "plotNumber": 0,
  "ext": {},
  "plot": {
    "version": 1,
    "sessionId": "r-1234-1",
    "device": {
      "width": 768,
      "height": 576,
      "dpi": 96,
      "bg": "rgba(255,255,255,1)"
    },
    "ops": []
  }
}

(Minimal example; real frames typically start with a clip op.)

Historical resize replay example:

{
  "type": "frame",
  "incremental": false,
  "resizeReplay": true,
  "plotIndex": 0,
  "plot": { "..." }
}

Top-level fields:

plot object:

Color format

Colors are represented as CSS-style RGBA strings:

"rgba(R,G,B,A)"

Transparent or NA colors are represented as JSON null.

Graphics context

Most drawing operations include a "gc" object (exceptions are noted per operation):

{
  "col": "rgba(0,0,0,1)",
  "fill": null,
  "lwd": 1.0,
  "lty": [],
  "lend": "round",
  "ljoin": "round",
  "lmitre": 10.0,
  "font": {
    "family": "sans",
    "face": 1,
    "size": 12.0,
    "lineheight": 1.2
  },
  "ext": {}
}

Drawing operations

Each element of the ops array is a JSON object with an "op" field. Most drawing operations include a "gc" field (see the Graphics context section). Exceptions are noted per operation.

All coordinates are in device pixels with a top-left origin (see the Coordinate system section).

clip – Set the clipping rectangle. No gc.

{"op": "clip", "x0": 0, "y0": 0, "x1": 768, "y1": 576}

line – A single line segment.

{"op": "line", "x1": 100, "y1": 200,
 "x2": 300, "y2": 400, "gc": {}}

polyline – Connected line segments (not closed).

{"op": "polyline", "x": [1, 2, 3],
 "y": [4, 5, 6], "gc": {}}

polygon – Closed polygon (filled and/or stroked).

{"op": "polygon", "x": [1, 2, 3],
 "y": [4, 5, 6], "gc": {}}

rect – Rectangle.

{"op": "rect", "x0": 10, "y0": 20,
 "x1": 100, "y1": 80, "gc": {}}

circle – Circle.

{"op": "circle", "x": 50, "y": 50, "r": 25, "gc": {}}

text – Text string.

{"op": "text", "x": 100, "y": 200, "str": "Hello",
 "rot": 0, "hadj": 0.5, "gc": {}}

path – Complex path with subpaths and a fill rule.

{"op": "path", "winding": "nonzero",
 "subpaths": [[[10, 20], [30, 40], [50, 20]]],
 "gc": {}}

raster – Raster image. No gc.

{"op": "raster", "x": 0, "y": 576, "w": 100, "h": -80,
 "rot": 0, "interpolate": true,
 "pw": 200, "ph": 160,
 "data": "data:image/png;base64,..."}

beginGroup – Start a drawing group (experimental). No gc.

{"op": "beginGroup",
 "ext": {"filter": "blur(5px)", "opacity": 0.8}}

endGroup – End the most recently opened group. No gc, no fields other than "op".

{"op": "endGroup"}

Groups nest arbitrarily.

Resize protocol

The server receives resize messages from the renderer and forwards them to R. R replays the drawing at the new dimensions and sends back a frame message.

Normal resize flow:

Renderer -> Server:  {"type":"resize","width":800,
                      "height":600}
Server   -> R:       {"type":"resize","width":800,
                      "height":600}
R        -> Server:  {"type":"frame",
                      "resizeReplay":true,
                      "incremental":false,...}

History resize flow (replay a historical plot):

Renderer -> Server:  {"type":"resize","width":800,
                      "height":600,"plotIndex":2,
                      "sessionId":"r-1234-1"}
Server   -> R:       {"type":"resize","width":800,
                      "height":600,"plotIndex":2}
R        -> Server:  {"type":"frame",
                      "resizeReplay":true,
                      "plotIndex":2,
                      "incremental":false,...}

Note: The server strips sessionId before forwarding to R. History resizes are routed only to the R session that owns the target plot.

Resize deduplication:

Servers should deduplicate consecutive normal resizes with identical dimensions for each R session. However, if the previous resize was a plotIndex resize, the next normal resize at the same dimensions must NOT be deduplicated, because they target different contexts (historical snapshot vs. current plot).

Multiple R sessions

A server may accept connections from multiple R processes simultaneously. Each R connection has its own sessionId and independent state. Servers should:

Session ID management

The sessionId in frame messages identifies the R device instance. Servers should treat it as an opaque string. Do not parse it or make assumptions about its format.

Session ID reuse is unlikely but possible (e.g., after process restart). As a defensive measure, servers should disambiguate if a previously retired sessionId reappears, to prevent plotIndex resizes for old plots from reaching the new connection. The server's (possibly remapped) sessionId is what the renderer sees; plotIndex resizes use it for routing.

Connection lifecycle

Graceful close: The client sends {"type":"close"} on device shutdown. The server should forward this to renderers and clean up routing state for that session.

Ungraceful disconnect: If the connection drops without a close message (e.g., process crash), the server should detect the broken connection (EOF or socket error), clean up the session, and optionally notify renderers.

Incomplete lines: If a connection drops mid-line (no trailing ⁠\n⁠), the partial data should be discarded.

Extension fields

Extension fields (ext) appear at three levels:

All ext fields are free-form JSON objects. When unset, the field is omitted from the message (never sent as null). When set, it may contain any JSON object, including an empty {}. Servers should preserve and forward them to renderers without validation. Renderers should ignore unknown keys.

Extension fields are preserved across resize replays, so historical plot snapshots retain their ext data.

See jgd_ext(), jgd_frame_ext(), and jgd_begin_group() for the R API to set these fields.

Implementing a server

A minimal server implementation needs to:

  1. Listen on a Unix socket, named pipe, or TCP port.

  2. Accept R connections and read JSONL lines.

  3. Send a deferred server_info welcome after receiving the first message from R (not before).

  4. Forward frame messages to connected renderers.

  5. Forward resize messages from renderers to R.

  6. Handle metrics_request/metrics_response routing between R and the renderer. Clients handle their own timeouts; servers are not required to synthesize fallback responses.

  7. Forward close messages to renderers and clean up the session state (remove metrics routing entries, etc.).

Optional:

See Also

jgd(), jgd_server_info(), jgd_ext(), jgd_frame_ext(), jgd_begin_group()


Scoped extended graphics context (experimental)

Description

Temporarily sets extension fields for the duration of expr, then clears ext on exit (sets to NULL).

Usage

with_jgd_ext(json, expr)

Arguments

json

A single JSON string representing the extension object. Must be valid JSON; an error is raised otherwise. Unlike jgd_ext(), NULL is not accepted (use jgd_ext(NULL) to clear ext explicitly).

expr

Expression to evaluate with the extension active.

Value

The result of evaluating expr.

Lifecycle

Experimental. This API may change in future versions.


Scoped frame-level extension fields (experimental)

Description

Temporarily sets frame-level extension fields for the duration of expr, then clears them on exit.

Usage

with_jgd_frame_ext(json, expr)

Arguments

json

A single JSON string representing the extension object.

expr

Expression to evaluate with the frame extension active.

Value

The result of evaluating expr.

Lifecycle

Experimental. This API may change in future versions.


Scoped drawing group (experimental)

Description

Opens a drawing group with extension fields, evaluates expr, then closes the group on exit.

Usage

with_jgd_group(ext, expr)

Arguments

ext

A single JSON string with extension fields for this group, or NULL for a group without extension fields.

expr

Expression to evaluate within the group.

Value

The result of evaluating expr.

Lifecycle

Experimental. This API may change in future versions.