Skip to main content

GLPlot: GPU-Accelerated Plotting for Python

Tests Lint Build License: MIT Python Version

GLPlot is a Python plotting library with a Matplotlib-like API that renders on the GPU through OpenGL instead of the CPU. Plots that make Matplotlib chug — millions of points, dense line families, large 3D scenes — stay interactive: smooth pan, zoom, and rotation, even at that scale. If you already know plt.plot() / plt.scatter(), most of what you know carries over directly.

Install

pip install glplot

or from source:

git clone https://github.com/AkarisDimitry/GLPlot.git
cd GLPlot
pip install -e .

Requires Python 3.11+. Core dependencies: numpy, scipy, matplotlib, glfw, PyOpenGL, imgui-bundle (for the on-screen control panel).

30-second example

import numpy as np
import glplot.pyplot as plt

x = np.linspace(0, 10, 100)

plt.figure("My Plot", figsize=(8, 5))
plt.plot(x, np.sin(x), "r-", lw=2, label="sin(x)")
plt.scatter(x[::10], np.sin(x[::10]), c="blue", s=20)
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.show()

That's it — a real window opens, fully interactive (pan/zoom/rotate) from the first frame.

Explore 2D plots, 3D scenes, and real-time animations. Click on any figure or title below to expand its full Python code, or click the file link to view the script.

📊 2D Visualizations

10M-point spiral scatter
🔍 Click to view code: 10-Million Point Spiral Scatter (02_scatter_fill.py)

📄 Full script: examples/gallery/02_scatter_fill.py

import numpy as np
import glplot.pyplot as plt

rng = np.random.default_rng(4)
n = 10_000_000
theta = rng.uniform(0, 18 * np.pi, n)
radius = 0.025 * theta + rng.gamma(1.5, 0.006, n)
x_pc = radius * np.cos(theta) + rng.normal(scale=0.012, size=n)
y_pc = radius * np.sin(theta) + rng.normal(scale=0.012, size=n)
formation_phase = theta + 6.0 * radius  # proxy for age along the spiral arm

plt.figure("Simulated Spiral Star Field", figsize=(9, 6))
plt.scatter(x_pc, y_pc, c=formation_phase, cmap="turbo", s=1.5, alpha=0.72)
plt.title(f"Simulated stellar density along a spiral arm (N = {n:,} stars)")
plt.xlabel("x (pc)")
plt.ylabel("y (pc)")
plt.show()
10M-sample 2D density histogram
🔍 Click to view code: 10M-Sample 2D Density Histogram (10_massive_hist2d_density.py)

📄 Full script: examples/gallery/10_massive_hist2d_density.py

import numpy as np
import glplot.pyplot as plt

rng = np.random.default_rng(123)
n = 10_000_000
theta = rng.uniform(0, 2 * np.pi, n)
r = rng.gamma(shape=2.2, scale=0.8, size=n)
mm_per_unit = 10.0
x = mm_per_unit * (r * np.cos(theta) + rng.normal(scale=0.08, size=n))
y = mm_per_unit * (r * np.sin(theta) + rng.normal(scale=0.08, size=n))

plt.figure("Detector Impact Density", figsize=(8, 6))
lim = 45.0
plt.hist2d(
    x, y,
    bins=170,
    range=[[-lim, lim], [-lim, lim]],
    cmap="plasma",
    s=40.0,
    label=f"{n:,} hits",
)
plt.title(f"{n:,}-hit density map on detector plane")
plt.xlabel("x (mm)")
plt.ylabel("y (mm)")
plt.legend()
plt.show()
10,072-line oscilloscope ensemble
🔍 Click to view code: 10,072-Line Oscilloscope Ensemble (01_line_plot.py)

📄 Full script: examples/gallery/01_line_plot.py

import colorsys
import numpy as np
import glplot.pyplot as plt

def chalk_color(hue: float, rng: np.random.Generator) -> tuple:
    sat = rng.uniform(0.22, 0.42)
    val = rng.uniform(0.90, 1.0)
    return colorsys.hsv_to_rgb(hue % 1.0, sat, val)

rng = np.random.default_rng(101)
n_channels = 10_072
n_points = 180
t = np.linspace(0, 40 * np.pi, n_points)
t_ms = t * (50.0 / (40 * np.pi))
stimulus = np.sin(t) + 0.35 * np.sin(2.7 * t)

