Part 1: WASM CPU-Driven SVG Animation

This chapter walks a complete beginner from zero to a real-time, animated scatter plot running in the browser, powered by Rust and WebAssembly.

The goal of this foundational stage is to understand the core Rust-to-WASM compilation pipeline. Here, we expose a draw_wave() function from Rust that utilizes the CPU to procedurally compute data points and serialize them into a standard SVG text string. JavaScript then drives the animation loop by continuously replacing the SVG markup inside the browser's DOM at an introductory ~20–25 FPS.

Architectural Note: Every single frame in this implementation is a brand‑new, heavy XML text string computed entirely on the CPU within the WebAssembly sandbox. While perfect for lightweight or static charting, this text-swapping approach serves as our baseline benchmark. It sets the stage for the true power of GPU hardware acceleration explored in Part 2.

0) Prerequisites

  • Rust toolchain (stable) – install via rustup
  • wasm-pack – install with: cargo install wasm-pack
  • A static file server – choose one:
    • Python: python -m http.server 8080 (make sure Python has been installed)
    • Node.js: npx serve .
    • Or any other HTTP server (browsers require HTTP for WASM)
  • clang (may be required on some systems)
    • Linux: sudo apt install clang
    • Windows: Download LLVM from releases.llvm.org and select Add LLVM to the system PATH
    • macOS: usually pre‑installed with Xcode command line tools

Important compatibility note: charton v0.5 depends on getrandom, which needs special configuration for wasm32-unknown-unknown. This tutorial includes all required settings.

1) Project Layout

Create a new project (e.g., cargo new wave --lib) and set up the following structure:

wave
├── Cargo.toml
├── index.html
├── pkg
└── src
    └── lib.rs

We will build a cdylib wasm package that wasm-pack will wrap into pkg/.

2) Cargo.toml

Put this into wave/Cargo.toml:

[package]
name = "wave"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]     # Produces a dynamic library for WASM

[dependencies]
wasm-bindgen = "0.2"        # JS ↔ Rust bridge
charton = "0.5"             # Declarative plotting library

# getrandom must be explicitly added with the "wasm_js" feature flag
# for wasm32-unknown-unknown target support.
getrandom = { version = "0.3", features = ["wasm_js"] }

[profile.release]
opt-level = "s"             # Optimize for size
lto = true                  # Link-time optimization
codegen-units = 1           # Better optimization
panic = "abort"             # Smaller panic handler

3) src/lib.rs- Rust (wasm entry points)

Create a lib.rs file in the src directory and add the following code:

#![allow(unused)]
fn main() {
//! Charton WASM demo: real-time animated line chart with color gradient.
//!
//! This module exposes a single function, `draw_wave`, which takes three
//! numeric arrays and returns an SVG string. The color channel is mapped
//! directly to the y-value, producing a continuous color gradient along the line.

use wasm_bindgen::prelude::*;
use charton::prelude::*;

/// Generate an SVG line chart with a color gradient.
///
/// # Arguments
/// * `xs` - X-axis values (e.g., time steps)
/// * `ys` - Y-axis values (e.g., amplitude)
/// * `colors` - Values for the continuous color scale (can be the same as `ys`)
///
/// # Returns
/// A `Result` containing the SVG string or a JavaScript error.
#[wasm_bindgen]
pub fn draw_wave(
    xs: Vec<f64>,
    ys: Vec<f64>,
    colors: Vec<f64>,
) -> Result<String, JsValue> {
    // Build a Charton Dataset from the three columns
    let ds = Dataset::new()
        .with_column("x", xs)
        .map_err(|e| JsValue::from_str(&e.to_string()))?
        .with_column("y", ys)
        .map_err(|e| JsValue::from_str(&e.to_string()))?
        .with_column("color", colors)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;

    // Build a chart using the declarative API
    let chart = Chart::build(ds)
        .map_err(|e| JsValue::from_str(&e.to_string()))?
        .mark_point()                                       // Use a line mark
        .map_err(|e| JsValue::from_str(&e.to_string()))?
        .encode((                                           // Map columns to visual channels
            alt::x("x"),
            alt::y("y"),
            alt::color("color"),                            // Continuous color scale
        ))
        .map_err(|e| JsValue::from_str(&e.to_string()))?
        .with_size(800, 400)
        .configure_theme(|t| t.with_left_margin(0.01).with_top_margin(0.12).with_bottom_margin(0.05));

    // Render the chart to a static SVG string
    let svg = chart
        .to_svg()
        .map_err(|e| JsValue::from_str(&e.to_string()))?;

    Ok(svg)
}
}

4) Build with wasm-pack

