# charcoal
[](https://crates.io/crates/charcoal)
[](https://docs.rs/charcoal/0.1.1/charcoal/)
A declarative, DataFrame-native chart library for Polars. No browser. No Python. No C FFI.
```toml
[dependencies]
charcoal = "0.1.1"
```
## Quickstart
```rust
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 Types
| 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()` |
## Output Formats
| 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 Flags
| *(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 |
## Themes
```rust
Theme::Default // clean light theme
Theme::Dark // dark background
Theme::Minimal // no gridlines, minimal chrome
Theme::Colorblind // Wong 8-color palette
```
## Error Quality
charcoal 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
```
## Null Handling
Every column role has a documented null policy. Nulls are never silently
dropped without a warning. Access warnings after rendering:
```rust
let chart = Chart::scatter(&df)
.x("sepal_length")
.y("sepal_width")
.build()?;
for warning in chart.warnings() {
eprintln!("warning: {warning}");
}
```
## Row Limits
- Above 500k rows: warning emitted, scatter points subsampled
- Above 1M rows: `.build()` returns `Err(CharcoalError::DataTooLarge)`
Configure the limit via the builder:
```rust
Chart::scatter(&df)
.x("x")
.y("y")
.row_limit(2_000_000)
.build()?;
```
## Alternatives
**[Plotters](https://crates.io/crates/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](https://crates.io/crates/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](https://crates.io/crates/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.
## License
Licensed under either of [MIT](LICENSE-MIT) or [Apache 2.0](LICENSE-APACHE) at your option.