MIDI Piano Roll Visualization — ggplot2

A grid-based visualization of musical notes over time, as seen in digital audio workstations (DAWs). Each note is represented as a horizontal rectangle positioned by pitch (y-axis) and time (x-axis), with bar length indicating note duration and color indicating velocity (dynamics). The background alternates between white and dark rows to mirror piano keyboard layout, with vertical grid lines marking beats and measures.

MIDI Piano Roll Visualization rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' piano-roll-midi: MIDI Piano Roll Visualization
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 89/100 | Created: 2026-06-03

library(ggplot2)
library(ragg)

set.seed(42)

# Theme tokens (Imprint palette)
THEME       <- Sys.getenv("ANYPLOT_THEME", "light")
PAGE_BG     <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420"
INK         <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
INK_SOFT    <- if (THEME == "light") "#4A4A44" else "#B8B7B0"

# Imprint sequential colormap for velocity: green (quiet) -> blue (loud)
CMAP_LOW  <- "#009E73"
CMAP_HIGH <- "#4467A3"

# Piano keyboard row background colors
KEY_WHITE <- if (THEME == "light") "#EBE7DC" else "#222220"
KEY_BLACK <- if (THEME == "light") "#D3CDB8" else "#1D1D1B"

NOTE_NAMES <- c("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B")
BLACK_KEYS <- c(1L, 3L, 6L, 8L, 10L)

# --- Data: 8-measure C-major piano piece (32 beats, 4/4 time) ---------------

# Right hand: eighth notes (0.5 beat each), melodic lines building to climax
rh_pitches <- c(
  60, 62, 64, 67, 69, 67, 64, 62,   # m1: gentle ascent/descent
  60, 64, 67, 72, 71, 69, 67, 65,   # m2: arpeggiated C chord
  64, 67, 69, 72, 74, 72, 69, 67,   # m3: E-based run
  65, 67, 69, 72, 71, 72, 69, 67,   # m4: F-based figure
  64, 67, 71, 72, 74, 76, 74, 72,   # m5: climbing higher
  71, 72, 74, 76, 77, 79, 77, 76,   # m6: climax in upper register
  74, 72, 71, 69, 67, 65, 64, 62,   # m7: long descent
  60, 62, 64, 65, 64, 62, 60, 60    # m8: quiet resolution to C4
)
rh_starts    <- seq(0, 31.5, by = 0.5)
rh_durations <- rep(0.45, 64L)

# Velocity arc: mf start -> ff climax (m6) -> p resolution
vel_raw <- c(
  seq(55,  70, length.out = 8L),
  seq(68,  82, length.out = 8L),
  seq(80,  90, length.out = 8L),
  seq(88,  95, length.out = 8L),
  seq(93, 103, length.out = 8L),
  seq(103, 118, length.out = 8L),
  seq(112,  85, length.out = 8L),
  seq(80,   48, length.out = 8L)
)
rh_vel <- pmin(pmax(round(vel_raw + rnorm(64L, 0, 4)), 30L), 127L)

# Left hand: bass root on every quarter beat (C-F-Am-G progression, 2 measures each)
bass_pitches <- rep(c(48L, 53L, 57L, 55L), each = 8L)
bass_starts  <- 0:31
bass_dur     <- rep(0.85, 32L)
bass_vel     <- pmin(pmax(round(seq(62, 72, length.out = 32L) + rnorm(32L, 0, 3)), 30L), 100L)

notes_df <- data.frame(
  start    = c(rh_starts, as.numeric(bass_starts)),
  end      = c(rh_starts + rh_durations, bass_starts + bass_dur),
  pitch    = c(rh_pitches, bass_pitches),
  velocity = c(rh_vel, bass_vel)
)

# Pitch range with 1-semitone margin
pitch_min  <- min(notes_df$pitch) - 1L
pitch_max  <- max(notes_df$pitch) + 1L
all_pitches <- seq(pitch_min, pitch_max)

# Separate background row data by key type
white_rows <- all_pitches[!(all_pitches %% 12 %in% BLACK_KEYS)]
black_rows <- all_pitches[  all_pitches %% 12 %in% BLACK_KEYS]

bg_white <- data.frame(xmin = 0, xmax = 32, ymin = white_rows - 0.5, ymax = white_rows + 0.5)
bg_black <- data.frame(xmin = 0, xmax = 32, ymin = black_rows - 0.5, ymax = black_rows + 0.5)

# Y-axis: label only white keys
y_labels <- paste0(NOTE_NAMES[(white_rows %% 12) + 1], floor(white_rows / 12) - 1)