From the project root (wave/):

wasm-pack build --release --target web --out-dir pkg

wasm-pack will:

  • Compile to wasm32-unknown-unknown
  • Run wasm-bindgen to generate JavaScript bindings
  • Output everything into pkg/:
    • wave_bg.wasm – the compiled WebAssembly binary
    • wave_bg.wasm.d.ts – TypeScript declaration file describing the shape of the compiled .wasm module.
    • wave.js – ES module bootstrap
    • wave.d.ts – TypeScript declarations (optional)

The .wasm file is roughly 300 kb in release mode. Gzip or Brotli compression can bring it down further, perfectly fine for web delivery.

5) index.html – Animated Frontend

Create index.html in the project root. The JavaScript:

  • Initialises the WASM module
  • Runs an animation loop with requestAnimationFrame
  • Pushes a new data point (sine wave + noise) every ~40 ms
  • Passes the arrays to draw_wave() and replaces the SVG in the DOM
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Charton WASM — Gradient Wave</title>
    <style>
        /* Dark background to make the gradient pop */
        body {
            font-family: system-ui, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            margin: 0;
            padding-top: 2rem;
            background: #0d1117;
            color: #c9d1d9;
        }
        #chart {
            width: 800px;
            height: 400px;
            border-radius: 12px;
            background: #161b22;
            border: 1px solid #30363d;
            box-shadow: 0 4px 16px rgba(0,0,0,0.6);
        }
        .tag {
            margin-top: 1rem;
            font-size: 0.9rem;
            color: #8b949e;
        }
    </style>
</head>
<body>
    <h2>🌈 Charton + WASM — Gradient Wave</h2>
    <div id="chart"></div>
    <div class="tag">Every frame is a brand‑new SVG computed by Rust in WebAssembly</div>

    <script type="module">
        // Import the generated JS glue and the Rust function
        import init, { draw_wave } from './pkg/wave.js';

        async function run() {
            // Boot the WASM module
            await init();

            const container = document.getElementById('chart');
            const WINDOW_SIZE = 200;          // Show the latest 200 data points
            const ADD_INTERVAL_MS = 40;       // Add a new point every 50ms
            let xs = [];
            let ys = [];
            let t = 0;                        // Time counter
            let lastAdd = 0;                  // Timestamp of the last data addition

            // Animation loop driven by requestAnimationFrame
            function loop(timestamp) {
                // Only append a new point if enough time has elapsed
                if (timestamp - lastAdd >= ADD_INTERVAL_MS) {
                    // Sine wave with a little noise for a more organic look
                    const y = Math.sin(t * 0.3) + (Math.random() - 0.5) * 0.2;
                    xs.push(t);
                    ys.push(y);

                    // Keep only the latest WINDOW_SIZE points
                    if (xs.length > WINDOW_SIZE) {
                        xs.shift();
                        ys.shift();
                    }
                    t += 0.5;
                    lastAdd = timestamp;

                    try {
                        // The color column is just a copy of the y-values,
                        // which gives a nice blue‑to‑orange gradient.
                        const svg = draw_wave(xs, ys, [...ys]);
                        container.innerHTML = svg;
                    } catch (e) {
                        console.error(e);
                    }
                }
                requestAnimationFrame(loop);
            }

            requestAnimationFrame(loop);
        }

        run();
    </script>
</body>
</html>

6) Serve and View

Open a terminal in the project directory and start a local server:

python -m http.server 8080

Then open http://localhost:8080 in your browser.

You will see a dark-themed page with a flowing stream of coloured dots – the colour changes smoothly from cool (trough) to warm (peak), and the entire chart is re‑rendered from scratch by Rust on every frame.

7) Troubleshooting

  • Compilation freezes / high RAM usage – Building for WASM can be heavy. If the process hangs during wasm-opt, you can stop it manually; the unoptimised .wasm is already functional and will run in the browser.
  • wasm-opt errors – If wasm-pack fails to install or run wasm-opt, ignore the error as long as pkg/ has been populated.
  • Port already in use – Try a different port: python -m http.server 8000.
  • Chart appears but no colour gradient – Make sure you are passing three vectors to draw_wave and that the third one is a numeric array (not all the same value). Check the browser console for any Rust panics.
  • Blank page or CORS errors – Always use an HTTP server, never open the HTML file directly with file://.

What's Next?

  • Adjust the animation speed by changing t += 0.5 and ADD_INTERVAL_MS and WINDOW_SIZE in index.html.
  • Replace the sine wave with real‑time data fetched from an API.
  • Explore Polars integration to pre‑process large datasets in the browser before plotting.