plt.figure("Oscilloscope Ensemble", figsize=(9, 5))
plt.plot_style("chalk", layers=False)

frac = np.linspace(0.0, 1.0, n_channels)
hues = (rng.uniform(0, 1) + frac + rng.uniform(-0.02, 0.02, n_channels)) % 1.0

for i in range(n_channels):
    t_frac = frac[i]
    phase = rng.uniform(0, 2 * np.pi)
    amp = 0.12 + 0.781 * t_frac
    freq = 1.1 + 0.852 * t_frac
    offset = 3.195 * t_frac
    channel = stimulus + amp * np.sin(freq * t + phase)
    color = chalk_color(hues[i], rng)
    plt.plot(t_ms, channel + offset, color=(*color, 0.35), lw=0.5)

plt.plot(t_ms, stimulus + 0.7, color=(0.95, 0.94, 0.90, 1.0), lw=3.0, label="Reference")
plt.xlabel("Time (ms)")
plt.ylabel("Amplitude")
plt.title(f"Traces from {n_channels} oscillator channels")
plt.legend()
plt.show()

🪐 3D Visualizations

1M-point 3D Point Cloud
🔍 Click to view code: 1M-Point Circumstellar Dust Halo (07_projected_3d_cloud.py)

📄 Full script: examples/gallery/07_projected_3d_cloud.py

import numpy as np
import glplot.pyplot as plt

rng = np.random.default_rng(77)
n = 1_000_000
sigma = 42.0
outer_radius = 2.5 * sigma
n_gen = 1_140_000
x = rng.normal(0.0, sigma, n_gen)
y = rng.normal(0.0, sigma, n_gen)
z = rng.normal(0.0, sigma, n_gen)
inside = (x**2 + y**2 + z**2) < outer_radius**2
x, y, z = x[inside][:n], y[inside][:n], z[inside][:n]

plt.figure("Circumstellar Dust Halo", figsize=(9, 6))
plt.scatter3d(
    x, y, z,
    c=z,
    cmap="viridis",
    s=1.1,
    alpha=0.75,
    elev=22,
    azim=-42,
    label=f"{n:,} grains",
)
plt.title(f"Simulated circumstellar dust halo ({n:,} grains)")
plt.xlabel("x (AU)")
plt.ylabel("y (AU)")
plt.zlabel("z (AU)")
plt.legend()
plt.show()
1.75M-Point Volumetric Nebula
🔍 Click to view code: 1.75M-Point Volumetric Nebula (13_volumetric_nebula.py)

📄 Full script: examples/gallery/13_volumetric_nebula.py

import numpy as np
import glplot.pyplot as plt

rng = np.random.default_rng(1313)
n = 1_750_000
outer_radius = 3.2
n_gen = 2_000_000
theta = rng.uniform(0, 8 * np.pi, n_gen)
phi = rng.normal(0.0, 0.48, n_gen)
r = rng.gamma(2.0, 0.8, n_gen)
inside = r < outer_radius
theta, phi, r = theta[inside][:n], phi[inside][:n], r[inside][:n]
arm = rng.integers(0, 4, n) * np.pi / 2
twist = theta + arm + 0.62 * r
x = r * np.cos(twist) + rng.normal(scale=0.06, size=n)
y = r * np.sin(twist) + rng.normal(scale=0.06, size=n)
z = 0.65 * r * np.sin(phi) + 0.18 * np.sin(theta)
emission = np.exp(-0.12 * r**2) + 0.28 * np.sin(3 * theta) ** 2

plt.figure("Volumetric 3D Nebula", figsize=(9, 6))
plt.volume3d(
    x, y, z, emission,
    threshold=0.14,
    cmap="magma",
    alpha=0.42,
    s=1.2,
    elev=24,
    azim=-52,
    label=f"{n:,} points",
)
plt.title(f"{n:,}-point volumetric emission nebula")
plt.xlabel("x (ly)")
plt.ylabel("y (ly)")
plt.zlabel("z (ly)")
plt.legend()
plt.show()
3D Turbulent Vector Field
🔍 Click to view code: 3D Turbulent Vector Field (19_turbulent_vector_field_3d.py)

📄 Full script: examples/gallery/19_turbulent_vector_field_3d.py

