u/Deep-Network1590

▲ 101 r/webgpu+1 crossposts

Building Charton: A WGPU-Powered General Visualization Library. Just pushed 50k points at 60 FPS on integrated graphics, but hit a WASM bottleneck. Any advice?

Hey Reddit!

I’ve been working on charton, a general-purpose visualization library for the web. In my previous iteration, I got basic interactive sine waves running via the GPU, but the architecture was still hurting from heavy heap allocations. After refactoring the JS/WASM boundary into a stateful, zero-allocation setup, I finally managed to render a 50,000-point Lorenz Attractor dynamically at a locked 60 FPS using WGPU.

Here is a quick look at how the data updates are handled now:

src/lib.rs:

pub async fn update_and_render(&mut self, xs: &[f64], ys: &[f64], colors: &[f64]) -> Result<(), JsValue> {
    self.dataset.update_column_f64("x", xs)?; // In-place memcpy without allocation
    Chart::build(self.dataset.clone())?.mark_point().render_to_canvas(&self.canvas_id).await?
}

index.html:

const app = new LiveChartApp("chart-canvas", TOTAL_POINTS);
// Inside the requestAnimationFrame loop:
await app.update_and_render(xs, ys, colors);

I'm currently stress-testing this on a budget laptop setup: a 13th Gen Intel Core i5-13420H (2.10 GHz) with integrated Intel UHD Graphics (128 MB). My GPU utilization hovers around 70%, but because WASM runs single-threaded here, one CPU core is completely maxed out feeding the render pipeline. Since charton is meant to be a general-purpose visualization library, I can't rely on equation-specific math hacks. Do you have any recommendations for generic data-streaming patterns or memory layouts across the WASM boundary to relieve the CPU?

Moving forward, I'm planning to use this engine to create equation-driven short videos and data art animations. I’d love to hear your thoughts—what kind of interesting use cases, large datasets, or simulation scenarios would you love to see a high-performance library like this tackle next?

u/Deep-Network1590 — 7 days ago
▲ 33 r/rust

Charton 0.5 — WGPU-accelerated & WASM Rust visualization library

A while back, I shared a demo here of charton (Altair-style Grammar of Graphics) running a real-time CPU-rendered SVG in the browser. It was cool, but generating megabytes of XML strings every frame meant the DOM choked at around 200 data points.

So I rewrote the rendering pipeline to use WGPU. Now, instead of fighting the DOM, charton pushes pure geometric primitives straight to VRAM while deferring typography to the native Canvas 2D.

As you can see in the attached video, it smoothly renders 50,000+ points at a locked 60 FPS. The best part is that I recorded this on a basic laptop with an Intel i5-13420H and integrated UHD Graphics. If a low-end iGPU can handle this without breaking a sweat, a dedicated card will fly.

Here is what the core logic looks like for the 50k points stress test.

#[wasm_bindgen]
pub async fn render_chart_gpu(
    canvas_id: String,
    xs: &[f64], // Taking slices allows zero-copy streaming from JS Float64Arrays
    ys: &[f64],
    colors: &[f64],
) -> Result<(), JsValue> {
    // 1. Build dataset
    let ds = Dataset::new()
        .with_column("x", xs.to_vec()).unwrap()
        .with_column("y", ys.to_vec()).unwrap()
        .with_column("color", colors.to_vec()).unwrap();

    // 2. Declarative Altair-style syntax
    Chart::build(ds).unwrap()
        .mark_point().unwrap()
        .configure_point(|p| p.with_size(1.0))
        .encode((alt::x("x"), alt::y("y"), alt::color("color"))).unwrap()
        .with_size(800, 400)
        .render_to_canvas(&canvas_id)
        .await
        .unwrap();

    Ok(())
}

Core JS (index.html):

import init, { render_chart_gpu } from './pkg/wave.js';

