Rust deserves charts that belong in a journal paper — without shelling out to Python, booting a browser, or hunting for a system font that exists on your laptop but not on CI.
plotkit is a pure-Rust plotting library with a matplotlib-shaped API. Add one crate, write three lines, and get an anti-aliased, professionally themed figure as a PNG, SVG, or PDF. No GPU. No subprocess. No system dependencies. Identical bytes on Linux, macOS, and Windows.
plotkit::plot(&x, &y)?;
plotkit::title("sin(x)");
plotkit::savefig("plot.png")?;- 🎨 It looks good by default. A hand-tuned theme, the Tableau-10 palette, the Talbot–Lin–Hanrahan tick algorithm, and an automatic layout that never clips a label.
- ⚡ It's fast. A 10,000-point line renders to PNG in ~11 ms; a decimated 1,000,000-point scatter in ~34 ms — all on the CPU. See BENCHMARKS.md.
- 📦 It's self-contained. One static binary, zero runtime dependencies. The Inter font is embedded, so text renders deterministically everywhere — no font lookup to fail at 2 AM.
- 🐍 It's familiar. If you know matplotlib, you already know plotkit:
Figure,Axes,ax.plot(...),ax.scatter(...),fig.savefig(...). - 🔌 It's composable. Plot straight from Polars
DataFrames andndarrayarrays, render inline in Jupyter (evcxr), or draw to an HTML canvas via WebAssembly.
[dependencies]
plotkit = "1.0"Optional features
plotkit = { version = "1.0", features = ["svg", "pdf", "jupyter", "ndarray", "polars"] }| Feature | Description |
|---|---|
png |
PNG rasterization (default) |
svg |
SVG vector output (default) |
pdf |
Print-ready PDF vector output |
jupyter |
Inline display in Evcxr notebooks |
ndarray |
Plot directly from ndarray::Array1 |
polars |
Plot directly from Polars Series / DataFrame |
The pyplot facade is perfect for scripts — it operates on a current figure, just like
matplotlib.pyplot:
use plotkit::prelude::*;
let x: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
let y: Vec<f64> = x.iter().map(|v| v.sin()).collect();
plotkit::plot(&x, &y)?;
plotkit::title("sin(x)");
plotkit::xlabel("x");
plotkit::ylabel("y");
plotkit::savefig("sine.png")?;For full control, drop down to the Figure / Axes model:
use plotkit::prelude::*;
let mut fig = Figure::with_size(1200, 500);
let ax1 = fig.add_subplot(1, 2, 1);
ax1.plot(&x, &sin_y)?.label("sin(x)").color(Color::TAB_BLUE);
ax1.plot(&x, &cos_y)?.label("cos(x)").color(Color::TAB_ORANGE);
ax1.set_title("Trigonometric Functions");
ax1.legend();
let ax2 = fig.add_subplot(1, 2, 2);
ax2.scatter(&x, &y)?.marker(Marker::Circle);
ax2.set_title("Scatter");
fig.tight_layout();
fig.save("subplots.png")?;Sixteen chart types, one familiar API:
plot — line |
scatter |
bar / barh |
bar_group |
hist |
fill_between |
step |
stem |
boxplot |
violin |
errorbar |
heatmap |
pie |
contour / contourf |
hexbin |
polar / waterfall |
Browse the gallery of 55+ runnable examples — each is a complete program:
cargo run --example 21_multi_line
cargo run --example 47_df_plot --features polarsPolars and ndarray are first-class inputs. Any Series, Array1, or slice flows through
the same IntoSeries trait — zero glue code:
use ndarray::Array1;
let x = Array1::linspace(0.0, 10.0, 100);
ax.plot(&x, &x.mapv(f64::sin))?;Polars users get a pandas-style df.plot() accessor with categorical grouping:
use plotkit::prelude::*;
use plotkit::plotkit_polars::DataFramePlotExt;
df.plot()
.color_by("symbol") // one labelled series per category
.line("date", "price")?
.save("stocks.png")?;fig.set_theme(Theme::dark()); // default · dark · seaborn · ggplot · publication · nature · solarized
ax.scatter(&x, &y)?.cmap(Colormap::Viridis).c(values); // 14 perceptual colormaps
ax.set_yscale(Scale::Log10); // Linear · Log10 · SymLogAxis control, twin axes, annotations
ax.set_xlim(0.1, 1000.0);
ax.set_xticks(&[0.1, 1.0, 10.0, 100.0, 1000.0]);
ax.invert_yaxis();
ax.grid(true);
let ax2 = fig.twinx(0); // independent secondary y-axis
ax2.plot(&time, &pressure)?;
ax.annotate("peak", (3.0, 0.5), (4.5, 0.8));
ax.text(2.0, 0.8, "note");| Format | Method | Notes |
|---|---|---|
| PNG | fig.save("out.png") |
CPU-rasterized, deterministic |
| SVG | fig.save("out.svg") |
Scalable vector (svg feature) |
fig.save("out.pdf") |
Print-ready vector (pdf feature) |
|
| Bytes | fig.to_png_bytes() / fig.to_pdf_bytes() |
Embed or stream |
| String | fig.to_svg_string() |
Web / templating |
Format is selected from the file extension — one method, every backend.
Pure-CPU rendering, measured with Criterion (medians, AMD Ryzen AI 7 350):
| Workload | Measured |
|---|---|
| 10k-point line → PNG | 10.9 ms |
| 100k-point line → PNG (auto LTTB) | 13.7 ms |
| 1M-point scatter → PNG (decimated) | 33.7 ms |
| Figure creation (no render) | 434 ns |
Automatic LTTB decimation keeps the rasterizer working on a screen-sized point count no matter how large the input. Full methodology and TRD targets in BENCHMARKS.md.
The Renderer trait is the only seam between plot logic and pixels, so the same figure code
targets every backend. plotkit-render-wasm draws to an HTML5 canvas via WebAssembly:
wasm-pack build crates/plotkit-render-wasm --target web --out-dir ../../web/pkgA ready-to-serve demo lives in web/.
plotkit is a focused multi-crate workspace:
| Crate | Purpose |
|---|---|
plotkit |
Umbrella crate — pyplot facade + save |
plotkit-core |
Figure, Axes, artists, the Renderer trait |
plotkit-render-skia |
PNG backend (tiny-skia + cosmic-text) |
plotkit-render-svg |
SVG backend |
plotkit-render-pdf |
PDF backend (printpdf) |
plotkit-render-wasm |
WASM Canvas2D backend |
plotkit-ndarray · plotkit-polars |
DataFrame / array integration |
Core logic never depends on a concrete backend — adding a new output format means implementing one trait.
v1.0 — stable. The public API is semver-stable; signatures shipped in 1.0 only grow, never break. Shipping today: 16 chart types, PNG/SVG/PDF, 7 themes, 14 colormaps, log/symlog scales, twin axes, subplots, Polars + ndarray, Jupyter inline rendering, and a WASM backend.
| Version | Focus |
|---|---|
| v1.1 | Animation / frame sequences |
| v1.2 | 3D surface plots |
| v1.x | Interactive widgets; optional GPU (Vello) backend behind the Renderer trait |
Bug reports, feature requests, and pull requests are all welcome — start with
CONTRIBUTING.md. Every visual change needs an insta golden-image
snapshot; run cargo insta review before committing.
Licensed under either of MIT or Apache-2.0 at your option.