import numpy as np
import glplot.pyplot as plt

rng = np.random.default_rng(1919)
cloud_n = 950_000
t = rng.uniform(0.0, 18.0 * np.pi, cloud_n)
shell_raw = rng.gamma(2.2, 0.42, cloud_n)
shell = 2.0 * np.tanh(shell_raw / 2.0)
twist = 0.22 * np.sin(2.6 * t) + 0.18 * np.cos(1.4 * t)
cloud_x = shell * np.sin(t) + 0.28 * np.sin(3.0 * t)
cloud_y = shell * np.cos(1.17 * t) + 0.32 * np.cos(2.4 * t)
cloud_z = 0.58 * np.sin(0.52 * t + twist) + 0.32 * shell * np.cos(0.33 * t)
cloud_energy = np.exp(-0.22 * shell**2) + 0.33 * np.sin(1.7 * t) ** 2

plt.figure("Turbulent 3D Flow", figsize=(10, 7), ssao=True)
plt.volume3d(
    cloud_x, cloud_y, cloud_z, cloud_energy,
    threshold=0.22,
    cmap="inferno",
    alpha=0.17,
    s=0.72,
    elev=27,
    azim=-49,
    label=f"{cloud_n:,} volumetric samples",
)
plt.title("Massive 3D turbulent swirl")
plt.xlabel("x")
plt.ylabel("y")
plt.zlabel("z")
plt.legend()
plt.show()

🎬 Animated Visualizations

Chladni wave animation
🔍 Click to view code: Animated Standing-Wave Interference (28_chladni_wave_animation.py)

📄 Full script: examples/gallery/28_chladni_wave_animation.py

import numpy as np
import glplot.animation as animation
import glplot.pyplot as plt

N = 360
x = np.linspace(-1.0, 1.0, N)
y = np.linspace(-1.0, 1.0, N)
X, Y = np.meshgrid(x, y)
FRAMES = 54

def chladni_field(m: float, n: float) -> np.ndarray:
    return np.sin(m * np.pi * X) * np.cos(n * np.pi * Y) - np.sin(n * np.pi * X) * np.cos(m * np.pi * Y)

def mode_pair(frame: int) -> tuple:
    t = frame / FRAMES
    m = 2.2 + 4.3 * (0.5 - 0.5 * np.cos(2 * np.pi * t))
    n = 3.1 + 3.6 * (0.5 - 0.5 * np.cos(2 * np.pi * t * 1.5 + 1.1))
    return m, n

def zoom_span(frame: int) -> float:
    t = frame / FRAMES
    return 0.75 - 0.35 * np.cos(2 * np.pi * t)

fig = plt.figure("Animated Standing-Wave Interference", figsize=(7.8, 6.2))
plt.plot_style("blueprint")

def update(frame: int):
    m, n = mode_pair(frame)
    span = zoom_span(frame)
    Z = chladni_field(m, n)
    plt.cla()
    plt.imshow(Z, extent=(-1, 1, -1, 1), origin="lower", cmap="RdBu_r", vmin=-1.0, vmax=1.0, aspect="equal")
    plt.xlim(-span, span)
    plt.ylim(-span, span)
    plt.title(f"Standing-wave interference (m={m:.1f}, n={n:.1f})")
    plt.xlabel("x (plate width)")
    plt.ylabel("y (plate width)")
    return []

ani = animation.FuncAnimation(fig, update, frames=FRAMES, interval=42)
ani.save("chladni_wave_animation.gif", fps=20)
plt.show()
Audio spectrum analyzer bars
🔍 Click to view code: Real-Time Audio Spectrum Analyzer (03_spectrum_analyzer_bars.py)

📄 Full script: examples/gallery/animations/03_spectrum_analyzer_bars.py

import numpy as np
import glplot.animation as animation
import glplot.pyplot as plt

N_BANDS = 64
FRAMES = 84
band_x = np.arange(N_BANDS)

fig = plt.figure("Audio Spectrum Analyzer", figsize=(9, 5))

def update(frame: int):
    t = frame / FRAMES
    levels = -25.0 + 18.0 * np.sin(np.linspace(0.2, 4.0 * np.pi, N_BANDS) + 2.0 * np.pi * t)
    plt.cla()
    plt.bar(band_x, levels, color="tab:cyan", alpha=0.85)
    plt.ylim(-60, 0)
    plt.title("Real-Time 64-Band Spectrum Analyzer")
    plt.xlabel("Frequency Band")
    plt.ylabel("Power (dBFS)")
    return []

