Quick Start

Welcome to Charton Quick Start!

This chapter will guide you through creating charts in Rust using Charton from scratch. By the end of this chapter, you'll know how to:

  • Initialize a Rust project and add Charton dependencies
  • Load and preprocess data using Polars
  • Build charts using Chart, Mark, and Encoding
  • Render charts in multiple formats and environments
  • Avoid common pitfalls and errors

The goal is to make you productive within minutes.

Project Setup

First, create a new Rust project:

cargo new demo
cd demo

Edit your Cargo.toml to add Charton and Polars dependencies:

[dependencies]
charton = "0.4"
polars = { version = "0.49", features = ["lazy", "csv", "parquet"] }

Run cargo build to ensure everything compiles.

Creating Your First Chart

Charton adopts a declarative visualization philosophy, drawing heavily from the design principles of Altair and Vega-Lite. Every Charton chart is composed of three core elements which allow you to specify what you want to see, rather than how to draw it:

  1. Chart – The base container that holds your data (Chart::build(&df)).
  2. Mark – The visual primitive you choose (point, bar, line, etc., defined by .mark_point()).
  3. Encoding – The mapping that links data fields to visual properties (x, y, color, size, etc., defined by .encode(...)).

Example: Analyzing Car Weight vs. MPG (Scatter Plot)

This minimal Charton example uses the built-in mtcars dataset to create a scatter plot of car weight (wt) versus miles per gallon (mpg).

use charton::prelude::*;
use polars::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Data Preparation (Polars)
    let df = load_dataset("mtcars")?
        .lazy()
        .with_columns([col("gear").cast(DataType::String)]) // Cast 'gear' for categorical coloring
        .collect()?;

    // 2. Chart Declaration (Chart, Mark, Encoding)
    Chart::build(&df)?          // Chart: Binds the data source
        .mark_point()?          // Mark: Specifies the visual primitive (dots)
        .encode((
            x("wt"),            // Encoding: Maps 'wt' (weight) to the X-axis
            y("mpg"),           // Encoding: Maps 'mpg' (fuel efficiency) to the Y-axis
        ))?
        // 3. Converted to Layered Chart
        .into_layered()
        // 4. Saving the Layered Chart to SVG
        .save("./scatter_chart.svg")?;

    println!("Chart saved to scatter_chart.svg");
    Ok(())
}

You can also display the result directly in your evcxr jupyter notebook using the show() method for quick iteration:

#![allow(unused)]
fn main() {
// ... (using the same 'df' DataFrame)
Chart::build(&df)?
    .mark_point()?
    .encode((x("wt"), y("mpg")))?
    .into_layered()
    .show()?;
}

You can even save the chart object to a variable and use it later. For example:

#![allow(unused)]
fn main() {
// ... (using the same 'df' DataFrame)
let chart = Chart::build(&df)?
    .mark_point()?
    .encode((x("wt"), y("mpg")))?
    .into_layered();

chart.save("./scatter_chart.svg")?; // or chart.show()?
}

This mirrors the declarative style of Altair, now in Rust.

Explicit form

The code above is equivalent to the following explicit construction using LayeredChart (see chapter 5).

use charton::prelude::*;
use polars::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Data Preparation (Polars)
    let df = load_dataset("mtcars")?
        .lazy()
        .with_columns([col("gear").cast(DataType::String)]) // Cast 'gear' for categorical coloring
        .collect()?;

    // 2. Chart Declaration (Chart, Mark, Encoding)
    let scatter = Chart::build(&df)?    // Chart: Binds the data source
        .mark_point()?                  // Mark: Specifies the visual primitive (dots)
        .encode((
            x("wt"),                    // Encoding: Maps 'wt' (weight) to the X-axis
            y("mpg"),                   // Encoding: Maps 'mpg' (fuel efficiency) to the Y-axis
        ))?;
    
    // 3. Create a layered chart
    LayeredChart::new() 
        .add_layer(scatter)             // Add the chart as a layer of the layered chart
        .save("./scatter_chart.svg")?;  // Save the layered chart

    println!("Chart saved to scatter_chart.svg");
    Ok(())
}

Loading and Preparing Data

Before creating visualizations, Charton requires your data to be stored in a Polars DataFrame. Charton itself does not impose restrictions on how data is loaded, so you can rely on Polars’ powerful I/O ecosystem.

Built-in Datasets

Charton provides a few built-in datasets for quick experimentation, demos, and tutorials.

#![allow(unused)]
fn main() {
let df = load_dataset("mtcars")?;
}

This returns a Polars DataFrame ready for visualization.

Loading CSV Files

CSV is the most common format for tabular data. Using Polars:

