ggplot-rs
A Rust implementation of ggplot2's Grammar of Graphics, rendering through the plotters backend.
No polars required. polars is a convenient — and fully
optional — input adapter. The core pipeline runs on its own internal DataFrame,
so you can plot straight from plain Rust vectors, or from
Apache Arrow RecordBatches produced by
DuckDB — with polars switched off entirely. See
Data Input and Feature Flags.
Gallery
Every image below is produced by examples/gallery.rs —
regenerate them all with cargo run --example gallery.
Themes
The same plot under each built-in theme — swap with a single .theme(theme_*()) call.
Quick Start
use *;
use *;
Features
Geoms
geom_point, geom_line, geom_bar, geom_col, geom_histogram, geom_boxplot, geom_violin, geom_smooth, geom_density, geom_area, geom_ribbon, geom_errorbar, geom_segment, geom_rug, geom_text, geom_label, geom_tile, geom_raster, geom_bin2d, geom_hex, geom_contour, geom_contour_filled, geom_path, geom_step, geom_hline, geom_vline, geom_abline, and more (40+)
Stats
StatIdentity, StatCount, StatBin, StatBoxplot, StatSmooth (Lm + Loess), StatDensity, StatLoess, StatSummary, StatEcdf, StatFunction, StatEllipse, StatContour, StatBin2d, StatBinHex, StatSum, StatYDensity, StatQQ, StatSummary2d, StatQuantile (feature regression), and more
Scales
- Continuous: linear, log10, log2, ln, sqrt, reverse, logit, probit, pseudo-log, reciprocal, exp, and Box–Cox transforms
- Discrete: automatic categorical mapping
- Color: discrete palettes (Viridis, Brewer Set1/Dark2, etc.), continuous gradients, diverging gradient2, binned/stepped scales (
scale_color_steps/fermenter), manual color assignment - Shape & Linetype: discrete mapping for point shapes and line styles
Coordinates
coord_cartesian, coord_flip, coord_fixed, coord_polar, coord_trans
Faceting
facet_wrap and facet_grid with free/fixed scales, proportional panel sizing
(space = "free" via facet_grid_space), and multi-variable columns
(facet_grid_multi, R's rows ~ b + c). Computed stats (density/histogram) are
estimated per panel.
Themes
theme_gray, theme_bw, theme_classic, theme_minimal, theme_dark, theme_light, theme_linedraw, theme_void — plus full customization via ElementText, ElementLine, ElementRect
Annotations
annotate_text, annotate_rect, annotate_segment
Guides & axes
- Legend inside the panel at panel-relative coords:
legend_position_inside(x, y)(R'slegend.position = c(x, y)). - Axis label rotation:
axis_text_x_angle(deg)/axis_text_y_angle(deg)(R'sguide_axis(angle = ...)). - Label dodging:
axis_text_x_dodge(n)staggers crowded x labels acrossnrows (guide_axis(n.dodge)). - Corner tag:
tag("A")for figure-panel labels (labs(tag)). - Axis position & expansion:
ScaleContinuous::with_position_opposite()(x-axis on top / y on the right) andwith_expand_sides(...)for per-side expansion.
Call theme-related builders after any theme_*() preset.
Computed aesthetics
An aesthetic can map an expression over columns, not just a bare column name:
new
.aes
.geom_point;
Supports + - * / % ^, parentheses, and ln/log/log10/log2/sqrt/exp/abs/sin/cos/tan/floor/ceil/round/sign. A plain column name is used directly (so existing mappings are unchanged); anything else is parsed and evaluated per row. after_scale_fill_from_color(l) / after_scale_color_from_fill(l) derive one color aesthetic from another's mapped color, lightness-adjusted (after_scale); Aes::stage(aes, start, after_stat) maps an aesthetic at two pipeline stages. The same expressions work in after_stat mappings, plus aggregate functions (sum, mean, max, min, count, median, prod) that reduce over all rows — e.g. .after_stat_y("count / sum(count)") for proportion histograms.
Command-line tool
A ggplot-rs CLI (behind the cli feature) plots parquet/CSV files or
DuckDB SQL straight from the shell — DuckDB is the query engine:
# discover columns first, then plot
# aggregate with SQL (reads parquet globs), faceted bars
Flags: --x/--y/--color/--fill/--size/--shape/--group, --geom, --facet-wrap/--facet-grid,
--log-x/--log-y/--flip, --title/--subtitle/--xlab/--ylab/--caption, -o FILE/--stdout,
--width/--height. Run --describe to list a source's columns and types.
Theming from the CLI: --theme <preset> (gray/bw/minimal/…), --palette <name>
(Set1/Dark2/viridis/RdBu/…), --primary "r,g,b" (brand color), and --theme-config <file>
— a TOML/JSON file of element overrides for full custom theming:
# brand.toml — applied on top of the base preset
= "minimal"
= "RdBu"
= [200, 60, 40]
[]
= 22
= [40, 40, 90]
[]
= [248, 246, 240]
[]
= "dashed"
[]
= "inside"
= 0.9
= 0.9
AI-ready: the repo ships a Claude Code skill at .claude/skills/plot-data/ that
teaches an agent the describe-then-map-then-render workflow, so "plot this parquet"
just works.
Data Input
GGPlot::new accepts anything implementing the GGData trait. Nothing here
requires polars — pick whichever source fits your stack.
Plain Rust — zero optional dependencies:
// Column-oriented
let cols: = vec!;
new
// Row-oriented
let rows: = vec!;
new
Apache Arrow / DuckDB — feed a RecordBatch straight from a DuckDB query
result, with polars switched off:
# Cargo.toml — no polars in the dependency tree
= { = "0.6", = false, = ["arrow"] }
let batch: RecordBatch = /* DuckDB query → Arrow */;
new
polars (optional, enabled by default) — for df! and polars pipelines:
let df = df! ?;
new
Rendering
Save to a file (format inferred from the extension — svg, png, jpg, ...):
plot.save?; // 800x600 default
plot.save_with_size?;
plot.ggsave?; // width_in, height_in, dpi
Or render in memory — no temp files — which is what you want when serving charts from a web/MCP service:
let svg: String = plot.clone.render_svg?; // or render_svg_with_size(w, h)
let png: = plot.render_png_with_size?; // fully-encoded PNG bytes
Headless / no system fonts. Rendering uses plotters' ab_glyph text backend
with a bundled font (DejaVu Sans), not font-kit/fontconfig — so text renders
deterministically in a minimal container with no system fonts installed. Nothing
to configure; there is no dependency on the host's font stack.
Theming & brand color
Everything about a theme is set at runtime, so one render process can serve many tenants' brands without touching chart code.
Inject a brand/primary color — it becomes the default for any single-series
geom that has no color/fill aesthetic mapped (an explicit mapping always wins):
new
.aes
.geom_col
.primary_color // DataZoo teal — no per-chart color code
.render_svg?;
Build a whole Theme at runtime and compose the brand into it:
let theme = theme_minimal.with_primary;
new.aes.geom_line.theme;
Supply an arbitrary sequential ramp (e.g. a green→red risk score) instead of
the built-in viridis/brewer scales — pass explicit (offset, color) stops:
new
.aes
.geom_point
.scale_color_gradientn;
Feature Flags
| Feature | Default | Provides |
|---|---|---|
polars |
yes | impl GGData for polars::DataFrame + polars re-export |
arrow |
no | impl GGData for arrow::RecordBatch (Arrow/DuckDB input) |
regression |
no | stat_quantile/geom_quantile + geom_smooth glm/rlm via anofox-regression |
cli |
no | the ggplot-rs command-line tool (parquet/CSV/DuckDB → SVG/PNG), via clap + bundled DuckDB |
To skip the heavy polars dependency (e.g. an Arrow-only service), disable defaults:
= { = "0.6", = false, = ["arrow"] }
Examples
Run any example with:
Dependencies
- plotters 0.3 — SVG/PNG rendering (
ab_glyphtext backend; no fontconfig) - image 0.24 — in-memory PNG encoding
- indexmap 2 — ordered maps for internal data
- rand 0.8 — jitter positioning
- polars 0.46 — DataFrame input (optional, default)
- arrow 53 — Arrow
RecordBatchinput (optional) - clap 4 + duckdb 1 (bundled) — the
clitool (optional)
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Bundled font
assets/fonts/DejaVuSans.ttf (plus -Bold and -Oblique faces) are bundled for headless text rendering, including bold/italic (element_text(face=)). DejaVu Sans
is distributed under a permissive, freely-redistributable license (Bitstream Vera
- Arev) — see
assets/fonts/LICENSE-DejaVu.txt.