Skip to content

Instantly share code, notes, and snippets.

@wader
Created September 8, 2026 14:23
Show Gist options
  • Select an option

  • Save wader/b56cfb140de7aac31b79a2b338bc396a to your computer and use it in GitHub Desktop.

Select an option

Save wader/b56cfb140de7aac31b79a2b338bc396a to your computer and use it in GitHub Desktop.
ggplot CLI tool
#!/usr/bin/env Rscript
# ggplot_cli.R — command-line ggplot2 plotting tool
#
# Usage examples:
# cat data.csv | Rscript ggplot_cli.R -x mpg -y hp
# Rscript ggplot_cli.R -i data.csv -o plot.png -x mpg -y hp -t line
# Rscript ggplot_cli.R -i data.csv -x date -y revenue -t bar --yscale log10 --ylabel-fmt dollar
# Rscript ggplot_cli.R -i data.csv -o plot.svg -x mpg -y hp -t line
# cat data.csv | Rscript ggplot_cli.R -x weight -y price --color group -t point --xscale log10 --ylabel-fmt comma -o -
# cat data.csv | Rscript ggplot_cli.R -x mpg -y hp -f svg -o -
suppressPackageStartupMessages({
library(optparse)
library(ggplot2)
library(scales)
})
# ── CLI options ──────────────────────────────────────────────────────────────
option_list <- list(
# I/O
make_option(c("-i", "--input"), default = "-",
help = "Input CSV path. Use '-' or omit for stdin [default: stdin]"),
make_option(c("-o", "--output"), default = "plot.png",
help = "Output path. Use '-' for stdout [default: plot.png]"),
make_option(c("-f", "--format"), default = NULL,
help = "Output format: png, svg. Auto-detected from --output extension [default: png]"),
# Aesthetics
make_option(c("-x", "--xcol"), help = "Column name for x-axis (required)"),
make_option(c("-y", "--ycol"), default = NULL,
help = "Column name for y-axis (optional for histogram/density)"),
make_option(c("--color"), default = NULL,
help = "Column name mapped to color/fill aesthetic"),
make_option(c("--facet"), default = NULL,
help = "Column name for facet_wrap (e.g. 'region')"),
# Plot type
make_option(c("-t", "--type"), default = "point",
help = paste("Plot type: point, line, bar, col, histogram,",
"density, box, violin, area, step [default: point]")),
# Axis scales
make_option("--xscale", default = "identity",
help = "X-axis transform: identity, log10, log2, sqrt, reverse [default: identity]"),
make_option("--yscale", default = "identity",
help = "Y-axis transform: identity, log10, log2, sqrt, reverse [default: identity]"),
# Label formatters (scales package)
make_option("--xlabel-fmt", default = NULL,
help = paste("X-axis label format: comma, dollar, percent, scientific,",
"number_si, bytes, date, time, ordinal [default: none]")),
make_option("--ylabel-fmt", default = NULL,
help = paste("Y-axis label format: comma, dollar, percent, scientific,",
"number_si, bytes, date, time, ordinal [default: none]")),
# Labels & appearance
make_option("--title", default = NULL, help = "Plot title"),
make_option("--subtitle", default = NULL, help = "Plot subtitle"),
make_option("--xlab", default = NULL, help = "X-axis label (default: column name)"),
make_option("--ylab", default = NULL, help = "Y-axis label (default: column name)"),
make_option("--theme", default = "minimal",
help = "ggplot2 theme: minimal, bw, classic, light, dark, void, gray [default: minimal]"),
# Dimensions
make_option("--width", default = 1200, type = "integer", help = "Plot width in pixels [default: 1200]"),
make_option("--height", default = 750, type = "integer", help = "Plot height in pixels [default: 750]"),
make_option("--dpi", default = 150, type = "integer", help = "Resolution in DPI [default: 150]"),
# Extras
make_option("--alpha", default = 0.8, type = "double", help = "Point/fill alpha [default: 0.8]"),
make_option("--size", default = 1.5, type = "double", help = "Point/line size [default: 1.5]"),
make_option("--bins", default = 30, type = "integer", help = "Bins for histogram [default: 30]"),
make_option("--position", default = "dodge",
help = "Bar/col position: dodge, stack, fill [default: dodge]"),
make_option("--smooth", default = FALSE, action = "store_true",
help = "Add a smoothing line (geom_smooth) to scatter/line plots"),
make_option("--se", default = FALSE, action = "store_true",
help = "Show confidence interval on smooth line")
)
opt <- parse_args(OptionParser(option_list = option_list))
if (is.null(opt$xcol)) {
stop("--xcol / -x is required. Run with --help for usage.", call. = FALSE)
}
# ── Read data ────────────────────────────────────────────────────────────────
if (opt$input == "-") {
df <- read.csv(file("stdin"), stringsAsFactors = FALSE)
} else {
if (!file.exists(opt$input)) stop("Input file not found: ", opt$input, call. = FALSE)
df <- read.csv(opt$input, stringsAsFactors = FALSE)
}
stopifnot(opt$xcol %in% names(df))
if (!is.null(opt$ycol)) stopifnot(opt$ycol %in% names(df))
# ── Helpers ──────────────────────────────────────────────────────────────────
get_theme <- function(name) {
switch(name,
minimal = theme_minimal(),
bw = theme_bw(),
classic = theme_classic(),
light = theme_light(),
dark = theme_dark(),
void = theme_void(),
gray = theme_gray(),
theme_minimal()
)
}
get_formatter <- function(name) {
if (is.null(name)) return(waiver())
switch(name,
comma = label_comma(),
dollar = label_dollar(),
percent = label_percent(),
scientific = label_scientific(),
number_si = label_number_si(),
bytes = label_bytes(),
date = label_date(),
time = label_time(),
ordinal = label_ordinal(),
waiver()
)
}
get_trans <- function(name) {
switch(name,
identity = "identity",
log10 = "log10",
log2 = "log2",
sqrt = "sqrt",
reverse = "reverse",
"identity"
)
}
# ── Build plot ───────────────────────────────────────────────────────────────
aes_map <- if (!is.null(opt$ycol)) {
if (!is.null(opt$color)) {
aes(x = .data[[opt$xcol]], y = .data[[opt$ycol]], color = .data[[opt$color]],
fill = .data[[opt$color]])
} else {
aes(x = .data[[opt$xcol]], y = .data[[opt$ycol]])
}
} else {
if (!is.null(opt$color)) {
aes(x = .data[[opt$xcol]], color = .data[[opt$color]], fill = .data[[opt$color]])
} else {
aes(x = .data[[opt$xcol]])
}
}
p <- ggplot(df, aes_map)
geom_layer <- switch(opt$type,
point = geom_point(alpha = opt$alpha, size = opt$size),
line = geom_line(alpha = opt$alpha, linewidth = opt$size),
step = geom_step(alpha = opt$alpha, linewidth = opt$size),
bar = geom_bar(alpha = opt$alpha, stat = "count", position = opt$position),
col = geom_col(alpha = opt$alpha, position = opt$position),
histogram = geom_histogram(alpha = opt$alpha, bins = opt$bins, position = opt$position),
density = geom_density(alpha = opt$alpha),
box = geom_boxplot(alpha = opt$alpha),
violin = geom_violin(alpha = opt$alpha),
area = geom_area(alpha = opt$alpha),
stop("Unknown plot type: ", opt$type, call. = FALSE)
)
p <- p + geom_layer
# Optional smooth line
if (opt$smooth && opt$type %in% c("point", "line", "step")) {
p <- p + geom_smooth(se = opt$se)
}
# ── Scales ───────────────────────────────────────────────────────────────────
if (is.numeric(df[[opt$xcol]])) {
p <- p + scale_x_continuous(
trans = get_trans(opt$xscale),
labels = get_formatter(opt$`xlabel-fmt`)
)
}
if (!is.null(opt$ycol) && is.numeric(df[[opt$ycol]])) {
p <- p + scale_y_continuous(
trans = get_trans(opt$yscale),
labels = get_formatter(opt$`ylabel-fmt`)
)
}
# ── Labels, theme, facets ────────────────────────────────────────────────────
p <- p + labs(
title = opt$title,
subtitle = opt$subtitle,
x = if (!is.null(opt$xlab)) opt$xlab else opt$xcol,
y = if (!is.null(opt$ylab)) opt$ylab else opt$ycol
)
p <- p + get_theme(opt$theme)
if (!is.null(opt$facet)) {
p <- p + facet_wrap(as.formula(paste("~", opt$facet)))
}
# ── Resolve output format ────────────────────────────────────────────────────
out_fmt <- if (!is.null(opt$format)) {
match.arg(opt$format, c("png", "svg"))
} else if (opt$output != "-") {
ext <- tolower(tools::file_ext(opt$output))
if (ext %in% c("png", "svg")) ext else "png"
} else {
"png"
}
# ── Save / write ─────────────────────────────────────────────────────────────
width_in <- opt$width / opt$dpi
height_in <- opt$height / opt$dpi
save_args <- list(plot = p, width = width_in, height = height_in, bg = "white")
if (out_fmt == "png") save_args$dpi <- opt$dpi
svg_device <- if (requireNamespace("svglite", quietly = TRUE)) svglite::svglite else grDevices::svg
if (opt$output == "-") {
tmp <- tempfile(fileext = paste0(".", out_fmt))
if (out_fmt == "svg") save_args$device <- svg_device
do.call(ggsave, c(list(filename = tmp), save_args))
raw_bytes <- readBin(tmp, "raw", file.info(tmp)$size)
con <- pipe("cat", "wb")
writeBin(raw_bytes, con)
close(con)
unlink(tmp)
} else {
if (out_fmt == "svg") save_args$device <- svg_device
do.call(ggsave, c(list(filename = opt$output), save_args))
msg <- paste0("Saved: ", opt$output, " (", opt$width, "x", opt$height, " px")
if (out_fmt == "png") msg <- paste0(msg, " @ ", opt$dpi, " dpi")
message(msg, ")")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment