Skip to main content

This package provides functionality for rendering visualizations using Pluot

Project description

pluot

Crates.io Link and Latest Version Info

pluots

Goal: Implement a data visualization once, then render it in multiple contexts (across languages, static or interactive, bitmap or vector).

Rust, Python, and JavaScript (including in a web browser) are currently supported. Additional bindings are future work.

How it works: "headless" plotting. Pluot uses Rust :crab: and WebGPU to quickly render plots to an array of pixels (or an SVG string), decoupled from any windowing system or other language runtime. On each "frame" of an interaction or animation, we re-render with updated plotting parameters, and we hope rendering will be fast enough.

:test_tube: Pluot is new and experimental.

Citation

If you found this useful, please cite:

@article{keller2026pluot,
  title = {{Pluot: Towards 'write once, run everywhere' visualization software}},
  author = {Keller, Mark S. and Gehlenborg, Nils},
  journal = {arXiv},
  year = {2026}
}

Features

  • Small: The bundle size (i.e., the WASM binary size) is small (currently less than 4MB) to make it feasible to integrate into web applications.
  • Scalable: Scales to out-of-memory dataset sizes using partial reads of arrays/columns and data tiling/aggregation strategies (currently using Zarr via zarrs to achieve this).
  • Language bindings: Usable from multiple languages, including JavaScript/TypeScript (via WASM) and Python (via PyO3/maturin bindings).
  • Bitmap or Vector Outputs: Plotting functions can implement bitmap and vector equivalent drawing logic, to support publication-quality graphics export.
  • Layer-based API: Compose the built-in layers to create complex plots, or build your own layers with full control over the WebGPU shaders, buffers, and draw calls. Usage of WebGPU compute (GPGPU) operations prior to each layer's draw call is also supported.

⚠️ Pluot does not yet implement very many "chart types". Thus, expect it to currently take some effort to build things using Pluot (similar to using a low-level visualization toolkit such as D3). However, if you do put in such effort, you will be able to render the plot whereever Pluot rendering works: from Rust, Python, JavaScript, a web application, a rust-based desktop GUI, or additional language bindings developed in the future.

How it works

Plotting functions are implemented in Rust using the wgpu implementation of WebGPU (Note: wgpu can be used as a standalone WebGPU renderer, decoupled from any web browser), when plotting to raster-based outputs or performing GPGPU compute operations. These Rust plotting functions are only concerned with producing a "static" plot output, given their input parameters and data.

When the language bindings are used, you can think of this as a form of "remote rendering", which is actually happening locally; rather than the "remote" being a far-away server, it is just across the language binding boundary.

Why would I want to use this? Why not just use JS+WebGPU directly?

The main reasons are:

  • Portability: render plots from multiple languages, without the overhead of an interpreted programming language runtime
  • Reproducibility: explore data using an interactive tool (e.g., web or desktop GUI) to identify plotting parameters of interest, and then use the same parameter values in a scripting language to reproduce the visualization as a static plot (e.g., a Python script in a Snakemake pipeline)

Using WebGPU via JavaScript would couple things to JavaScript, which we do not want for a library that should be usable in multiple languages, including without a JS runtime. Our approach enables our CPU-based operations to benefit from the performance characteristics of Rust (or, in web contexts, at least those of Rust-via-WASM).

You can likely achieve better performance by using WebGPU directly via JavaScript. The question is whether the performance of this Rust-based approach is good enough, and whether the benefits are worth the potential performance tradeoffs for your use case.

