Why implement a data visualization toolkit in Rust?
I am happy to share Pluot, a new library for creating interoperable data visualizations.
Update (August 2026): I have updated this post to reframe the problem and interleave slides from a recent presentation.
Motivations
The size and complexity of modern datasets, such as those generated during scientific experiments, often cannot be easily distilled into a small number of static figures (e.g., figures within a journal paper). While journal figures remain valuable precisely because they require the original researchers to compress their findings into those that they perceive as most impactful, each reader of a scientific paper also arrives with their own interests and questions (e.g., their own favorite gene).
Interactive data portals aim to address this problem: a user can arrive with their own questions, querying large and high-dimenensional scientific datasets and rendering visualizations on-demand. The web-based nature of many such data portals also democratizes access: the user does not need to fiddle with installing a native app and does not need to download the (increasingly large) datasets in order to view the visualizations.
The problem
The development of interactive visualization tools and the creation of static publication figures are complementary processes. However, these processes are most often performed with separate software toolkits and programming languages, sharing almost zero code. Yet these processes often rely on the same input data and data formats, use similar visualization designs, and have overlapping audiences.
Thinking about these as separate processes (i.e., exporation and hypothesis generation -> interactive; confirmation and communication -> static) makes it easier to justify the usage of different software toolkits during them. We tell ourselves that using a WebGL-based interactive tool in the former phase and creating a ggplot in the latter phase is perfectly fine!
And it is fine in many cases. But it gnaws at me to pour time and effort into developing specialized software to power web-based interactive tools, while knowing that entirely separate software toolkits will be used to create any downstream static publication figures.
So why does the field of visualization software need to keep reinventing the wheel?
The wish list
Why not map out the design space and create a solution that satisfies everyone's needs?
Looking more broadly at how we select visualization toolkits, there are often additional dimensions to consider besides simply "static vs. interactive". We often also need to consider the preferred programming language, whether we want bitmap vs. vector graphics, and whether we are rendering basic statistical plots vs. volumes/meshes/3D elements ("InfoVis vs. SciVis").
Based on my experiences, the following characteristics are required for any such solution:
- The same code can be used across programming languages, including in a web browser, but without requiring a web browser in the desktop case.
- The same code can power static and interactive visualization rendering.
- The same code can be used for bitmap and vector graphics output.
- Small WASM binary size (disclaimer: subjective).
- Scalability to out-of-core dataset sizes.
- Decoupled from coordinated multiple views implementation.
- Decoupled from UI framework and event loop management.
(The latter two criteria simplify the scope of the problem and allow for developers to use their favorite state management and UI frameworks, which also change rapidly over time.)
Satisfying such a wish list has historically been difficult, primarily for technical reasons.
The proof of concept
In a 2024 report titled "The Moving Target of Visualization Software", Gillmann et al. remark on the potential for WebGPU and WebAssembly to be used to implement "write once, run everywhere" visualization software.
WebAssembly allows us to efficiently run software developed using systems programming languages in the web browser, and the new WebGPU API offers an ergonomic way to do GPU-accelerated rendering in the web browser. Specifically, the Rust programming language ecosystem offers tools to easily compile Rust code to WebAssembly (wasm-bindgen, etc) and wgpu is a Rust-based implementation of WebGPU that can be run both inside and outside the browser (in fact, wgpu is how the WebGPU API is implemented under-the-hood in Firefox). By the end of 2025, WebGPU was supported in all three major web browsers.
Side note: "Great," you say! We can just take our existing native apps for visualization and compile them to run in the browser with WebAssembly! While possible, this is much easier said than done. These native applications are often large and complex, built over a long time assuming that all of the usual native app capabilities are available: filesystem, concurrency, etc. When building for the web there are additional constraints to consider, such as the application bundle size and user network speed.
For better or worse, rather than trying to squeeze a legacy native application code into these constraints it was not designed to satisfy, it can be quicker (and with fewer headaches) to build a solution from scratch, with the constraints considered from the beginning.
An alternative approach is to do the opposite: reuse existing web-based visualization code in native contexts. If you want to avoid depending on a full web browser, the best way to do this is via a standalone JavaScript runtime, as done by vl-convert. The advantage of this approach is that you can reuse most JavaScript code (caveat: the JS code must not be too tightly coupled to the browser), making it great from a reproducibility standpoint. The disadvantage is that there is still some overhead to using the JavaScript runtime (disclaimer: it would require benchmarking to confirm the exact performance tradeoffs). For many users and use-cases, this may be plenty good enough.
So, we can now implement our visualizations in Rust using WGPU, but what about usage from data science environments? Luckily, it is straightforward to create bindings to Rust programs from other languages such as Python.
The next key insight (or maybe just lie I told myself) is that it only takes a few key elements to go a long way to creating a visualization. Just being able to render circles, rectangles, and text can allow us to implement scatterplots, line charts, and bar charts! When it comes to interactivity, there are also only a small number of crucial interactions: clicking, hovering (tooltips), scrolling (zooming), and dragging (panning, brushing, lassoing). Seems easy enough!
We just need to implement a plot rendering function, and return the bytes back to the calling language! If this is fast enough, then it will be feasible to execute such a rendering function on every frame (of a user interaction or animation), enabling us to use the same plotting function to generate both interactive and static plots. And with a little extra work, we can also implement SVG variants of each plotting function.
I set out to implement a proof of concept to determine whether this was feasible. I made my initial commit in August of 2025, marking the start of the Pluot project. A week later, I had gotten some (very basic) interactive Rust+WGPU rendering working in the browser via WASM bindings. With a few more days, I got proof-of-concept Python bindings working. However, it took until early 2026 to polish this proof of concept and get many other features working: text rendering, async data loading, cacheing, coordinate systems, aspect ratio handling, margins, vector rendering, R bindings, and a layered plotting API -- to the point that I can now claim Pluot addresses the goals of the original wish list.
Embracing stateless, declarative paradigms
In addition to WebGPU, WebAssembly, and the Rust ecosystem (pyo3, rextendr), the Pluot architecture is made possible in large part due to embracing stateless, declarative paradigms. For example, we treat interactive plotting as rendering multiple static frames.
We treat camera updates upon zoom/pan interactions as stateless functions which return a new camera matrix, given the previous camera matrix and the mouse event.
Inspired by declarative grammars of graphics and frameworks such as DeckGL, we define a layered plotting API. This layered API makes it easy to compose existing layers to define more complex layers.
Scalable data loading
To scale to out-of-memory dataset sizes, we use Zarr via the Zarrs Rust implementation.
Crucially, we use Zarr to load not only data that conforms to the Zarr array format, but also to load arbitrary binary data.
The composable nature of Zarr stores, enabling virtualization from legacy formats without modifying data on-disk, has also made Zarr a great fit for this project. It also helps that the datasets I work with in the single-cell and spatial biology domain and their communities have adopted Zarr, namely, AnnData, SpatialData, and OME-Zarr.
Reproducibility benefits
In addition to rendering graphical outputs, the Pluot Rust core provides a render-to-script function. In other words, given the same declarative inputs which are used to generate a static or interactive plot, we can ask Pluot for a Python/R/Rust/Bash/JS/JSX/HTML script or expression that, when executed, renders the same plot.
You can try this in any of the examples on the Pluot documentation website, using the Plot Controls dropdown:

