Why another plotting library?
JavaScript has no shortage of plotting libraries. But scientific computing needs more than a way to render charts, so we're building a plotting system designed around the entire workflow.
JavaScript does not suffer from a shortage of ways to draw a chart. There are mature libraries for quick statistical graphics, dashboards, bespoke data-driven documents, publication-quality figures, and nearly every use case in between. Some are small and focused. Others are entire ecosystems in their own right.
So, naturally, stdlib is building another one.
At first glance, this may look like a textbook case of not-invented-here. It is not. We are not building a plotting library because we think the existing libraries are bad. Quite the opposite: JavaScript has some of the best visualization tooling of any programming ecosystem. We are building one because stdlib is trying to solve a different problem.
The gap we care about is not at the level of pixels. Current JavaScript charting libraries can draw lines, symbols, axes, legends, and tooltips just fine. The gap is at the level of workflow: how plotting fits into numerical computation across the browser, the REPL, the command line, on the server, and in desktop windows.
That is the problem we have not seen addressed as one coherent system.
TL;DR
We are not building another rendering engine.
We are building a plotting layer for scientific JavaScript: one which understands n-dimensional arrays (a.k.a., ndarrays), works naturally in the Node.js REPL, can render to an Electron-powered desktop window, participates in command-line workflows, can be embedded in a browser, exposes a programmatic event model, and provides a path from exploratory analysis to publication-quality graphics.
For rendering and interaction, we are building on Vega, a declarative visualization grammar and runtime which produces canvas and SVG output in the browser, and supports static rendering on the server. Vega gives us a robust target specification and rendering backend. stdlib provides the scientific data model, ergonomic plotting APIs, runtime integrations, object model, viewers, editing tools, documentation, testing, and long-term compatibility layer around it.
Put another way: Vega handles the pixels. We are building the workflow.
Start where the computation happens
Most JavaScript visualization libraries treat the browser as the center of the universe. That makes sense. JavaScript was born in the browser, and the browser provides a rich environment for interaction, layout, animation, and graphical output. For many applications, targeting a DOM element on a web page is the right abstraction.
But stdlib does not live only in the browser.
It also lives in Node.js scripts, REPL sessions, command-line programs, batch jobs, data pipelines, web services, and other server-side environments. If plotting is going to be a first-class part of scientific computing in JavaScript, it needs to feel at home in all of those places.
When I am exploring a dataset, I do not want to create a browser application just to see whether two variables are correlated. I do not want to scaffold a project, write HTML, start a development server, move my data across an application boundary, and attach a chart to a DOM node. I want to start the stdlib REPL, load my data, create a plot, and keep working.
That should not be a radical request.
The plotting work currently underway in stdlib deliberately separates a chart from how that chart is viewed. The same chart can target stdout, a browser, or an Electron-powered desktop window. A browser remains a first-class destination, but it is not assumed to be the only destination.
Programmatic environments such as REPLs and interactive notebooks are fundamental to exploratory data analysis. Filtering, reshaping, visualizing, and interrogating data is inherently iterative, and requiring a separate browser application introduces unnecessary friction into that loop. Here, an ndarray created in the stdlib REPL is passed directly to a chart constructor and rendered in a browser, after which programmatic changes to the chart are propagated automatically to the rendered view.
This matters for exploratory data analysis. Plotting is not merely a final presentation step. It is part of the feedback loop through which we inspect data, question assumptions, identify structure, discover errors, and decide what to compute next. A plotting library for scientific computing should begin where the computation happens, not require users to move the computation somewhere else.
Numerical data is not always a JSON table
A great deal of JavaScript visualization is organized around arrays of objects (often referred to as "tidy datasets"; e.g., see Vega and Observable Plot):
[
{ 'x': 1.0, 'y': 3.2 },
{ 'x': 2.0, 'y': 4.8 },
{ 'x': 3.0, 'y': 4.1 }
]
That is a perfectly reasonable interchange format centered on JSON. It is readable, flexible, and maps naturally to rows and fields. It is not, however, necessarily the data model used by a numerical program.
Scientific code routinely works with typed arrays, multi-dimensional arrays, strided views, non-contiguous memory layouts, stacks of matrices, and arrays with explicit shape and data type information. Those objects are not incidental implementation details. They are fundamental abstractions around which numerical software is built.
In stdlib, we want users to be able to provide the same ndarray objects they use for statistics, linear algebra, simulation, and signal processing directly to plotting APIs. Users should not need to manually flatten, reshape, transpose, copy, or serialize their data just to look at it.
Our work-in-progress line-chart implementation already accepts ndarray-like inputs, determines how to organize those inputs as chart data, and lowers the result into Vega datasets, scales, axes, and marks.
const uniform = require( '@stdlib/random/uniform' );
const Float64Vector = require( '@stdlib/ndarray/vector/float64' );
const LineChart = require( './../lib' );
// Define minimum lower and upper bounds for uniformly distributed random numbers:
const a = new Float64Vector( [ 10, 20, 30, 40, 50 ] );
const b = new Float64Vector( [ 20, 30, 40, 50, 60 ] );
// Generate 5 datasets containing 100 rows of data:
const y = uniform( [ 100, 5 ], a, b );
// Create a new chart instance:
const chart = new LineChart( y );
This is not the sort of feature which produces a flashy landing-page demo. But it is foundational.
A plotting library within stdlib should understand the data structures produced by the rest of stdlib. Anything less would make plotting feel like an attached application rather than an integrated part of the numerical computing environment.
A plot should be a live computational object
A plot is often treated as a terminal artifact: provide some data and options, create an SVG or canvas element, and move on. That model is useful, but incomplete.
Plots change. Data arrives. Parameters are updated. Axes are rescaled. Themes are switched. A server may need to notify a connected client. A host application may need to export an image after a computation finishes. A stream may need to trigger an update without any DOM event occurring.
Vega signals provide an excellent reactive model within the visualization runtime. Signal values can respond to input events, external API calls, and upstream changes, with updated values propagating through the visualization dataflow. But signals inside a visualization runtime are not a replacement for a general-purpose event model across a JavaScript object graph.
In our current work, stdlib plotting objects use event emitters throughout the programmatic API. Changes to child objects propagate upward, allowing a chart, server, viewer, logger, or another consumer to respond. A change can result in a visual update, but it could just as readily result in writing a file, emitting a message over a socket, recording an audit log, or initiating some other non-DOM action. This is particularly important on the server, where there may be no user-interface element to bind and no mouse or keyboard event to observe.
It is also why we are investing in debug logging throughout the plotting stack. Once a visualization becomes reactive, understanding what changed, where that change originated, and why a render occurred becomes essential. Plotting should not be a black box which mysteriously redraws itself. Failure should be easy to diagnose.
In other words, we are treating a plot as a live computational object, not just a picture.
Code and GUI should meet in the middle
There is a longstanding tension in scientific plotting between code and graphical editing. Code is reproducible. It can be reviewed, versioned, rerun, tested, and applied consistently across many figures. A graphical interface, however, can be much more efficient for the final stages of figure creation: adjusting padding, changing font sizes, refining tick marks, moving a legend, modifying line styles, and making the dozens of small visual decisions which turn a serviceable plot into a publication-quality graphic.
Most tools force users to choose. Either every visual detail must be expressed in code, even when direct manipulation would be faster, or a figure is edited in a graphical application and the relationship between the resulting artifact and the code which produced it is severed.
We want both.
The current in-progress plotting work contains an early graphical editor which reads from a visualization schema, exposes controls for chart properties, and posts changes back to a server-side plot. The underlying schema remains the source of truth. Programmatic changes can update the editor, and editor changes can update the programmatic object.
A graphical user interface (GUI) allows chart properties to be edited directly while keeping the underlying chart object synchronized with the rendered view. Changes made in the GUI are reflected in the chart instance running in the REPL, while programmatic changes made in the REPL are reflected in the browser. This two-way synchronization makes it possible to combine the reproducibility of a programmatic workflow with the immediacy of direct manipulation when iterating on a visualization.
This work is still early, but the direction is important. The goal is not to replace code with a GUI. It is to make the GUI another frontend to the same underlying representation.
Knowing when to stop building everything ourselves
At this point, an obvious question arises. If we are building so much custom plotting infrastructure, why build on Vega at all? Why not implement the renderer ourselves?
We tried.
stdlib has a strong bias toward building foundational infrastructure in-house. That tendency has served us well. It gives us control over API design, behavior, performance, portability, testing, documentation, and release cadence.
Our original plotting implementation followed that instinct. We built SVG components, virtual-DOM infrastructure, scales, axes, marks, layout logic, and rendering machinery. It worked. It also made increasingly clear just how much work remained.
A serious visualization renderer needs to handle scenegraphs, data joins, text measurement, clipping, layout, rendering order, incremental updates, animation, browser differences, event handling, image export, accessibility, canvas, SVG, server-side rendering, and a seemingly endless collection of edge cases. We could keep investing in all of that. But doing so would mean spending an enormous amount of time reproducing infrastructure which already exists and is maintained by people who have devoted years to solving those exact problems.
At some point, persistence becomes stubbornness. So we folded that hand.
The pull request containing the current work explicitly removes the previous virtual-DOM plotting implementation and replaces it with packages for constructing and generating Vega specifications. That does not mean the previous work was wasted. Quite the opposite. It taught us where the correct abstraction boundary should be.
We do not need to own every pixel in order to own the user experience. Rendering is not where stdlib can add the most value. Integrating plotting into scientific JavaScript is.
Why Vega?
Vega is described as a visualization grammar. Rather than directly issuing imperative drawing commands, a Vega visualization is described using a JSON specification containing data, scales, axes, legends, marks, signals, and other components. The specification is then parsed into a reactive dataflow and rendered by the Vega runtime.
That distinction makes Vega a particularly good fit for what we are building.
A portable intermediate representation
We treat a Vega specification as an intermediate representation, which is one of the stated goals of Vega: namely, to provide a "target language" in which computer programs can dynamically generate visualizations.
Conceptually, the architecture looks something like this:
high-level chart future plotting
constructors DSLs
\ /
\ /
low-level builders GUI editor
\ /
\ /
Vega specification
|
Vega runtime
|
SVG / Canvas / other renderers
A high-level line-chart constructor does not need to know how to create SVG paths, draw onto a canvas context, or generate shaders. It needs to know how to translate the user's intent and data into a valid visualization specification. Similarly, a graphical editor does not need its own rendering engine. It can modify the same specification generated by a programmatic API.
This is not an untested architectural model. Vega-Lite is itself a higher-level grammar which compiles to Vega. Lyra was an initial attempt at a graphical environment for custom visualization design without writing any code. We are applying a similar separation, but building a different frontend, one which is shaped around JavaScript numerical computing, stdlib conventions, and direct integration with stdlib data structures, such as ndarrays.
Vega specifications also give us portable artifacts which can be inspected, serialized, stored, shared, transformed, and handed to other tools. Because Vega and Vega-Lite sit underneath tools in other ecosystems, including Altair and VegaFusion in Python, the specification provides a natural bridge beyond JavaScript as well.
The schema becomes a boundary rather than a burden.
Multiple rendering environments
Vega supports canvas and SVG rendering in the browser and static SVG and PNG export on the server. Server-side SVG generation does not require constructing a server-side DOM. Vega provides multi-view layout machinery, configuration-based theming, and explicit accessibility properties for SVG output. Third-party work has additionally demonstrated a WebGL renderer for the Vega scenegraph.
Those capabilities would each represent substantial projects in their own right. By building on Vega, we inherit a rendering foundation which already spans the environments stdlib cares about. We can then spend our time on the problems which are specific to scientific computing: data adapters, chart semantics, numerical types, server integration, REPL workflows, command-line composition, and user-facing APIs.
Reactive behavior on the client
Vega's signals and reactive dataflow allow many updates to be resolved directly within the client-side runtime. Not every change should require sending an entirely new specification from a server. A line color, tick size, font weight, or opacity can often be updated without rebuilding the world.
The current in-progress stdlib view implementation already distinguishes between properties which can be updated dynamically and structural changes which require the specification to be re-parsed. This gives us a useful division of labor. stdlib can own the server-side object model and change propagation, while Vega can handle reactive visual updates close to where rendering occurs and facilitate a more responsive and snappier user experience.
An ecosystem rather than an island
Depending on a rendering backend is not a decision we take lightly. Visualization libraries are expensive dependencies. They have large surface areas, and choosing one means placing a certain amount of trust in its architecture, maintainers, and continued development. Vega has continued to evolve, remains actively maintained, and supports a broader ecosystem including Vega-Lite, Altair, VegaFusion, Vega-Embed, Vega Themes, and the Vega Editor.
That matters.
We are not tying stdlib to an isolated renderer which only we depend upon. We are aligning with a specification and runtime already shared across languages, applications, and communities.
Vega is the backend, not the user-facing API
One might reasonably ask: why not simply tell users to write Vega specifications directly? Our answer is that, while powerful, Vega is also low-level.
Creating even a conventional chart can require thinking about datasets, scales, domains, ranges, axes, marks, encoding sets, value references, signals, and layout, and then trying to encode that in JSON. That level of control is invaluable when building novel visualizations. It is unnecessary ceremony when someone simply wants to plot x against y. A scientist should not need to learn a visualization grammar before inspecting an array.
stdlib's role is to provide ergonomic plotting APIs inspired by established numerical environments such as MATLAB and matplotlib, while retaining the ability to reach for the full expressive power of Vega when needed. At the highest level, that may mean static chart constructors which make common plots easy. At a lower level, it means programmatic objects corresponding to Vega concepts such as data sources, scales, axes, marks, and encoding sets. Those objects provide validation, defaults, property accessors, change events, debug logs, and predictable serialization. And, at the bottom, the generated Vega specification remains available as an escape hatch and interoperability format.
The work-in-progress implementation already reflects this layering: high-level quantitative and line-chart objects are composed from lower-level Vega dataset, scale, axis, mark, encoding, and value objects before serializing to a standard specification.
This is not intended to be a thin wrapper which renames a few Vega properties. What we are building is a frontend and integration layer.
The value is in the continuity
To be clear, we are not claiming that every individual feature we want is unprecedented. That would be both false and unhelpful.
Vega already renders on the server. Other libraries can be made to work with Node.js. Command-line visualization tools exist. Graphical chart editors exist. Some libraries accept typed arrays or can be used inside Electron.
The individual pieces are out there. What we have not found is a coherent system which brings those pieces together around scientific computing in JavaScript.
Consider the workflow we are working toward:
Load an ndarray. Create a chart without manually reshaping the data. Open the chart in a desktop window directly from the REPL. Update a property and see the view react. Listen for changes from another server-side object. Inspect debug logs to understand why the plot updated. Fine-tune the figure in a graphical editor. Serialize the same underlying specification to stdout. Embed the same visualization in a browser application. Hand the specification to a tool in another ecosystem.
No single step is the point. The continuity is.
Building it the stdlib way
The plotting work is also being built according to the same principles which guide the rest of stdlib. Low-level components are modular and independently usable. Higher-level behavior is composed from those components. Complexity is pushed upward into APIs which can provide convenience without compromising the predictability of the foundations underneath.
Yes, this results in more packages than placing everything inside a single plot function. It also gives each concept a clear boundary. Components can be tested in isolation, documented independently, benchmarked where appropriate, reused by other packages, and replaced without rewriting the entire plotting system.
Just as importantly, stdlib owns the user-facing compatibility boundary.
Vega may evolve. Renderers may change. New specification versions may be released. By placing stdlib APIs in front of that machinery, we can absorb upstream changes, maintain consistent conventions across the rest of the project, and release plotting functionality according to the needs of stdlib users.
Depending on Vega does not mean surrendering control. It means focusing our control at the layer where we can add the most value.
Where things stand
This is not a launch announcement. The plotting work currently lives in a long-running draft pull request, and it is not ready for general use. API names may change. Package boundaries may move. Some features are incomplete, and many details remain to be worked out. But we have made significant progress.
The branch now contains a programmatic Vega object model, data conversion utilities, scales, axes, marks, encoding objects, high-level chart constructors, direct ndarray support, multiple view targets, server-side change propagation, and an early graphical editor. Enough of the architecture is now working that the direction is no longer hypothetical.
We are sharing the rationale before the software is finished because we think the motivation matters. From the outside, another plotting library can look like duplication for duplication's sake. From inside stdlib, it is an important missing piece in our effort to provide a comprehensive environment for scientific computing and data analysis in JavaScript.
We welcome initial feedback, especially from those whose workflows move between the REPL, scripts, servers, browsers, command-line tools, and publication systems. Those are precisely the boundaries we want to make disappear.
Closing
JavaScript does not need another plotting library in the abstract. stdlib does need a plotting system designed around scientific JavaScript.
A scientific computing environment is incomplete if it can perform a computation but cannot help you inspect, explore, understand, and communicate the result. Visualization is not an accessory to data analysis. It is part of the process by which analysis happens.
We are not trying to replace every existing visualization library, nor are we trying to win a chart-library arms race. We are trying to make scientific computing in JavaScript feel whole.
That means starting where the data lives. It means supporting the runtime the user is already working in. It means understanding numerical data structures. It means treating plots as live computational objects. And it means knowing when to build our own infrastructure and when to rely on excellent work already available in the broader ecosystem.
Yes, data visualization in JavaScript is a rather frothy and noisy space.
And yet, here we are.
That is why we are building another plotting library.
Athan Reines is a software engineer at Quansight and core developer of stdlib.
stdlib is an open source software project dedicated to providing a comprehensive suite of robust, high-performance libraries to accelerate your project's development and give you peace of mind knowing that you're depending on expertly crafted, high-quality software.
If you've enjoyed this post, give us a star 🌟 on GitHub and consider supporting the project. Your contributions and continued support help ensure the project's long-term success and are greatly appreciated!
Acknowledgments
This work was supported in part by the National Science Foundation under Award No. 2449410.
Disclaimer: Any opinions, findings and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.