async function run() {
    await init();
    const TOTAL_POINTS = 50_000;
    let xs = new Float64Array(TOTAL_POINTS);
    let ys = new Float64Array(TOTAL_POINTS);
    let colors = new Float64Array(TOTAL_POINTS);

    let offset = 0;
    
    // 60 FPS requestAnimationFrame loop
    async function loop() {
        offset += 0.05;
        for (let i = 0; i < TOTAL_POINTS; i++) {
            xs[i] = i * 0.01;
            ys[i] = Math.sin(i * 0.05 + offset) * Math.cos(i * 0.002 + offset * 0.5);
            colors[i] = ys[i]; // Map color to Y-axis intensity
        }
        
        // Stream the buffers directly to the WGPU pipeline
        await render_chart_gpu("chart-canvas", xs, ys, colors);
        requestAnimationFrame(loop);
    }
    loop();
}
run();

Would love to hear what you guys think or if you have any feature requests for the WGPU backend!

u/Deep-Network1590 — 18 days ago
▲ 89 r/rust

Rust + WASM real‑time rendering in the browser using Charton

Charton (a rust visualization crate based on Grammar of Graphics) now has WASM support — build browser‑native interactive charts in Rust, no JS plotting libraries required.

This demo runs a real‑time color‑gradient scatter plot in the browser. Every frame is a fresh SVG computed by Rust/WASM (~20–25 FPS), simulating a sensor sampling signal intensity over time.

Core Rust (lib.rs):

#[wasm_bindgen]
pub fn draw_wave(xs: Vec<f64>, ys: Vec<f64>, colors: Vec<f64>) -> Result<String, JsValue> {
    let ds = Dataset::new()
        .with_column("x", xs)?
        .with_column("y", ys)?
        .with_column("Intensity", colors)?;

    let chart = Chart::build(ds)?
        .mark_point()?
        .encode((alt::x("x"), alt::y("y"), alt::color("Intensity")))?
        .with_size(800, 400);

    Ok(chart.to_svg()?)
}

Core JS (index.html):

import init, { draw_wave } from './pkg/wave.js';

async function run() {
    await init();
    const container = document.getElementById('chart');
    let xs = [], ys = [], t = 0;

    function loop() {
        ys.push(Math.sin(t * 0.3) + (Math.random() - 0.5) * 0.2);
        xs.push(t++);
        if (xs.length > 200) { xs.shift(); ys.shift(); }
        container.innerHTML = draw_wave(xs, ys, [...ys]);
        requestAnimationFrame(loop);
    }
    loop();
}
run();

Build with wasm-pack build --release --target web, serve with any HTTP server, and you’re good to go.

u/Deep-Network1590 — 1 month ago
▲ 33 r/rust

Charton: A columnar-native plotting library for Rust (Polars-friendly)

Charton is a columnar-native plotting library designed to keep memory layouts as close to Apache Arrow as possible, allowing for near-zero-copy integration.

Here’s the core of how it handles data:

pub enum ColumnVector {
    Boolean { data: Vec<bool>, validity: Option<Vec<u8>> },
    Int8 { data: Vec<i8>, validity: Option<Vec<u8>> },
    // ... handles all int/float types
    String { data: Vec<String>, validity: Option<Vec<u8>> },
    Categorical { keys: Vec<u32>, values: Vec<String>, validity: Option<Vec<u8>> },
    Datetime { data: Vec<i64>, validity: Option<Vec<u8>>, timezone: Option<String> },
}
    // ... other types

And the Dataset structure ensures alignment:

pub struct Dataset {
    schema: AHashMap<String, usize>,
    columns: Vec<Arc<ColumnVector>>,
    row_count: usize,
}

The load_polars_df! macro is ready to use, and the Altair-style declarative API makes building layered charts intuitively.

The roadmap is to go full native Arrow and eventually integrate DataFusion for a "SQL-to-chart" workflow. It’s still early days, so if you’re doing data analysis in Rust, I’d love to get your thoughts or feedback on the API.

u/Deep-Network1590 — 2 months ago
▲ 24 r/dataanalysis+1 crossposts

Designing a plotting Dataset for Rust: Balancing Polars support with zero-dependency weight

When building a visualization library in Rust, a classic architectural dilemma emerges: hard-coding Polars as the backend instantly makes the library heavy, slow to compile, and riddled with large dependencies—making it a no-go for lightweight applications. However, sticking purely to native Rust vectors alienates the data science community who live in Polars DataFrames.

For Charton (a rust visualization crate), the goal was to bridge this gap: keep the core plotting Dataset dependency-free, but provide a seamless, opt-in bridge for Polars users.