When discussing reproducibility, I am primarily thinking about workflows which transition from an interactive, GUI-based tool to the creation of static publication figures. Currently, this could involve taking a screenshot or clicking an "Export to PNG/SVG" button, neither of which is very reproducible (and would require some kind of browser automation to make it so). To make things reproducible, we could alternatively define a Python/R script that generates a static plot and run it within a pipeline (e.g., with Snakemake or Nextflow). But in the absence of an interoperable/cross-platform toolkit, such a script will require re-implementing the visualization using an alternative toolkit such as ggplot or matplotlib, and is therefore time consuming and involves context-switching between web-based interactive and non-web-based static plotting toolkits and their mental models.

Related work
As I went down this rabbit hole, I kept thinking to myself that I couldn't be the first person to think about this problem. I came across many projects that seemed quite similar, but none that seemed to check all of the boxes on the wish list. The small WASM bundle size requirement turns out to be especially tricky, as it is easy to inadvertently add Rust crate dependencies that significantly increase the resulting WASM bundle size. Similarly, it is easy to add Rust crate dependencies that make WASM compilation difficult for one reason or another (e.g., depend on a C library that is tricky to compile to WASM) or use concurrency features (Rust's Send and Sync) in a way that prevents WASM compilation. Identifying this gap motivated me to continue to push the project along, especially once I had established the feasibility via the initial experiments.
Conclusion
While not yet production-ready, Pluot demonstrates the potential for a paradigm shift in how we create reproducible, performant, and cross-platform static and interactive visualizations.
Of course, developing a new plotting library is always a bit like the classic XKCD comic:
Is this useful, or just an additional competing thing? I will leave it to you to decide.
Learn more
Here, I have focused on the personal motivations behind this project and a few technical details.
If you are interested in learning more, check out the Pluot repository. There are still many open questions, some of which are listed in the GitHub issues.
