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.

Multi-series line plot 3D surface plot Spider chart
ML loss curves 2D embeddings

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"}
  ]
end

Quick 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.gp

Gnuplot 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:

FormatUse Case
:svgWeb, scalable (default)
:pngRaster images
:pdfDocuments
:canvasHTML5 interactive
:wxt / :qtDesktop interactive
:gifAnimations
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

# config/config.exs
config :gnuplot_ex,
  default_terminal: :svg,
  svg_options: [:enhanced, size: {800, 600}],
  default_palette: :viridis,
  timeout: 10_000

Error 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
end

Phoenix 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
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 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 columns

ML 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.

Large Dataset Benchmark Parallel Rendering Benchmark

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