#plot #image-processing #graphics

bin+lib plotpx

Pixel-focused plotting engine that renders magnitude grids, heatmaps, and spectra to RGBA buffers

2 releases

0.1.7 Oct 10, 2025
0.1.5 Oct 10, 2025

#130 in Visualization

MIT license

670KB
4K SLoC

Rust 3K SLoC // 0.0% comments Julia 525 SLoC // 0.0% comments Python 207 SLoC // 0.1% comments

PlotPx – High-Performance Rust Plotting

PlotPx is a pixel-focused plotting engine written in Rust. It targets workloads where you need to turn magnitude arrays, heatmaps, or spectral data into RGBA images with minimal overhead. The library can be consumed directly from Rust, via a C FFI, or through the bundled Julia helper package.

Highlights

  • Magnitude plotting – render scalar fields with automatic min/max tracking and color remapping.
  • Mapped magnitude grids – upscale/downscale structured data while keeping the original grid topology intact.
  • Heatmaps – drop weighted points into a soft-stamped accumulator to probe spatial density.
  • Spectrums – visualise FFT output with multiple bar styles, peak markers, and custom palettes.
  • Color schemes – use the bundled gradients or supply your own RGBA table.

All renderers produce raw RGBA bytes which can be written to disk through the write_png helper or forwarded to external consumers.

Installation

Rust crate

Add PlotPx to your project via Cargo:

cargo add plotpx

If you are working from a local checkout instead, use a path or git dependency:

[dependencies]
plotpx = { git = "https://github.com/stephenberry/plotpx.git", tag = "vX.Y.Z" }

For FFI consumers, build the cdylib target to produce the shared library:

cargo build --release --features ""
# Shared object lands in target/release/ (platform-specific extension)

Julia helper project

A lightweight Julia package ships in julia/PlotPx. It knows how to download GitHub release artifacts (Artifacts.toml) or fall back to the locally-built shared library.

julia --project=julia/PlotPx -e 'using Pkg; Pkg.develop(path="julia/PlotPx"); Pkg.instantiate()'

You can now load the wrapper from Julia:

julia --project=julia/PlotPx
julia> using PlotPx
julia> PlotPx.load_plotpx()  # downloads artifacts or uses target/release

Quickstart (Rust)

use plotpx::{write_png, Magnitude};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    const W: u32 = 512;
    const H: u32 = 512;
    let mut plot = Magnitude::new(W, H);

    for y in 0..H {
        for x in 0..W {
            let value = (x + y) as f32 / (W + H) as f32;
            plot.add_point(x, y, value);
        }
    }

    let rgba = plot.render();
    write_png("magnitude.png", &rgba, W, H)?;
    Ok(())
}

Run the example with cargo run --release --bin your_binary. The helper stores the PNG in the current directory.

Quickstart (Julia)

using PlotPx

# 4×6 magnitude surface
surface = reshape(Float32[ x + y for y in 0:3, x in 0:5 ], 4, 6)
bytes = PlotPx.write_png_bytes(surface)

open("surface.png", "w") do io
    write(io, bytes)
end

write_png_bytes accepts keyword arguments such as saturation or custom RGBA color tables. File helpers (write_png_file) are also available.

Using PlotPx from other languages (FFI)

Compile the crate with cargo build --release --target <triple> to obtain the shared library. The exported symbols follow the naming convention plotpx_write_* and operate on plain C types. A minimal C usage example:

#include <stdint.h>
#include <stdio.h>

typedef struct {
    uint8_t *data;
    size_t len;
} PlotpxBuffer;

extern int plotpx_write_magnitude_png_buffer(const float *data, size_t len,
                                             uint32_t width, uint32_t height,
                                             float saturation,
                                             const uint8_t *colors,
                                             size_t colors_len,
                                             PlotpxBuffer *out);
extern void plotpx_free_buffer(PlotpxBuffer buffer);

