GnuplotEx
An Elixir wrapper for Gnuplot 6+ with SVG-first output and ergonomic API design.
Ideal for data science, machine learning visualization, and scientific computing in Elixir.
Features
- SVG-first design - Default to scalable vector graphics for web applications
- Ergonomic API - Higher-level abstractions while maintaining low-level access
- Full 2D and 3D support - Scatter, line, surface, parametric plots and more
- ML/Data Science ready - Visualize datasets, model outputs, loss curves, and embeddings
- Gnuplot 6+ features - Data blocks, voxels, spider charts, animations, and named palettes
- Stream-based - Efficient memory usage for large datasets (1M+ points)
- Named sessions - Run multiple independent gnuplot processes
- Dry mode - Test command generation without gnuplot installed
- Save script - Export reproducible .gp files
- Nx integration - Plot tensors directly from Nx
- LiveView ready - Phoenix LiveView components for real-time plotting
Requirements
- Elixir 1.18+
- Erlang/OTP 27+
- Gnuplot 6.0+
Installing Gnuplot
# Ubuntu/Debian
sudo apt install gnuplot
# macOS
brew install gnuplot
# Arch Linux
sudo pacman -S gnuplot
# Fedora
sudo dnf install gnuplot
Verify your version:
gnuplot --version
# gnuplot 6.0 patchlevel 0
Installation
Add gnuplot_ex to your list of dependencies in mix.exs:
def deps do
[
{:gnuplot_ex, "~> 0.1.0"}
]
endQuick Start
Low-level API
Direct control over gnuplot commands:
# 2D scatter plot
dataset = for x <- 1..100, do: [x, :math.sin(x / 10) + :rand.uniform()]
GnuplotEx.plot([
[:set, :term, :svg, :size, {800, 600}],
[:set, :output, "/tmp/scatter.svg"],
[:set, :title, "Scatter Plot"],
[:plot, "-", :with, :points, :pt, 7]
], [dataset])
# 3D surface
GnuplotEx.plot([
[:set, :term, :svg],
[:set, :output, "/tmp/surface.svg"],
[:splot, 'sin(x)*cos(y)']
])
# Named sessions - run multiple independent gnuplot processes
GnuplotEx.plot(:analysis, commands, data)
GnuplotEx.plot(:realtime, other_commands, other_data)
GnuplotEx.sessions() # => [:analysis, :realtime, :default]
# Dry mode for testing (no gnuplot required)
GnuplotEx.plot(commands, data, dry: true)
# => {:dry, %{commands: [...], script: "..."}}
# Inspect the command spec before execution
specs = GnuplotEx.build_specs(commands, data)
IO.inspect(specs)High-level API
Ergonomic pipeline-style plotting:
# Simple scatter plot
data
|> GnuplotEx.scatter(title: "My Data", color: "#E95420")
|> GnuplotEx.to_svg("/tmp/plot.svg")
# Multiple datasets
GnuplotEx.new()
|> GnuplotEx.title("Comparison")
|> GnuplotEx.scatter(data1, label: "Experiment")
|> GnuplotEx.line(data2, label: "Baseline")
|> GnuplotEx.x_label("Time")
|> GnuplotEx.y_label("Value")
|> GnuplotEx.render(:svg)
# 3D surface from function
GnuplotEx.surface(fn x, y -> :math.sin(x) * :math.cos(y) end,
x_range: -5..5,
y_range: -5..5,
palette: :viridis
)
|> GnuplotEx.to_svg("/tmp/surface.svg")
# Keyword abbreviations for compact syntax
GnuplotEx.scatter(data, t: "Plot", xr: 0..100, yr: -1..1, xl: "X", yl: "Y")
# Save reproducible gnuplot script
plot
|> GnuplotEx.to_svg("/tmp/plot.svg")
|> GnuplotEx.save_script("/tmp/plot.gp") # Can re-run with: gnuplot plot.gpGnuplot 6 Features
Spider/Radar Charts
stats = [
%{name: "Warrior", speed: 6, power: 9, defense: 8, magic: 2, luck: 5},
%{name: "Mage", speed: 5, power: 3, defense: 4, magic: 10, luck: 6}
]
GnuplotEx.spider(stats,
axes: [:speed, :power, :defense, :magic, :luck],
title: "Character Comparison"
)
|> GnuplotEx.render(:svg)Parallel Coordinates
cars = [
[25000, 30, 180, 1500],
[35000, 25, 220, 1800],
[45000, 20, 300, 2000]
]
GnuplotEx.parallel(cars,
axes: ["Price", "MPG", "HP", "Weight"],
title: "Car Comparison"
)
|> GnuplotEx.render(:svg)GIF Animation
frames = for phase <- 0..60 do
for x <- 0..100, do: [x / 10, :math.sin(x / 10 + phase / 10)]
end
GnuplotEx.animate(frames,
delay: 50,
loop: :infinite,
style: :lines
)
|> GnuplotEx.to_file("/tmp/wave.gif")Voxel and Isosurface
voxel_data = for x <- -10..10, y <- -10..10, z <- -10..10 do
value = :math.exp(-(x*x + y*y + z*z) / 50)
{x, y, z, value}
end
GnuplotEx.isosurface(voxel_data,
level: 0.5,
title: "Gaussian Blob"
)
|> GnuplotEx.render(:svg)Named Color Palettes
GnuplotEx.surface(data, palette: :viridis)
GnuplotEx.surface(data, palette: :magma)
GnuplotEx.surface(data, palette: :plasma)
GnuplotEx.surface(data, palette: :inferno)Machine Learning & Data Science
Training Loss Curves
# Plot training progress
GnuplotEx.new()
|> GnuplotEx.title("Model Training")
|> GnuplotEx.line(train_losses, label: "Training Loss", color: "#E95420")
|> GnuplotEx.line(val_losses, label: "Validation Loss", color: "#0066CC")
|> GnuplotEx.x_label("Epoch")
|> GnuplotEx.y_label("Loss")
|> GnuplotEx.to_svg("/tmp/training.svg")Confusion Matrix Heatmap
# Visualize classification results
GnuplotEx.heatmap(confusion_matrix,
x_labels: class_names,
y_labels: class_names,
palette: :viridis,
title: "Confusion Matrix"
)Dataset Visualization
# 2D dataset with class labels
GnuplotEx.new()
|> GnuplotEx.scatter(class_0_points, label: "Class 0", color: "#E95420")
|> GnuplotEx.scatter(class_1_points, label: "Class 1", color: "#0066CC")
|> GnuplotEx.title("Dataset Distribution")
|> GnuplotEx.render(:svg)
# 3D point cloud (e.g., embeddings)
GnuplotEx.scatter3d(embeddings,
color_by: labels,
palette: :viridis,
title: "t-SNE Embeddings"
)Decision Boundaries
# Plot decision surface with data points
GnuplotEx.new()
|> GnuplotEx.contour_filled(decision_scores, levels: 20, palette: :plasma)
|> GnuplotEx.scatter(data_points, color_by: labels)
|> GnuplotEx.title("Decision Boundary")
|> GnuplotEx.render(:svg)Nx Tensor Support
# Plot directly from Nx tensors
tensor = Nx.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
GnuplotEx.Nx.heatmap(tensor, title: "Weight Matrix")
GnuplotEx.Nx.surface(tensor, title: "3D Surface from Tensor")
# Plot loss history from training
loss_tensor = Nx.tensor([0.9, 0.7, 0.5, 0.3, 0.2, 0.15, 0.1])
GnuplotEx.Nx.line(loss_tensor, title: "Loss Curve")Feature Distributions
# Histogram of feature values
GnuplotEx.histogram(feature_values,
bins: 50,
title: "Feature Distribution",
x_label: "Value",
y_label: "Frequency"
)
# Multiple feature comparison
GnuplotEx.new()
|> GnuplotEx.histogram(feature_1, label: "Feature 1", alpha: 0.7)
|> GnuplotEx.histogram(feature_2, label: "Feature 2", alpha: 0.7)
|> GnuplotEx.render(:svg)Output Formats
GnuplotEx supports multiple output terminals:
| Format | Use Case |
|---|---|
:svg | Web, scalable (default) |
:png | Raster images |
:pdf | Documents |
:canvas | HTML5 interactive |
:wxt / :qt | Desktop interactive |
:gif | Animations |
plot
|> GnuplotEx.render(:svg) # Returns SVG string
|> GnuplotEx.to_svg(path) # Writes to file
|> GnuplotEx.to_png(path) # PNG output
|> GnuplotEx.show() # Interactive windowConfiguration
# config/config.exs
config :gnuplot_ex,
default_terminal: :svg,
svg_options: [:enhanced, size: {800, 600}],
default_palette: :viridis,
timeout: 10_000Error Handling
case GnuplotEx.plot(commands, datasets) do
{:ok, output} ->
# Success
{:error, :gnuplot_not_found} ->
# Gnuplot binary not in PATH
{:error, :gnuplot_version_unsupported} ->
# Gnuplot version < 6.0
{:error, {:command_error, line, message}} ->
# Gnuplot rejected a command
endPhoenix LiveView Integration
Add Phoenix LiveView to your dependencies:
{:phoenix_live_view, "~> 1.0"}Use the live_gnuplot/1 component for real-time plotting:
defmodule MyAppWeb.ChartLive do
use Phoenix.LiveView
import GnuplotEx.LiveView.Component
def render(assigns) do
~H"""
<.live_gnuplot plot={@plot} width={1200} height={600} />
"""
end
def mount(_params, _session, socket) do
plot = GnuplotEx.new()
|> GnuplotEx.line(initial_data())
{:ok, assign(socket, plot: plot)}
end
def handle_info({:new_data, data}, socket) do
plot = GnuplotEx.new() |> GnuplotEx.line(data)
{:noreply, assign(socket, plot: plot)}
end
endFeatures:
- Real-time plot updates
- Automatic caching for performance
- Interactive 3D controls (mouse/touch)
- SVG and PNG rendering
- Error handling with fallback content
See the LiveView Integration Guide for complete documentation and examples.
Ecosystem Integration
GnuplotEx integrates with the Elixir ML/data science ecosystem. Add optional dependencies:
{:nx, "~> 0.7", optional: true}, # Tensor support
{:explorer, "~> 0.8", optional: true} # DataFrame support (Polars backend)Nx Tensors
Plot tensors directly with automatic dimension handling:
# 1D tensor as line plot (auto x-indices)
tensor = Nx.tensor([1.0, 4.0, 2.0, 8.0, 5.0])
GnuplotEx.line(tensor, label: "Signal")
# 2D tensor as scatter/line
points = Nx.tensor([[1, 2], [3, 4], [5, 6]])
GnuplotEx.scatter(points)
# Matrix as heatmap
matrix = Nx.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
GnuplotEx.surface(matrix)Explorer DataFrames
Plot DataFrames with automatic column detection:
df = Explorer.DataFrame.new(%{x: [1, 2, 3], y: [2, 4, 6]})
GnuplotEx.scatter(df, label: "Data")
GnuplotEx.line(df, x: :x, y: :y) # Explicit columnsML Visualization Helpers
Pre-built helpers for common ML visualizations:
alias GnuplotEx.ML.{Loss, Confusion, ROC, Embeddings}
# Training curves
Loss.plot(train_loss, val_loss, title: "Training Progress")
# Confusion matrix
Confusion.plot(matrix, ["Cat", "Dog", "Bird"], normalize: true)
# ROC curves
ROC.plot(fpr, tpr, auc: 0.87)
# Embedding visualization
Embeddings.plot(tsne_points, labels, label_names: ["A", "B", "C"])See the Ecosystem Integration Guide for complete documentation.
Performance
GnuplotEx handles large datasets efficiently with binary mode and parallel rendering.
![]() |
![]() |
See Benchmarks for details. Run mix bench to generate charts on your system.
Documentation
Contributing
Contributions are welcome!
# Clone and setup
git clone https://gitlab.com/tristanperalta/gnuplot_ex
cd gnuplot_ex
mix deps.get
# Run tests (requires Gnuplot 6+ installed)
mix test
# Run only tests that don't require gnuplot
mix test --exclude gnuplot
# Run credo and dialyzer
mix credo
mix dialyzer
License
MIT License

