glyph vs ggplot2: Side-by-Side Comparison

1. Basic Scatterplot

ggplot2:

ggplot(mtcars, aes(x = wt, y = mpg, color = factor(cyl))) +
  geom_point(size = 3) +
  scale_color_brewer(palette = "Set2") +
  labs(
    title = "Motor Trend Cars",
    x = "Weight (1000 lbs)",
    y = "Miles per Gallon",
    color = "Cylinders"
  ) +
  theme_minimal()

glyph:

glyph(mtcars, x = wt, y = mpg) |>
  mark_point(color = cyl, style = list(size = 6)) |>
  scale_color("Set2") |>
  scale("x", label = "Weight (1000 lbs)") |>
  scale("y", label = "Miles per Gallon") |>
  titles(title = "Motor Trend Cars") |>
  theme_tokens(preset = "minimal")

What changed: No aes(), no factor() coercion, no + operator. Pipeline reads left-to-right with |>. Color scale is one function call.


2. Interactive Exploration

ggplot2 + plotly:

library(plotly)

p <- ggplot(mtcars, aes(x = wt, y = mpg, color = factor(cyl),
                         text = paste("Car:", rownames(mtcars),
                                      "<br>MPG:", mpg,
                                      "<br>Weight:", wt))) +
  geom_point(size = 3)

ggplotly(p, tooltip = "text")
# Note: loses theme, some formatting; no brush-to-filter

glyph:

glyph(mtcars, x = wt, y = mpg) |>
  mark_point(color = cyl) |>
  interact(
    tooltip = "Car: {.rownames}\nMPG: {mpg}\nWeight: {wt}",
    zoom = TRUE,
    brush = TRUE,
    hover = "enlarge"
  )
# Theme, formatting, all interactions preserved — no conversion step

What changed: No lossy ggplotly() conversion. Interactions are part of the spec, not a post-hoc wrapper. Tooltip template is a simple string, not a paste() call inside aes().


3. Animated Bar Chart

ggplot2 + gganimate:

library(gganimate)

p <- ggplot(gapminder, aes(x = continent, y = lifeExp, fill = continent)) +
  geom_col(stat = "summary", fun = "mean") +
  transition_states(year, transition_length = 2, state_length = 1) +
  labs(title = "Year: {closest_state}") +
  theme_minimal() +
  enter_grow() +
  ease_aes("bounce-out")

animate(p, fps = 20, width = 600, height = 400)
# Renders to GIF (not interactive)

glyph:

glyph(gapminder, x = continent, y = lifeExp) |>
  mark_bar() |>
  animate(by = year, transition = "morph", easing = "bounce") |>
  interact(tooltip = TRUE) |>
  titles(title = "Life Expectancy by Continent") |>
  theme_tokens(preset = "minimal")
# Renders as interactive HTML with playback controls

What changed: Animation is one function call in the pipeline, not a separate package. Output is interactive HTML (play/pause/scrub), not a static GIF. Tooltips work during animation.


4. Multi-Panel Dashboard

ggplot2 + patchwork:

library(patchwork)

p1 <- ggplot(mtcars, aes(wt, mpg)) + geom_point() + theme_minimal()
p2 <- ggplot(mtcars, aes(hp, mpg)) + geom_point() + theme_minimal()
p3 <- ggplot(mtcars, aes(factor(cyl), mpg)) + geom_boxplot() + theme_minimal()

(p1 | p2) / p3 +
  plot_annotation(title = "Motor Trend Dashboard")
# No linked interactions between panels

glyph:

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)

p3 <- glyph(mtcars, x = cyl, y = mpg) |>
  mark_bar()

compose(p1, p2, p3,
        type = "wrap",
        linked_selections = TRUE,
        title = "Motor Trend Dashboard")
# Brushing in p1 highlights the same cars in p2 and p3

What changed: No extra package. Linked selections across panels via linked_selections = TRUE. Brush in one panel cross-filters the others.


5. Marginal Distributions

ggplot2 + ggExtra:

library(ggExtra)

p <- ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
  geom_point() +
  theme_minimal()

ggMarginal(p, type = "histogram", groupColour = TRUE)

glyph:

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

What changed: Built-in. One function call. Size is configurable.


6. Dark Theme

ggplot2:

ggplot(mtcars, aes(wt, mpg)) +
  geom_point(color = "#6ec6ff") +
  theme(
    plot.background = element_rect(fill = "#1a1a2e"),
    panel.background = element_rect(fill = "#1a1a2e"),
    panel.grid.major = element_line(color = "#2a2a4a"),
    panel.grid.minor = element_blank(),
    axis.text = element_text(color = "#e0e0e0"),
    axis.title = element_text(color = "#e0e0e0"),
    text = element_text(color = "#e0e0e0")
  )
# 9 lines of theme overrides

glyph:

glyph(mtcars, x = wt, y = mpg) |>
  mark_point() |>
  theme_tokens(preset = "dark")
# 1 line. All contrast/grid/text colors auto-derived.

Or with custom colors:

glyph(mtcars, x = wt, y = mpg) |>
  mark_point() |>
  theme_tokens(bg = "#1a1a2e")
  # fg, grid_color, accent all adapt automatically

7. Export Flexibility

ggplot2:

ggsave("plot.png", width = 8, height = 6)   # static only
ggsave("plot.pdf", width = 8, height = 6)   # static only
ggsave("plot.svg", width = 8, height = 6)   # static only
# No interactive HTML export. No spec export.

glyph:

spec <- glyph(mtcars, x = wt, y = mpg) |>
  mark_point() |>
  interact(tooltip = TRUE, zoom = TRUE)

export(spec, "plot.html")              # interactive HTML
export(spec, "plot.svg")               # static SVG
export(spec, "plot.json")             # raw spec (inspect/debug)
cat(to_vegalite(spec))                 # Vega-Lite JSON (use in Python/JS)

Summary: When to Use Which

Use ggplot2 when: - You need a specific extension (ggridges, ggalluvial, etc.) - You’re producing static plots for print/PDF - You want maximum community support and StackOverflow answers - Statistical transforms (smooth, density2d) are central to your workflow

Use glyph when: - Interactivity is part of the deliverable (dashboards, reports, exploration) - You want linked views without Shiny - Animation is important (presentations, storytelling) - You’re working with large datasets (>10K points) - You want a cleaner, more composable API - You need to export specs to JavaScript (Vega-Lite, D3) - Theme consistency across many plots matters (token system)