A declarative, DataFrame-native chart library for Polars. No browser. No Python. No C FFI.
[dependencies]
charcoal = "0.1.1"use charcoal::{Chart, Theme};
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = DataFrame::new(vec![
Series::new("x", &[1.0f64, 2.0, 3.0, 4.0, 5.0]),
Series::new("y", &[2.3f64, 3.1, 2.8, 4.5, 3.9]),
Series::new("group", &["a", "a", "b", "b", "b"]),
])?;
let chart = Chart::scatter(&df)
.x("x")
.y("y")
.color_by("group")
.title("My First Chart")
.theme(Theme::Default)
.build()?;
chart.save_svg("output.svg")?;
chart.save_html("output.html")?;
Ok(())
}| Chart | Method | Required Columns |
|---|---|---|
| Scatter | Chart::scatter(&df) |
.x(), .y() |
| Line | Chart::line(&df) |
.x(), .y() |
| Bar | Chart::bar(&df) |
.x(), .y() |
| Histogram | Chart::histogram(&df) |
.x() |
| Heatmap | Chart::heatmap(&df) |
.x(), .y(), .z() |
| Box Plot | Chart::box_plot(&df) |
.x(), .y() |
| Area | Chart::area(&df) |
.x(), .y() |
| Format | Method | Feature Flag |
|---|---|---|
| SVG string | chart.svg() |
none |
| SVG file | chart.save_svg("out.svg") |
none |
| Standalone HTML | chart.save_html("out.html") |
none |
| PNG / JPEG / WEBP | chart.save_png("out.png") |
static |
| evcxr notebook | chart.display() |
notebook |
| Feature | Enables | Extra Dependencies |
|---|---|---|
| (default) | SVG and HTML output | — |
static |
PNG/JPEG/WEBP raster export | resvg (pure Rust) |
notebook |
Inline display in evcxr | evcxr_runtime |
ndarray |
Array2<f64> input for heatmaps |
ndarray |
interactive |
Plotly.js interactive HTML export | none |
Theme::Default // clean light theme
Theme::Dark // dark background
Theme::Minimal // no gridlines, minimal chrome
Theme::Colorblind // Wong 8-color palettecharcoal errors tell you what went wrong, where, and what to do next:
CharcoalError::ColumnNotFound
column "sepal_lenght" not found
Did you mean: sepal_length
Available: sepal_length, sepal_width, petal_length, petal_width, species
Every column role has a documented null policy. Nulls are never silently dropped without a warning. Access warnings after rendering:
let chart = Chart::scatter(&df)
.x("sepal_length")
.y("sepal_width")
.build()?;
for warning in chart.warnings() {
eprintln!("warning: {warning}");
}- Above 500k rows: warning emitted, scatter points subsampled
- Above 1M rows:
.build()returnsErr(CharcoalError::DataTooLarge)
Configure the limit via the builder:
Chart::scatter(&df)
.x("x")
.y("y")
.row_limit(2_000_000)
.build()?;Plotters is a low-level drawing library that gives you full control over rendering primitives, but axis scaling, layout, and data mapping are all your responsibility. charcoal trades that flexibility for a DataFrame-in, chart-out API: if your data is already in Polars, charcoal needs one builder chain where Plotters needs a full rendering pipeline.
plotly wraps Plotly.js and produces rich interactive charts, but output requires a browser and a JavaScript runtime at view time. charcoal targets static, self-contained SVG and HTML — no JavaScript engine, no network dependency, embeddable anywhere.
charts-rs produces SVG output with a similar philosophy but uses a JSON/config-driven API and has no Polars integration. charcoal is built around Polars DataFrames as the primary input, so column selection, null handling, and type inference come for free.
Licensed under either of MIT or Apache 2.0 at your option.