# 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.
<table>
<tr>
<td><img src="guides/examples/high_level_multi_series.svg" alt="Multi-series line plot" width="280"></td>
<td><img src="guides/examples/high_level_surface.svg" alt="3D surface plot" width="280"></td>
<td><img src="guides/examples/high_level_spider.svg" alt="Spider chart" width="280"></td>
</tr>
<tr>
<td><img src="guides/examples/ecosystem_ml_loss.svg" alt="ML loss curves" width="280"></td>
<td><img src="guides/examples/ecosystem_ml_embeddings_2d.svg" alt="2D embeddings" width="280"></td>
<td></td>
</tr>
</table>
## 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
```bash
# Ubuntu/Debian
sudo apt install gnuplot
# macOS
brew install gnuplot
# Arch Linux
sudo pacman -S gnuplot
# Fedora
sudo dnf install gnuplot
```
Verify your version:
```bash
gnuplot --version
# gnuplot 6.0 patchlevel 0
```
## Installation
Add `gnuplot_ex` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:gnuplot_ex, "~> 0.1.0"}
]
end
```
## Quick Start
### Low-level API
Direct control over gnuplot commands:
```elixir
# 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:
```elixir
# 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.gp
```
### Gnuplot 6 Features
#### Spider/Radar Charts
```elixir
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
```elixir
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
```elixir
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
```elixir
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
```elixir
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
```elixir
# 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
```elixir
# Visualize classification results
GnuplotEx.heatmap(confusion_matrix,
x_labels: class_names,
y_labels: class_names,
palette: :viridis,
title: "Confusion Matrix"
)
```
### Dataset Visualization
```elixir
# 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
```elixir
# 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
```elixir
# 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
```elixir
# 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 |
```elixir
plot
|> GnuplotEx.render(:svg) # Returns SVG string
|> GnuplotEx.to_svg(path) # Writes to file
|> GnuplotEx.to_png(path) # PNG output
|> GnuplotEx.show() # Interactive window
```
## Configuration
```elixir
# config/config.exs
config :gnuplot_ex,
default_terminal: :svg,
svg_options: [:enhanced, size: {800, 600}],
default_palette: :viridis,
timeout: 10_000
```
## Error Handling
```elixir
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
end
```
## Phoenix LiveView Integration
Add Phoenix LiveView to your dependencies:
```elixir
{:phoenix_live_view, "~> 1.0"}
```
Use the `live_gnuplot/1` component for real-time plotting:
```elixir
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
end
```
**Features:**
- 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](guides/liveview.md) for complete documentation and examples.
## Ecosystem Integration
GnuplotEx integrates with the Elixir ML/data science ecosystem. Add optional dependencies:
```elixir
{: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:
```elixir
# 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:
```elixir
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 columns
```
### ML Visualization Helpers
Pre-built helpers for common ML visualizations:
```elixir
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](guides/ecosystem.md) for complete documentation.
## Performance
GnuplotEx handles large datasets efficiently with binary mode and parallel rendering.
<table>
<tr>
<td><img src="guides/examples/benchmark_large_dataset.png" alt="Large Dataset Benchmark" width="400"></td>
<td><img src="guides/examples/benchmark_parallel.png" alt="Parallel Rendering Benchmark" width="400"></td>
</tr>
</table>
See [Benchmarks](guides/benchmarks.md) for details. Run `mix bench` to generate charts on your system.
## Documentation
- [HexDocs](https://hexdocs.pm/gnuplot_ex)
- [Gnuplot Documentation](http://www.gnuplot.info/documentation.html)
- [Gnuplot 6.0 Release Notes](http://gnuplot.info/ReleaseNotes_6_0_0.html)
## Contributing
Contributions are welcome!
```bash
# 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