# 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:
```bash
cargo add plotpx
```
If you are working from a local checkout instead, use a path or git dependency:
```toml
[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:
```bash
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.
```bash
julia --project=julia/PlotPx -e 'using Pkg; Pkg.develop(path="julia/PlotPx"); Pkg.instantiate()'
```
You can now load the wrapper from Julia:
```julia
julia --project=julia/PlotPx
julia> using PlotPx
julia> PlotPx.load_plotpx() # downloads artifacts or uses target/release
```
## Quickstart (Rust)
```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)
```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:
```c
#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.
## Example Gallery
PlotPx ships with a collection of rendered PNGs under `docs/examples/`. The snippets below show how each image is produced.
### Magnitude Gradient

```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 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

```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);
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

```rust
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

```rust
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

```rust
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

```rust
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

```rust
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)

```rust
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)

```rust
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:
```bash
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.