#![allow(unused)]
fn main() {
use polars::prelude::*;

let df = CsvReadOptions::default()
    .with_has_header(true)
    .try_into_reader_with_file_path(Some("./datasets/iris.csv".into()))?
    .finish()?;
}

Loading Parquet Files

Parquet is a high-performance, columnar storage format widely used in data engineering.

#![allow(unused)]
fn main() {
let file = std::fs::File::open("./datasets/foods.parquet")?;
let df = ParquetReader::new(file).finish()?;
}

Parquet is recommended for large datasets due to compression and fast loading.

Loading Data from Parquet Bytes (Vec<u8>) — Cross-Version Interoperability

One of the challenges when working with the Polars ecosystem is that different crates may depend on different Polars versions, which prevents passing DataFrame values directly between libraries. Charton solves this problem by offering a version-agnostic data exchange format based on Parquet-serialized bytes.

Charton provides an implementation of:

#![allow(unused)]
fn main() {
impl TryFrom<&Vec<u8>> for DataFrameSource
}

This allows you to:

  • Serialize a Polars DataFrame into Parquet bytes (Vec<u8>)
  • Pass those bytes to Charton
  • Let Charton deserialize them internally using its Polars version
  • Avoid Polars version conflicts entirely

This is especially useful when your application depends on a uncompatible Polars version with Charton. By using Parquet bytes as the intermediate format, data can be exchanged safely across Polars versions.

Example: Passing a DataFrame to Charton Using Parquet Bytes

Below is a full example demonstrating:

  1. Creating a Polars DataFrame
  2. Serializing it into Parquet bytes using your Polars version
  3. Passing those bytes to Charton
  4. Rendering a scatter plot

Cargo.toml

[dependencies]
polars = { version = "0.51", features = ["parquet"] }
charton = { version = "0.3" }

Source Code Example

use charton::prelude::*;
use polars::prelude::*;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    // Create a Polars DataFrame using Polars 0.51
    let df = df![
        "length" => [5.1, 4.9, 4.7, 4.6, 5.0, 5.4, 4.6, 5.0, 4.4, 4.9],
        "width"  => [3.5, 3.0, 3.2, 3.1, 3.6, 3.9, 3.4, 3.4, 2.9, 3.1]
    ]?;

    // Serialize DataFrame into Parquet bytes
    let mut buf: Vec<u8> = Vec::new();
    ParquetWriter::new(&mut buf).finish(&mut df.clone())?;

    // Build a Chart using the serialized Parquet bytes
    Chart::build(&buf)?
        .mark_point()?
        .encode((
            x("length"),
            y("width"),
        ))?
        .into_layered()
        .save("./scatter.svg")?;

    Ok(())
}

Simple Plotting Examples

This section introduces the most common chart types in Charton.

Line Chart

#![allow(unused)]
fn main() {
// Create a polars dataframe
let df = df![
    "length" => [4.4, 4.6, 4.7, 4.9, 5.0, 5.1, 5.4], // In ascending order
    "width" => [2.9, 3.1, 3.2, 3.0, 3.6, 3.5, 3.9]
]?;

// Create a line chart layer
Chart::build(&df)?
    .mark_line()?              // Line chart
    .encode((
        x("length"),           // Map length column to X-axis
        y("width"),            // Map width column to Y-axis
    ))?
    .into_layered()
    .save("line.svg")?;
}

Useful for trends or ordered sequences.

Bar Chart

#![allow(unused)]
fn main() {
let df = df! [
    "type" => ["a", "b", "c", "d"],
    "value" => [4.9, 5.3, 5.5, 6.5],
]?;

Chart::build(&df)?
    .mark_bar()?
    .encode((
        x("type"),
        y("value"),
    ))?
    .into_layered()
    .save("bar.svg")?;
}

Histogram

#![allow(unused)]
fn main() {
let df = load_dataset("iris")?;

Chart::build(&df)?
    .mark_hist()?
    .encode((
        x("sepal_length"),
        // The number of data points (or Frequency) falls into the corresponding bin are named "count".
        // You can use any arbitray name for the y-axis, here we use "count".
        y("count")
    ))?
    .into_layered()
    .save("hist.svg")?;
}

Charton automatically computes bin counts when y("count") is specified.

Boxplot

#![allow(unused)]
fn main() {
let df = load_dataset("iris")?;

Chart::build(&df)?
    .mark_boxplot()?
    .encode((x("species"), y("sepal_length")))?
    .into_layered()
    .save("boxplot.svg")?;
}

Boxplots summarize distributions using quartiles, medians, whiskers, and outliers.

Layered Charts