ani = animation.FuncAnimation(fig, update, frames=FRAMES, interval=33)
ani.save("spectrum_analyzer.gif", fps=30)
plt.show()
Orbiting star cluster
🔍 Click to view code: Differential Keplerian Star Cluster (01_orbiting_star_cluster.py)

📄 Full script: examples/gallery/animations/01_orbiting_star_cluster.py

import numpy as np
import glplot.animation as animation
import glplot.pyplot as plt

N_STARS = 20_000
FRAMES = 80
LIM = 6.8

rng = np.random.default_rng(42)
radius = rng.exponential(scale=1.35, size=N_STARS) + 0.10
arm_id = rng.integers(0, 3, N_STARS)
theta0 = arm_id * (2.0 * np.pi / 3) + 1.65 * np.log(radius / 0.10)
omega = 1.35 / np.sqrt(radius + 0.30)

fig = plt.figure("Orbiting Star Cluster", figsize=(8, 8))

def update(frame: int):
    t = frame * 0.08
    theta = theta0 + omega * t
    x = radius * np.cos(theta)
    y = radius * np.sin(theta)
    plt.cla()
    plt.scatter(x, y, c=radius, cmap="plasma", s=1.2, alpha=0.8)
    plt.xlim(-LIM, LIM)
    plt.ylim(-LIM, LIM)
    plt.title(f"Differential Keplerian rotation ({N_STARS:,} stars)")
    return []

ani = animation.FuncAnimation(fig, update, frames=FRAMES, interval=40)
ani.save("star_cluster.gif", fps=25)
plt.show()

All of the above render at 60+ FPS with interactive panning, zooming, and rotation, regardless of point count. Discover more in the example gallery (28 static scripts) and the animated gallery (16 animated scripts).

Features

  • Matplotlib-compatible API — plot, scatter, bar, hist, hist2d, imshow, contour/contourf, quiver, format strings ("r-o", "b--"), and more
  • Millions of points, still interactive — GPU instancing and density accumulation instead of CPU-side geometry construction
  • Full 2D/3D — lines, scatter, filled regions, bars, histograms, matrices, surfaces, wireframes, 3D bars, vector fields, with SSAO depth shading in 3D
  • Real multi-panel subplots — plt.subplots(), per-panel interaction
  • Animation — glplot.animation.FuncAnimation/ArtistAnimation, exportable to GIF/video
  • A live control panel — python -m glplot opens a workstation for editing a scene, its layers, and its styling interactively, with undo history
  • Native interactive controls — gplt.slider, button, checkbox, dropdown, range_slider, textbox, color_picker and @gplt.interact put widgets in a Controls panel beside the plot; callbacks run once per frame with the latest values, errors are shown in place, values can be saved as presets. glplot.widgets runs matplotlib Slider/Button/CheckButtons/RadioButtons/TextBox scripts unchanged
  • Numerically stable at extreme zoom — double-precision, viewport-relative coordinate transforms avoid the jitter that single-precision GPU pipelines show at large offsets

Quick Reference Recipes

A million lines at once (plot_lines)

Ordinary plotting draws each line as CPU-generated geometry — a million calls to plot() would build a million separate meshes. plot_lines instead uploads each line as an (a, b) coefficient pair and lets the GPU work out what's visible:

import numpy as np
import glplot.pyplot as plt

n = 1_000_000
a = np.random.randn(n)
b = np.random.randn(n)

plt.figure("Density")
plt.plot_lines(a, b, x_range=(-2, 2))
plt.show(density=True)

Sliders and buttons that drive your analysis (interact)

Each keyword becomes a control in the Controls panel; the function re-runs whenever one moves. Return (x, y) and GLPlot draws the curve and keeps it updated; return a dict and it appears as a table of results:

import numpy as np
import glplot.pyplot as plt

x = np.linspace(0, 10, 5000)

@plt.interact(amp=(0, 5), freq=(0.1, 10), wave=["sin", "cos"])
def signal(amp=1.0, freq=1.0, wave="sin"):
    return x, amp * getattr(np, wave)(freq * x)