int main(void) {
    float data[16] = {0};
    PlotpxBuffer png = {0};
    if (plotpx_write_magnitude_png_buffer(data, 16, 4, 4, 0.0f,
                                          NULL, 0, &png) != 0) {
        fprintf(stderr, "render failed\n");
        return 1;
    }
    FILE *fp = fopen("grid.png", "wb");
    fwrite(png.data, 1, png.len, fp);
    fclose(fp);
    plotpx_free_buffer(png);
    return 0;
}

Remember to call plotpx_free_buffer on buffers that PlotPx allocates for you. Spectrum rendering uses PlotpxSpectrumConfig to describe bar styling—refer to src/ffi.rs for the full struct definitions and invariants enforced by the library.

PlotPx ships with a collection of rendered PNGs under docs/examples/. The snippets below show how each image is produced.

Magnitude Gradient

Magnitude gradient

use plotpx::{write_png, Magnitude};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    const W: u32 = 512;
    const H: u32 = 512;
    let mut plot = Magnitude::new(W, H);

    for y in 0..H {
        for x in 0..W {
            let magnitude = (x + y) as f32 / (W + H) as f32;
            plot.add_point(x, y, magnitude);
        }
    }

    let image = plot.render();
    write_png("docs/examples/magnitude.png", &image, W, H)?;
    Ok(())
}

Concentric Wave Magnitude

Concentric wave magnitude

use plotpx::{write_png, Magnitude};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    const W: u32 = 512;
    const H: u32 = 512;
    let mut plot = Magnitude::new(W, H);

    let center_x = W as f32 / 2.0;
    let center_y = H as f32 / 2.0;
    let frequency = 20.0;

    for y in 0..H {
        for x in 0..W {
            let dx = x as f32 - center_x;
            let dy = y as f32 - center_y;
            let distance = (dx * dx + dy * dy).sqrt();
            let max_distance = (center_x * center_x + center_y * center_y).sqrt();
            let normalized_distance = distance / max_distance;
            let magnitude = ((normalized_distance * frequency).sin() + 1.0) / 2.0;
            plot.add_point(x, y, magnitude);
        }
    }

    let image = plot.render();
    write_png("docs/examples/magnitude2.png", &image, W, H)?;
    Ok(())
}

Magnitude Mapped Surface

Magnitude mapped surface

use plotpx::{write_png, MagnitudeMapped};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    const W_DATA: u32 = 128;
    const H_DATA: u32 = 128;
    const W: u32 = 640;
    const H: u32 = 480;
    let mut plot = MagnitudeMapped::new(W_DATA, H_DATA, W, H);

    for y in 0..H_DATA {
        for x in 0..W_DATA {
            let magnitude = (x + y) as f32 / (W_DATA + H_DATA) as f32;
            plot.add_point(x, y, magnitude);
        }
    }

    let image = plot.render();
    write_png("docs/examples/magnitude_mapped.png", &image, W, H)?;
    Ok(())
}

Magnitude Mapped Shrink

Magnitude mapped shrink

use plotpx::{write_png, MagnitudeMapped};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    const W_DATA: u32 = 512;
    const H_DATA: u32 = 512;
    const W: u32 = 256;
    const H: u32 = 256;
    let mut plot = MagnitudeMapped::new(W_DATA, H_DATA, W, H);

    for y in 0..H_DATA {
        for x in 0..W_DATA {
            let magnitude = (x + y) as f32 / (W_DATA + H_DATA) as f32;
            plot.add_point(x, y, magnitude);
        }
    }

    let image = plot.render();
    write_png("docs/examples/magnitude_mapped_shrink.png", &image, W, H)?;
    Ok(())
}

Annotated Magnitude Chart

Annotated magnitude chart

