README.md

# 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