Non-goals

  • Window/Canvas/Eventloop management via Rust. This should be handled by the parent/calling code. The Rust code should be concerned with returning the rendered bytes, which can be written to HTML Canvas or saved to a file by the calling library. This both reduces the scope and decouples the plotting from any particular GUI framework. However, it does complicate certain things, like rendering while asynchronously loading data.
  • Coordinated multiple views. This can be achieved via the parent/calling library, for example, by wrapping with use-coordination or your favorite state management library.
  • WebGL fallbacks via the webgl feature of WGPU. Why not: this both increases the WASM bundle size in order to include the shader translation code paths, and it requires providing the compatible_surface parameter when initializing the WGPU Instance (which conflicts with our first non-goal of avoiding canvas management).
    • As a workaround, environments that do not support WebGPU can either use the slower SVG-based rendering, or future CPU-based raster rendering (#157).
    • Alternatively, tell the user to use a different browser/platform, or just be patient and wait for WebGPU support to become more widespread.
      • WebGPU is available in all major browsers already (implementation status, web.dev article).
        • Caveat: WebGPU is only available in Safari on macOS Tahoe 26 and above. According to one April 2026 estimate of macOS version market share, approximately ~70% of macOS users are running Tahoe or later. Until certain design issues are resolved by Apple, I suspect that a subset of users will resist upgrading to Tahoe for as long as possible.

Development

Further developer documentation can be found in dev-docs.

Set up environment

Install Rust tools with Rustup.

# Install rustup
cargo install wasm-pack
cargo build

# Install pnpm
# may need to run `wasm-pack build crates/pluot --target web` first
pnpm install

# Install uv

# Generate/download sample data
# See data/README.md

uv sync --extra dev

Build for WASM

# Install nightly version of wasm-bindgen CLI (potentially not needed anymore)
# Reference: https://github.com/wasm-bindgen/wasm-bindgen/issues/4446#issuecomment-3172624621
cargo install --git https://github.com/rustwasm/wasm-bindgen --rev b766ac3e206a8efab2c7cf91923cd502b2bc77a5 wasm-bindgen-cli

wasm-pack build crates/pluot --target web && pnpm run start-demo
# or
wasm-pack build crates/pluot --dev --target web && pnpm run start-demo
# or
wasm-pack build crates/pluot --release --target web && pnpm run start-demo

Test in Headless Browsers with wasm-pack test

wasm-pack test --headless --chrome crates/pluot
# or
wasm-pack test --headless --chrome crates/pluot -- --nocapture
# or
wasm-pack test --chrome crates/pluot
# or
wasm-pack test --firefox crates/pluot

Build for Python

uv sync --extra dev

Build:

uv run maturin develop --features python

Run tests:

uv run pytest

Use in REPL:

uv run python -m asyncio
>>> from pluot import render_py
>>> await render_py(width=100, height=100, plotId="test", plotType="triangle", storeName="test")

Try in Marimo notebook:

uv run marimo edit

Try in Jupyter notebook:

uv run jupyter lab --notebook-dir bindings-python/notebooks

Build for plain Rust

cargo build

Run tests

cargo test
# or
cargo test --features lacks_gpu
# or, run a specific test file
cargo test -p pluot_core --test test_positioning

Lint with clippy

cargo clippy
cargo clippy --fix

Generate crate docs locally

cargo doc --no-deps
open target/doc/pluot/index.html

Inspired by

This work has been informed by my experiences in contributing to projects including vitessce, use-coordination, viv, cistrome-explorer, deck-to-svg, higlass, vueplotlib, and easy_vitessce.

It is also inspired by many other projects such as deck.gl, deck.gl-native, jupyter-scatter, gosling, napari-spatialdata, spatialdata-plot, and scanpy.

Related work

See awesome-rust-vis for a list of crates related to data visualization and plotting.

About the name

A pluot is a plum-apricot hybrid. The fruit's pit is to its flesh as the Rust core of this project is to its non-Rust bindings.

Rust learning resources

License

See LICENSE and NOTICE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pluot-0.1.0.tar.gz (352.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pluot-0.1.0-cp312-cp312-macosx_11_0_arm64.whl (4.1 MB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

File details

Details for the file pluot-0.1.0.tar.gz.

File metadata

  • Download URL: pluot-0.1.0.tar.gz
  • Upload date:
  • Size: 352.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.8.16

File hashes

Hashes for pluot-0.1.0.tar.gz
Algorithm Hash digest
SHA256 9f14e54380e8f92658f1f5643f400c5bf04faf64d38865b60d0433d52eba479c
MD5 e73fcdeeb650cea3ffdebe4fb4914466
BLAKE2b-256 c4462d3c45411faeda5a1a6ec57ca7abd49090749020e59affd28703ee20eba0

See more details on using hashes here.

File details

Details for the file pluot-0.1.0-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for pluot-0.1.0-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 d23c7e1254da887bcd5ebd11d57d2efa84cc4cc86a69366ac91793a702316d18
MD5 adf3646a34b4c93288f5d2dff4857ae7
BLAKE2b-256 ce96cca982c443db61c5c7c18be0af53fe84daf3f4f4a05721719b21ef34f909

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page