use plotpx::{
    write_png, AxisConfig, BorderColor, ChartAnnotations, ChartTitle, Magnitude,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    const W: u32 = 1024;
    const H: u32 = 576;
    let mut plot = Magnitude::new(W, H);

    for y in 0..H {
        for x in 0..W {
            let fx = x as f32 / W as f32;
            let fy = y as f32 / H as f32;
            let magnitude =
                (fx * std::f32::consts::PI * 4.0).sin() * (fy * std::f32::consts::PI * 2.0).cos();
            plot.add_point(x, y, magnitude);
        }
    }

    let mut annotations = ChartAnnotations::default();
    annotations.border_color = BorderColor::White;
    annotations.title = Some(ChartTitle::new("Sine Wave Interference"));
    annotations.x_axis = Some(
        AxisConfig::new("Horizontal Position", 0.0, W as f32)
            .with_units("px")
            .with_tick_count(6)
            .with_decimal_places(0),
    );
    annotations.y_axis = Some(
        AxisConfig::new("Vertical Position", 0.0, H as f32)
            .with_units("px")
            .with_tick_count(5)
            .with_decimal_places(0),
    );

    let chart = plot.render_default_with_annotations(&annotations);
    write_png(
        "docs/examples/magnitude_annotated.png",
        &chart.pixels,
        chart.width,
        chart.height,
    )?;
    Ok(())
}

Spiral Grid

Spiral grid

use plotpx::{make_color_scheme, write_png, MagnitudeMapped, MagnitudeMappedGrid, Rgba};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    const INPUT_SIZE: u32 = 200;
    const PLOT_SIZE: u32 = 150;
    const GRID_SIZE: usize = 4;

    let mut grid = MagnitudeMappedGrid::new(GRID_SIZE, INPUT_SIZE, INPUT_SIZE, PLOT_SIZE, PLOT_SIZE);

    for row in 0..GRID_SIZE {
        for col in 0..GRID_SIZE {
            let plot = grid.get_plot(row, col);
            let num_points = 150 + (row * 40) + (col * 30);
            let turns = 1.5 + (row as f32 * 0.4) + (col as f32 * 0.2);
            let spiral_points = generate_spiral(INPUT_SIZE, INPUT_SIZE, num_points, turns);
            plot_spiral(plot, &spiral_points, 1.0);
        }
    }

    let palette: Vec<Rgba> = vec![
        [20, 0, 100, 255],
        [50, 0, 200, 255],
        [0, 100, 255, 255],
        [0, 200, 200, 255],
        [0, 255, 100, 255],
        [100, 255, 0, 255],
        [200, 255, 0, 255],
        [255, 200, 0, 255],
        [255, 100, 0, 255],
        [255, 0, 100, 255],
        [200, 0, 200, 255],
    ];

    let colors = make_color_scheme(&palette, 128);
    let image = grid.render(&colors);
    write_png(
        "docs/examples/spiral_grid.png",
        &image,
        PLOT_SIZE * GRID_SIZE as u32,
        PLOT_SIZE * GRID_SIZE as u32,
    )?;
    Ok(())
}

fn generate_spiral(width: u32, height: u32, num_points: usize, turns: f32) -> Vec<(f32, f32)> {
    let mut data = Vec::with_capacity(num_points);
    let center_x = width as f32 / 2.0;
    let center_y = height as f32 / 2.0;
    let max_radius = width.min(height) as f32 / 2.0;

    for i in 0..num_points {
        let t = i as f32 / num_points as f32;
        let angle = turns * 2.0 * std::f32::consts::PI * t;
        let radius = max_radius * t;
        let x = center_x + radius * angle.cos();
        let y = center_y + radius * angle.sin();
        data.push((x, y));
    }

    data
}

fn plot_spiral(plot: &mut MagnitudeMapped, points: &[(f32, f32)], intensity: f32) {
    plot.reset();
    for &(x, y) in points {
        let px = x as u32;
        let py = y as u32;
        if px < plot.input_width && py < plot.input_height {
            plot.add_point(px, py, intensity);
        }
    }
}

Heatmap

Heatmap

use plotpx::{write_png, Heatmap};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    const W: u32 = 512;
    const H: u32 = 512;
    const NPOINTS: usize = 600;
    let mut heatmap = Heatmap::new(W, H);

    let data = generate_spiral(W, H, NPOINTS, 10.0);
    for (x, y) in data {
        heatmap.add_point(x as u32, y as u32);
    }

    let image = heatmap.render();
    write_png("docs/examples/heatmap.png", &image, W, H)?;
    Ok(())
}