In Charton, complex visualizations are built by layering multiple charts on the same axes. Each layer defines a single mark type, and layers are composed to form a unified view with shared scales and coordinates.

use charton::prelude::*;
use polars::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a polars dataframe
    let df = df![
        "length" => [4.4, 4.6, 4.7, 4.9, 5.0, 5.1, 5.4],
        "width" => [2.9, 3.1, 3.2, 3.0, 3.6, 3.5, 3.9]
    ]?;

    // Create a line chart layer
    let line = Chart::build(&df)?
        .mark_line()?                       // Line chart
        .encode((
            x("length"),                    // Map length column to X-axis
            y("width"),                     // Map width column to Y-axis
        ))?;

    // Create a scatter point layer
    let scatter = Chart::build(&df)?
        .mark_point()?                      // Scatter plot
        .encode((
            x("length"),                    // Map length column to X-axis
            y("width"),                     // Map width column to Y-axis
        ))?;

    LayeredChart::new()       
        .add_layer(line)                    // Add the line layer
        .add_layer(scatter)                 // Add the scatter point layer
        .save("./layeredchart.svg")?;

    Ok(())
}

Exporting Charts

Charton supports exporting charts to different file formats depending on the selected rendering backend. All backends share the same API:

#![allow(unused)]
fn main() {
chart.save("output.png")?;
}

The file format is inferred from the extension.

This section describes the supported formats and saving behavior for each backend.

Rust Native Backend

The Rust native backend is the default renderer and supports:

  • SVG — vector graphics output
  • PNG — rasterized SVG (using resvg with automatic system font loading)

Saving SVG

#![allow(unused)]
fn main() {
chart.save("chart.svg")?;
}

Saving PNG

PNG is generated by rasterizing the internal SVG at 2× resolution:

#![allow(unused)]
fn main() {
chart.save("chart.png")?;
}

This produces high-quality PNG output suitable for publication.

Altair Backend (Vega-Lite)

The Altair backend uses Vega-Lite as the rendering engine and supports:

  • SVG — via Vega → SVG conversion
  • PNG — SVG rasterized via resvg
  • JSON — raw Vega-Lite specification

Saving SVG

#![allow(unused)]
fn main() {
chart.save("chart.svg")?;
}

Saving PNG

#![allow(unused)]
fn main() {
chart.save("chart.png")?;
}

Saving Vega-Lite JSON

#![allow(unused)]
fn main() {
chart.save("chart.json")?;
}

The JSON file can be opened directly in the online Vega-Lite editor.

Matplotlib Backend

The Matplotlib backend supports:

  • PNG — returned as base64 from Python, decoded and saved

Saving PNG

#![allow(unused)]
fn main() {
chart.save("chart.png")?;
}

Other formats (SVG, JSON, PDF, etc.) are not currently supported by this backend.

Unsupported Formats & Errors

Charton will return an error if:

  • The file extension is missing
  • The extension is not supported by the selected backend
  • SVG → PNG rasterization fails
  • File write errors occur

Example:

#![allow(unused)]
fn main() {
if let Err(e) = chart.save("output.bmp") {
    eprintln!("Save error: {}", e);
}
}

Summary of Supported Formats

BackendSVGPNGJSON
Rust Native✔️✔️
Altair✔️✔️✔️
Matplotlib✔️

Exporting Charts as Strings (SVG / JSON)

In addition to saving charts to files, Charton also supports exporting charts directly as strings. This is useful in environments where writing to disk is undesirable or impossible, such as:

  • Web servers returning chart data in API responses
  • Browser/WASM applications
  • Embedding charts into HTML templates
  • Passing Vega-Lite specifications to front-end visualizers
  • Testing and snapshot generation

Charton provides two kinds of in-memory exports depending on the backend.

SVG Output (Rust Native Backend) The Rust-native renderer can generate the complete SVG markup of a chart and return it as a String:

#![allow(unused)]
fn main() {
let svg_string = chart.to_svg()?;
}

This returns the full <svg>...</svg> element including:

  • Layout
  • Axes
  • Marks
  • Legends
  • Background

The string can be:

  • Embedded directly into HTML
  • Returned from a web API
  • Rendered inside a WASM application
  • Passed to a templating engine such as Askama or Tera

Example

#![allow(unused)]
fn main() {
let svg = chart.to_svg()?;
}

Vega-Lite JSON (Altair Backend) When using the Altair backend, charts can be exported as raw Vega-Lite JSON:

#![allow(unused)]
fn main() {
let json = chart.to_json()?;
}

