# Gnuplot Elixir
A simple interface from [Elixir data][7] to the [Gnuplot graphing utility][1] that uses [Erlang Ports][5] to transmit data from your application to Gnuplot. Datasets are streamed directly to STDIN without temporary files and you can plot [1M points in 12.7 seconds](examples/stress.exs).
Please visit the [Gnuplot demos gallery](http://gnuplot.sourceforge.net/demo_5.3/) to see all the possibilities, the [manual which describes the grammar](http://www.gnuplot.info/docs_5.2/Gnuplot_5.2.pdf), and the [examples folder](examples/).
This is a conversion of the [Clojure Gnuplot library][4] by [aphyr][2]. This library has been tested on OS X, Ubuntu 16.04 and CentOS 7.6.
[![Build Status](https://github.com/devstopfix/gnuplot-elixir/workflows/ci/badge.svg)](https://github.com/devstopfix/gnuplot-elixir/actions)
[![Hex.pm](https://img.shields.io/hexpm/v/gnuplot.svg?style=flat-square)](https://hex.pm/packages/gnuplot)
[![API Docs](https://img.shields.io/badge/api-docs-MediumPurple.svg?style=flat)](https://hexdocs.pm/gnuplot/Gnuplot.html)
![Platforms](https://img.shields.io/badge/platform-osx%7Cubuntu%7Ccentos-black.svg)
## Usage
The `plot` function takes two arguments:
* a list of commands (each of which is a list of terms)
* a list of Streams or Enumerable datasets (not required when plotting functions)
Commands are lists of terms that normally start with an atom such as `:set`. They may be written as lists or [Word lists](https://elixir-lang.org/getting-started/sigils.html#word-lists) - the following lines are equivalent:
* `[:set, :xtics, :off]`
* `~w(set xtics off)a`
and both convert to `set xtics off`.
Strings are output inside double quotes, and charlists are output without modification. `[:plot, 'sin(x)', :title, "Sine Wave"]` becomes: `plot sin(x) title "Sine Wave"`
A dataset is a list of points, each point is a list of numbers. A dataset can be a Stream.
### Scatter plot with a single dataset
Lets compare the distributions of the [Erlang rand functions](http://erlang.org/doc/man/rand.html):
```elixir
dataset = for _ <- 0..1000, do: [:rand.uniform(), :rand.normal()]
{:ok, _cmd} = Gnuplot.plot([
[:set, :title, "rand uniform vs normal"],
[:plot, "-", :with, :points]
], [dataset])
```
Gnuplot will by default open a window containing your plot:
![rand](docs/gnuplot.PNG)
The command string sent (`_cmd` above) can be manually inspected should the chart not appear as you expected. If the chart is not drawn due to an error then the result will be `{:error, cmd, errors}`.
### PNG of two datasets
Write two datasets to a PNG file:
```elixir
import Gnuplot
{:ok, _cmd} = plot([
[:set, :term, :pngcairo],
[:set, :output, "/tmp/rand.png"],
[:set, :title, "rand uniform vs normal"],
[:set, :key, :left, :top],
plots([
["-", :title, "uniform", :with, :points],
["-", :title, "normal", :with, :points]
])
],
[
for(n <- 0..100, do: [n, n * :rand.uniform()]),
for(n <- 0..100, do: [n, n * :rand.normal()])
])
```
![uniform-vs-rand](docs/rand.PNG)
When we are plotting multiple datasets in the same chart we need a comma separated list for the `plot` command which is made with the `plots`, `splots` or `list` function.
NB the `:png` terminal can also be used but it produces [rougher output](http://www.gnuplotting.org/output-terminals/).
### Plot functions without datasets
```elixir
Gnuplot.plot([[:plot, 'sin(x)', :title, "Sine Wave"]])
```
![rand](docs/sine.PNG)
```elixir
Gnuplot.plot([
~w(set autoscale)a,
~w(set samples 800)a,
[:plot, -30..20, 'sin(x*20)*atan(x)']
])
```
NB [ranges](https://hexdocs.pm/elixir/Range.html) can be used
![rand](docs/atan_sin.PNG)
### Multiplot
The `multiplot` mode places serveral plots on the same page:
```elixir
Gnuplot.plot([
[:set, :multiplot, :layout, '2,1'],
[:plot, 'sin(x)/x'],
[:plot, 'cos(x)']
])
```
## Installation
This library is [available in Hex](https://hex.pm/packages/gnuplot) with [documentation](https://hexdocs.pm/gnuplot/Gnuplot.html) and the package can be installed by adding `gnuplot` to your project:
```elixir
def deps do
[
{:gnuplot, "~> 1.22"}
]
end
```
## Testing
Some tests create plots which require `gnuplot` to be installed. They can be be excluded with:
mix test --exclude gnuplot:true
## Performance
The performance of the library on a MacBook Air is comparable to the Clojure version when `gnuplot` draws to a GUI. It is a little faster when writing directly to a PNG when running on a server. The times below are in milliseconds. Each plot was made in increasing order of the number of points and after a cold start of the VM. The last two columns show the refactoring from Enumerable to Streams.
| Points | Clojure GUI | Elixir GUI | Elixir PNG | Elixir Enum | Elixir Stream |
| -----: | ----------: | ---------: | ---------: | ------------: | ------------: |
| 1 | 1,487 | 5 | 18 | 4 | 5 |
| 10 | 1,397 | 10 | 1 | <1 | 1 |
| 1e2 | 1,400 | 4 | 12 | 1 | 1 |
| 1e3 | 1,381 | 59 | 52 | 8 | 10 |
| 1e4 | 1,440 | 939 | 348 | 211 | 211 |
| 1e5 | 5,784 | 5,801 | 3,494 | 1,873 | 1,313 |
| 1e6 | 49,275 | 43,464 | 35,505 | 19,916 | 12,775 |
| | MacBook | MacBook | MacBook | Ubuntu 16.04 | Ubuntu 16.04 |
| | 2.5 GHz i5 | 2.5 GHz i5 | 2.5 GHz i5 | 3.3 GHz 2vCPU | 3.3 GHz 2vCPU |
![performance](docs/perf.PNG)
```elixir
points = [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000]
clojure_gui = [1.487, 1.397, 1.400, 1.381, 1.440, 5.784, 49.275]
elixir_gui = [0.005, 0.010, 0.004, 0.059, 0.939, 5.801, 43.464]
elixir_png = [0.002, 0.010, 0.049, 0.040, 0.349, 4.091, 41.521]
ubuntu_t2m = [0.004, 0.002, 0.001, 0.008, 0.211, 1.873, 19.916]
ubuntu_strm = [0.002, 0.001, 0.001, 0.009, 0.204, 1.279, 12.858]
datasets = for ds <- [clojure_gui, elixir_gui, elixir_png, ubuntu_t2m, ubuntu_strm], do:
Enum.zip(points, ds)
Gnuplot.plot([
[:set, :title, "Time to render scatter plots"],
[:set, :xlabel, "Points in plot"],
[:set, :ylabel, "Elapsed (s)"],
~w(set key left top)a,
~w(set logscale xy)a,
~w(set grid xtics ytics)a,
~w(set style line 1 lw 2 lc '#63b132')a,
~w(set style line 2 lw 2 lc '#2C001E')a,
~w(set style line 3 lw 2 lc '#5E2750')a,
~w(set style line 4 lw 2 lc '#E95420')a,
~w(set style line 5 lw 4 lc '#77216F')a,
Gnuplot.plots([
["-", :title, "Clojure GUI", :with, :lines, :ls, 1],
["-", :title, "Elixir GUI", :with, :lines, :ls, 2],
["-", :title, "Elixir PNG", :with, :lines, :ls, 3],
["-", :title, "Elixir t2.m", :with, :lines, :ls, 4],
["-", :title, "Elixir Stream", :with, :lines, :ls, 5]
])],
datasets
)
```
## Credits and licence
Original design ©2015 [Kyle Kingsbury][2].
Elixir code ©2022 [DEVSTOPFIX LTD][3]. Contributions from [piisgaaf](https://github.com/piisgaaf)
Distributed under the [Eclipse Public License v2][6].
[1]: http://www.gnuplot.info/
[2]: https://github.com/aphyr
[3]: http://www.devstopfix.com/
[4]: https://github.com/aphyr/gnuplot
[5]: http://erlang.org/doc/reference_manual/ports.html
[6]: https://www.eclipse.org/legal/epl-2.0/
[7]: https://elixir-lang.org/getting-started/basic-types.html