@plt.interact
def stats(amp, freq):                      # reuses the controls above by name
    return {"Peak": amp, "Period": 2 * np.pi / freq}

@plt.button("Double amplitude", shortcut="Mod+Alt+D")
def double():
    plt.controls()["amp"].value *= 2

plt.show()

Controls can also be used on their own (amp = plt.slider("Amplitude", 0, 5), amp.on_change(fn), amp.value), grouped with with plt.control_group("Filter"):, and saved or restored with plt.controls().save_preset(...) / .save(path). See examples/ex_interactive_controls.py for a complete signal-analysis bench.

How it compares

Feature GLPlot Matplotlib Plotly Datashader VisPy
GPU acceleration ✓ (OpenGL) ✗ ✗ ✓ ✓
Matplotlib-style API ✓ ✓ ✗ ✗ ✗
Millions of points, interactive ✓ ✗ Limited ✓ ✓
Interactive 3D ✓ Limited ✓ Limited ✓
Density visualization ✓ (HDR) Basic Limited ✓ ✗
Precision at extreme zoom ✓ (double precision) ✓ Basic ✓ ✗

GLPlot sits between "familiar API, CPU-bound" (Matplotlib) and "GPU-fast, low-level" (VisPy): a Matplotlib-shaped surface backed by a GPU renderer.

Rendering architecture

GLPlot runs two rendering pipelines — one for 2D primitives (lines, scatter, density) and one for 3D geometry (bars, surfaces, wireframes, scatter3d) — both driven from the CPU but doing their actual work on the GPU. glReadPixels is only ever called for export; the interactive path never reads pixels back to the CPU.

GLPlot rendering pipeline

See docs/ARCHITECTURE.md for the full derivation of each stage, including the density-accumulation math and the viewport-relative projection that keeps zoom numerically stable.

Testing

pytest                                   # run everything
pytest --cov=glplot --cov-report=html    # with coverage
pytest tests/test_pyplot.py::test_plot_accepts_y_only_and_returns_artists  # one test

6,800+ tests, run fully headless (no window is ever displayed) so they work in CI. The matrix covers Python 3.11–3.13 on Ubuntu, macOS, and Windows; black, isort, and flake8 are enforced on every push. See examples/benchmark/ for reproducible performance comparisons against Matplotlib, VisPy, fastplotlib, Datashader, and hvPlot, and tools/ for GPU/environment diagnostics.

Documentation

Citation

@software{lombardi2026glplot,
  title={GLPlot: High-Performance GPU-Accelerated Plotting Library for Python},
  author={Lombardi, Juan Manuel and Riccius, Felix and Holland, Julian and Ducci, Gianmarco},
  year={2026},
  url={https://github.com/AkarisDimitry/GLPlot},
  doi={10.5281/zenodo.PLACEHOLDER}
}

See CITATION.cff for other formats.

License

MIT.

Acknowledgments

Built on PyOpenGL, GLFW, NumPy, SciPy, Matplotlib, and Dear ImGui — thanks to those communities for the foundations this sits on.

Release files for glplot 0.1.13

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for glplot 0.1.13
File Size Uploaded
glplot-0.1.13.tar.gz 1.7 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for glplot 0.1.13
File Interpreter ABI Platform
glplot-0.1.13-py3-none-any.whl Python 3 none any Details

Total release size: 2.9 MB

Release files / glplot-0.1.13.tar.gz

Download URL glplot-0.1.13.tar.gz
Size 1.7 MB
Tags Source
SHA-256 checksum
How to use checksums
53f8ada55ba2959deaa2545231857441101970e93c669b829140a621bfff9084
BLAKE2b-256 checksum
How to use checksums
3fbcccf0b9ad0c4ea604439d499fcf55fd384c2235a281af26c0e4a5ef22ced9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / glplot-0.1.13-py3-none-any.whl

Download URL glplot-0.1.13-py3-none-any.whl
Size 1.2 MB
Tags Python 3
SHA-256 checksum
How to use checksums
41694d40c18db6668e1761052d9aebf6265843423c8c3ec100955b8a1e639585
BLAKE2b-256 checksum
How to use checksums
202ada256950c8f054a83d21e9669955f2016af952cf5bf169c86f773e542cae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.1.13 This release

2 release files

0.1.12

2 release files

0.1.11

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page