This produces the complete Vega-Lite specification generated by Altair. Typical usage scenarios include:

  • Front-end rendering using Vega/Vega-Lite
  • Sending the chart spec from a Rust API to a browser client
  • Storing chart specifications in a database
  • Generating reproducible visualization specs

Example

#![allow(unused)]
fn main() {
let json_spec = chart.to_json()?;
println!("{}", json_spec);
}

This JSON is fully compatible with the official online Vega-Lite editor.

Summary: In-Memory Export Methods

Backendto_svg()to_json()
Rust Native✔️ SVG string❌ unsupported
Altair❌ (file-only)✔️ Vega-Lite JSON string
Matplotlib

String-based export complements file export by enabling fully in-memory rendering and programmatic integration.

Viewing Charts

Charton charts can be viewed directly inside Evcxr Jupyter notebooks using the .show() method.

When running inside Evcxr Jupyter, Charton automatically prints the correct MIME content so that the chart appears inline.

Outside Jupyter (e.g., running a binary), .show() does nothing and simply returns Ok(()).

The rendering behavior differs depending on the selected backend.

Rust Native Backend

The Rust-native backend renders charts to inline SVG.

When .show() is called inside Evcxr Jupyter, the SVG is printed using text/html MIME type.

Example

#![allow(unused)]
fn main() {
use charton::prelude::*;
use polars::prelude::*;

let df = df![
    "x" => [1, 2, 3],
    "y" => [10, 20, 30]
]?;

let chart = Chart::build(&df)?
    .mark_point()?
    .encode((x("x"), y("y")))?
    .into_layered();

chart.show()?;   // displays inline SVG in Jupyter
}

Internal Behavior

EVCXR_BEGIN_CONTENT text/html
<svg>...</svg>
EVCXR_END_CONTENT

This enables rich inline SVG display in notebooks.

Altair Backend (Vega-Lite)

When using the Altair backend, .show() emits Vega-Lite JSON with the correct MIME type:

application/vnd.vegalite.v5+json

Jupyter then renders the chart using the built-in Vega-Lite renderer.

Example

#![allow(unused)]
fn main() {
chart.show()?;   // displays interactive Vega-Lite chart inside Jupyter
}

Internal Behavior

EVCXR_BEGIN_CONTENT application/vnd.vegalite.v5+json
{ ... Vega-Lite JSON ... }
EVCXR_END_CONTENT

This produces interactive charts (tooltips, zooming, etc.) if supported by the notebook environment.

Matplotlib Backend

The Matplotlib backend produces base64-encoded PNG images and sends them to the notebook using image/png MIME type.

Example

#![allow(unused)]
fn main() {
chart.show()?;   // displays inline PNG rendered by Matplotlib
}

Internal Behavior

EVCXR_BEGIN_CONTENT image/png
<base64 image>
EVCXR_END_CONTENT

Summary: What .show() displays in Jupyter

BackendOutput TypeMIME Type
Rust NativeSVGtext/html
AltairVega-Liteapplication/vnd.vegalite.v5+json
MatplotlibPNGimage/png

.show() is designed to behave naturally depending on the backend, giving the best viewing experience for each renderer.

Static interactive-style display in Jupyter (via evcxr)

Charton integrates with evcxr to display static charts inline inside Jupyter notebooks. This mode is “static” because the output is a fixed SVG, but it behaves “interactive-style” because:

  • Each execution immediately re-renders the chart inside the notebook
  • Any changes to code/data result in instant visual updates
  • Ideal for exploration, education, and iterative refinement

This is similar to how Plotters or PlotPy integrate with evcxr.

Example: Displaying a Charton chart inline in Jupyter

#![allow(unused)]
fn main() {
:dep charton = { version="0.4" }
:dep polars = { version="0.49" }

use charton::prelude::*;
use polars::prelude::*;

// Create sample data
let df = df![
    "length" => [5.1, 4.9, 4.7, 4.6, 5.0],
    "width"  => [3.5, 3.0, 3.2, 3.1, 3.6]
]?;

// Build a simple scatter plot
Chart::build(&df)?
    .mark_point()?
    .encode((x("length"), y("width")))?
    .show()?;   // <-- Displays directly inside the Jupyter cell
}

Even though the chart itself is static, the workflow feels interactive due to the rapid feedback loop.

Summary

In this chapter, you learned how to:

  • Load datasets from CSV, Parquet, and built-in sources
  • Create essential chart types: scatter, bar, line, histogram, boxplot, layered plots
  • Export your charts to SVG, PNG, and Vega JSON
  • Preview visualizations in the notebook

With these foundations, you now have everything you need to build end-to-end data visualizations quickly and reliably. The next chapters will introduce the building blocks of Charton, including marks and eocodings.