fn generate_spiral(width: u32, height: u32, num_points: usize, turns: f32) -> Vec<(f32, f32)> {
    let mut data = Vec::with_capacity(num_points);
    let center_x = width as f32 / 2.0;
    let center_y = height as f32 / 2.0;
    let max_radius = width.min(height) as f32 / 2.0;

    for i in 0..num_points {
        let t = i as f32 / num_points as f32;
        let angle = turns * 2.0 * std::f32::consts::PI * t;
        let radius = max_radius * t;
        let x = center_x + radius * angle.cos();
        let y = center_y + radius * angle.sin();
        data.push((x, y));
    }

    data
}

Spectrum (Gradient Bars)

Spectrum sine

use plotpx::{write_png, BarStyle, Spectrum};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    const BINS: u32 = 256;
    const W: u32 = 640;
    const H: u32 = 360;
    let mut plot = Spectrum::new(BINS, W, H);
    plot.style = BarStyle::Gradient;
    plot.show_peaks = true;
    plot.bar_width_factor = 0.8;

    let mut magnitudes = vec![0.0f32; BINS as usize];
    let peak_bin = 64usize;
    magnitudes[peak_bin] = 1.0;

    for i in 1..=10 {
        if peak_bin >= i {
            magnitudes[peak_bin - i] = 1.0 / (i * i) as f32;
        }
        if peak_bin + i < BINS as usize {
            magnitudes[peak_bin + i] = 1.0 / (i * i) as f32;
        }
    }

    plot.update(&magnitudes);
    let image = plot.render();
    write_png("docs/examples/spectrum_sine.png", &image, W, H)?;
    Ok(())
}

Spectrum (Inferno)

Spectrum complex

use plotpx::{make_color_scheme, write_png, BarStyle, Spectrum, INFERNO_KEY_COLORS};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    const BINS: u32 = 256;
    const W: u32 = 640;
    const H: u32 = 360;
    let mut plot = Spectrum::new(BINS, W, H);
    plot.style = BarStyle::Solid;
    plot.show_peaks = true;
    plot.bar_width_factor = 0.9;

    let mut magnitudes = vec![0.0f32; BINS as usize];
    let peaks = [32usize, 64, 96, 128, 192];
    let amplitudes = [0.5f32, 1.0, 0.7, 0.3, 0.8];

    for (idx, &peak_bin) in peaks.iter().enumerate() {
        let amplitude = amplitudes[idx];
        magnitudes[peak_bin] = amplitude;

        for i in 1..=5 {
            if peak_bin >= i {
                magnitudes[peak_bin - i] = amplitude / (i * i) as f32;
            }
            if peak_bin + i < BINS as usize {
                magnitudes[peak_bin + i] = amplitude / (i * i) as f32;
            }
        }
    }

    plot.update(&magnitudes);
    let image = plot.render_with_colors(&make_color_scheme(INFERNO_KEY_COLORS, 128));
    write_png("docs/examples/spectrum_complex.png", &image, W, H)?;
    Ok(())
}

The generating code also lives in src/main.rs inside the tests::generates_example_gallery integration test. Regenerate all artefacts with:

cargo test -- --nocapture

Customising color schemes

Use make_color_scheme to resample a list of RGBA knot points. The helper returns an evenly spaced palette that matches the renderer’s expectations.

Development & Testing

  • cargo fmt – format the Rust sources
  • cargo clippy --all-targets --all-features – lint the codebase
  • cargo test – run unit and integration tests (also rebuilds the PNG gallery)
  • julia --project=julia/PlotPx -e 'using Pkg; Pkg.test()' – exercise the Julia wrapper against the shared library

License

PlotPx is distributed under the MIT License (LICENSE). The bundled font RobotoMono-SemiBold.ttf is provided by Google Fonts under the Apache 2.0 license.

Dependencies

~2.5MB
~47K SLoC