# Beat / measure grid positions (beat lines exclude measure boundaries)
beat_xs    <- (1:31)[!(1:31 %% 4 == 0)]
measure_xs <- seq(0L, 32L, by = 4L)

# Climax highlight: measure 6 (beats 20-24) is the dynamic peak (ff)
climax_df <- data.frame(xmin = 20, xmax = 24, ymin = pitch_min - 0.5, ymax = pitch_max + 0.5)

plot_title <- "piano-roll-midi · r · ggplot2 · anyplot.ai"

# --- Plot -------------------------------------------------------------------
p <- ggplot() +
  # Piano keyboard background: white keys
  geom_rect(
    data = bg_white,
    aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax),
    fill = KEY_WHITE, color = NA
  ) +
  # Piano keyboard background: black keys
  geom_rect(
    data = bg_black,
    aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax),
    fill = KEY_BLACK, color = NA
  ) +
  # Subtle translucent highlight at dynamic climax (m6, beats 20-24)
  geom_rect(
    data = climax_df,
    aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax),
    fill = CMAP_HIGH, color = NA, alpha = 0.10
  ) +
  # Beat subdivision lines (very faint)
  geom_vline(
    xintercept = beat_xs,
    color = INK_SOFT, linewidth = 0.12, alpha = 0.35
  ) +
  # Measure boundary lines (stronger)
  geom_vline(
    xintercept = measure_xs,
    color = INK, linewidth = 0.4, alpha = 0.5
  ) +
  # MIDI notes colored by velocity
  geom_rect(
    data = notes_df,
    aes(
      xmin = start, xmax = end,
      ymin = pitch - 0.42, ymax = pitch + 0.42,
      fill = velocity
    ),
    color = PAGE_BG, linewidth = 0.15
  ) +
  # "ff" annotation marking the dynamic peak
  annotate(
    "text", x = 22, y = pitch_max - 0.3,
    label = "ff", color = INK, size = 2.5, fontface = "bold.italic"
  ) +
  # Imprint sequential colormap for velocity (green=quiet, blue=loud)
  scale_fill_gradient(
    low    = CMAP_LOW,
    high   = CMAP_HIGH,
    limits = c(0, 127),
    name   = "Velocity",
    breaks = c(0, 32, 64, 96, 127),
    labels = c("pp", "p", "mf", "f", "ff")
  ) +
  scale_x_continuous(
    name   = "Measure",
    breaks = seq(0, 32, by = 4),
    labels = paste0("m", 1:9),
    expand = expansion(add = c(0, 0))
  ) +
  scale_y_continuous(
    name   = "Pitch",
    breaks = white_rows,
    labels = y_labels,
    expand = expansion(add = c(0.5, 0.5))
  ) +
  labs(title = plot_title) +
  theme_minimal(base_size = 8) +
  theme(
    plot.background   = element_rect(fill = PAGE_BG, color = PAGE_BG),
    panel.background  = element_rect(fill = PAGE_BG, color = NA),
    panel.grid.major  = element_blank(),
    panel.grid.minor  = element_blank(),
    panel.border      = element_rect(color = INK_SOFT, fill = NA, linewidth = 0.35),
    axis.title        = element_text(color = INK,      size = 10),
    axis.text         = element_text(color = INK_SOFT, size = 8),
    axis.text.y       = element_text(color = INK_SOFT, size = 8),
    plot.title        = element_text(color = INK,      size = 12, face = "bold"),
    legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.3),
    legend.text       = element_text(color = INK_SOFT, size = 8),
    legend.title      = element_text(color = INK,      size = 9),
    legend.position   = "right",
    plot.margin       = margin(12, 12, 10, 10)
  ) +
  guides(
    fill = guide_colorbar(
      barheight = 12,
      barwidth  = 0.8
    )
  )

# --- Save -------------------------------------------------------------------
ggsave(
  filename = sprintf("plot-%s.png", THEME),
  plot     = p,
  device   = ragg::agg_png,
  width    = 8,
  height   = 4.5,
  units    = "in",
  dpi      = 400
)

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/piano-roll-midi/ggplot2/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "piano-roll-midi",
  "language": "r",
  "library": "ggplot2",
  "page": "https://anyplot.ai/piano-roll-midi/r/ggplot2",
  "hub": "https://anyplot.ai/piano-roll-midi",
  "code_json": "https://api.anyplot.ai/specs/piano-roll-midi/ggplot2/code",
  "spec_json": "https://api.anyplot.ai/specs/piano-roll-midi",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/piano-roll-midi/r/ggplot2/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/piano-roll-midi/r/ggplot2/plot-dark.png",
  "quality_score": 89.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of MIDI Piano Roll Visualization on anyplot.ai.

Other implementations