Instead of embedding Polars into the core, Charton works natively with clean Rust types but offers a load_polars_df!() macro. This allows Polars users to instantly ingest their data frames with zero friction, while keeping the core library dead-lightweight.

Here is how the API handles data ingestion in practice:

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Create a Polars DataFrame with diverse, high-performance types
    let df = df!(
        "id" => &[1, 2, 3, 4, 5],
        "status" => &["High", "Low", "High", "Medium", "Low"],
        "value" => &[Some(1.2), None, Some(5.6), Some(7.8), None],
        "date" => Series::new("date".into(), &[19858i32, 19859, 19860, 19861, 19862]).cast(&DataType::Date)?, 
        "datetime" => Series::new("datetime".into(), &[1715760000000i64, 1715763600000, 1715767200000, 1715770800000, 1715774400000])
            .cast(&DataType::Datetime(TimeUnit::Milliseconds, None))?,
        "duration" => Series::new("duration".into(), &[3_600_000i64, 7_200_000, 1_800_000, 10_800_000, 5_400_000])
            .cast(&DataType::Duration(TimeUnit::Milliseconds))?,
    )?;

    // 2. Convert to Charton dataset seamlessly via macro (Polars remains optional at compile time)
    let ds = load_polars_df!(df)?;
    
    // 3. Dataset is now ready for encoding-axis binding and layout transformations
    println!("{:?}", ds);

    Ok(())
}

Charton ensures strict metadata alignment during conversion. The following table illustrates how Polars logical types map to Charton physical storage:

Polars Logical Type Charton Physical Type Notes
Int8, Int16, Int32, Int64 i8, i16, i32, i64 Direct physical mapping.
UInt32, UInt64 u32, u64 Direct physical mapping.
Float32, Float64 f32, f64 NaN values are treated as Nulls.
Boolean bool Mapped to nullable boolean vector.
Utf8 / String String Stored as nullable string vectors.
Categorical(_, _), Enum(_, _) Categorical Preserves dictionary encoding + validity.
Date Date Stored as i32 days since Unix epoch.
Time Time Stored as i64 nanoseconds since midnight.
Datetime(unit, _) Datetime Normalized to i64 nanoseconds since Unix epoch.
Duration(unit) Duration Normalized to i64 nanoseconds.

Curious to hear how other library authors tackle the "heavy data frame dependency vs. lightweight core" problem in Rust and hope it helps for everyone who are facing this dilemma.

u/Deep-Network1590 — 2 months ago
▲ 357 r/rust

Hi,

I’ve been working on a Rust plotting library called Charton. I wanted to bring a true "Grammar of Graphics" experience to Rust, similar to what Altair does for Python or ggplot2 for R.

I’ve always felt that many Rust plotting tools are either too low-level—where you have to write dozens of lines just for a simple chart—or they’re just too rigid. To fix this, I built Charton with a custom Dataset engine that feels a lot like Polars. You can feed it native arrays, vectors, or Polars DataFrames directly, and it handles the data very efficiently.

The API is minimal by design. In most cases, you can get a plot done in a single line. It uses a layered approach, so you can stack different marks to build complex visualizations. Right now, it supports 9 core types: points, bars, lines, area, rect, boxplots, rules, ticks, and text.

Here’s a quick look at how you’d use it:

let height = vec![160.0, 170.0, 180.0];
let weight = vec![60.0, 75.0, 85.0];

chart!(height, weight)?
    .mark_point()?
    .encode((alt::x("height"), alt::y("weight")))?
    .save("out.svg")?;

And for layered chart:

// Create individual layers
let line = chart!(height, weight)?.mark_line()?.encode((alt::x("height"), alt::y("weight")))?;
let point = chart!(height, weight)?.mark_point()?.encode((alt::x("height"), alt::y("weight")))?;

// Combine into a composite chart
line.and(point).save("layered.svg")?;

Next on my list is adding a WGPU backend for hardware acceleration and integrating with Nushell for terminal-based plotting.

However, I’m curious to hear from the community: What are you looking for in a Rust plotting library? In what direction should Charton evolve to best fit the current trends in the Rust ecosystem?

u/Deep-Network1590 — 2 months ago