glyph

I built glyph so I could make interactive graphs straight from my R scripts, without writing a lot of extra code just to make a plot interactive. It is for anyone who wants a simpler grammar for interactive graphics in R. glyph is in its early stages, and new features are being added regularly.

Installation

install.packages("glyph")

# Development version
# # remotes::install_github("josh45-source/glyph")
install.packages("glyph", repos = c('https://josh45-source.r-universe.dev', 'https://cloud.r-project.org'))

A first plot

library(glyph)

glyph(mtcars, x = wt, y = mpg) |>
  mark_point(color = cyl, size = hp)

glyph() takes the data and the mappings shared by every mark. Mappings are bare column names, or expressions such as log(wt) or factor(cyl). Each mark_*() adds a layer and can add mappings of its own.

The result is a specification: a plain R list that describes the data, the marks, the scales, the interactions and the animation. Nothing is drawn until it is printed or rendered. At that point compile() turns the spec into JSON and a D3 renderer draws it in the browser as an htmlwidget.

Drawing a plot

Plots print automatically. Type the name of a plot at the console, or leave it as the last value of a pipeline, and it is drawn in the viewer. In R Markdown, Quarto and vignettes it is drawn in the document. Layouts built with compose() work the same way. You only need render() when you want to set the width and height yourself:

p <- glyph(mtcars, x = wt, y = mpg) |> mark_point()

p                                    # drawn at the default size
render(p, width = 600, height = 400) # drawn at 600 x 400

For a text description of a plot or a layout, use summary().

Interaction

Tooltips, hover effects, zoom and brushing are part of the spec. They are added with interact():

glyph(mtcars, x = wt, y = mpg) |>
  mark_point(color = cyl) |>
  interact(
    tooltip = "{cyl} cyl, {mpg} mpg at {wt} tons",
    zoom = TRUE,
    hover = "enlarge"
  )

A tooltip can be TRUE, which lists every mapped column, or a template with column names in braces. Named selections, including a clickable legend, come from selection().

Animation

animate() sets how marks enter and how they move between states.

glyph(mtcars, x = cyl, y = mpg) |>
  mark_bar() |>
  animate(transition = "slide", stagger = 80, easing = "bounce") |>
  set_scale("y", zero = TRUE, label = "Miles per gallon")

With by, the plot steps through groups of the data and the marks move from one group to the next. A play/pause control is added to the plot.

glyph(mtcars, x = wt, y = mpg) |>
  mark_point(size = hp, color = cyl) |>
  animate(by = gear, transition = "morph", duration = 800)

Layout

Several plots can share one widget. With linked_selections = TRUE, brushing points in one panel highlights the same rows in the others.

p1 <- glyph(mtcars, x = wt, y = mpg) |>
  mark_point(color = cyl) |>
  interact(brush = TRUE)
p2 <- glyph(mtcars, x = hp, y = mpg) |>
  mark_point(color = cyl) |>
  interact(brush = TRUE)

compose(p1, p2, type = "hstack", linked_selections = TRUE)

facet() splits one plot into small multiples. marginals() adds a histogram, density curve or boxplot along each axis. inset() places a second plot in a corner of the first.

glyph(mtcars, x = wt, y = mpg) |>
  mark_point(color = cyl) |>
  marginals(x = "histogram", y = "density")

Themes

Colours, fonts and spacing come from a small set of tokens: font, background, foreground, accent, grid and padding. Set a few and the rest are derived. A dark background gets light text and a grid colour that still shows up against it.

glyph(mtcars, x = wt, y = mpg) |>
  mark_point() |>
  theme_tokens(font = "Inter", bg = "#1a1a2e") |>
  titles(title = "Motor Trend Cars", subtitle = "Weight vs fuel efficiency")

There are five presets: "light", "dark", "minimal", "publication" and "presentation". Use one with theme_tokens(preset = "dark").

Inspecting and exporting

Because the plot is data, you can look inside it before anything is drawn. summary() lists what a spec contains. compile() resolves the mappings and scales and returns the JSON that the renderer reads. to_vegalite() converts a spec to Vega-Lite JSON for use in the Vega Editor, Altair or JavaScript.

export() writes a plot to a file:

export(p, "plot.html")  # interactive widget
export(p, "plot.json")  # the compiled spec

A single self-contained HTML file needs pandoc, which RStudio includes. Without pandoc, export() writes the HTML with a plot_files folder next to it. Static SVG, PNG and PDF export are not implemented yet.

Rendering

There is one renderer: an HTML widget drawn with D3.js and SVG. compile(engine = ...) accepts "auto" or "html" and rejects anything else. The spec does not depend on the renderer, so other backends can be added later without changing how plots are written. See ARCHITECTURE.md for how the pieces fit together.

Status

glyph is at version 0.2. It has point, line, bar, area, text, rule, ribbon, link and polygon marks, and boxplot, violin, sina and histogram marks that compute their statistics from the data. It supports tooltips, hover, click, zoom, brushing and crossfilter; entrance and keyframe animation; compose(), facet(), marginals() and inset(); theme tokens and presets; JSON and Vega-Lite export; and Shiny apps, through glyphOutput() and renderGlyph(), with brush, click and legend selections sent to the server as row numbers.

Large plots are limited by the browser, not by R. A 100,000-point scatter plot takes about 0.1s to compile and under half a second to reach the Viewer, but every point is an SVG element, so the browser slows down as points are added. Point plots respond to hover, brushing and zoom in under 0.1s up to about 10,000 points. At 15,000 points a step takes about 0.1s, at 50,000 about half a second, and at 100,000 about a second, and the plot takes about 2s to draw. Zooming a 100,000-point line takes about 0.8s per step. Boxplots and histograms send only their summaries, so at 100,000 rows they draw in under 0.1s and respond within a frame. Exporting a 100,000-point plot to a single HTML file takes several seconds, most of it in pandoc. These numbers come from Chromium on a 4-core machine; the details are in #8.

Not done yet:

Contributing

glyph is early, and feedback on the design is as useful as code. Bug reports, questions and ideas are welcome in the issue tracker.

Acknowledgements

glyph's design draws directly on the ideas and prior art of others:

This package also bundles D3.js (Copyright Mike Bostock, ISC License); see inst/COPYRIGHTS for the full notice.

License

MIT