A simple interface from Elixir data to the Gnuplot graphing utility that uses Erlang Ports 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.
Please visit the Gnuplot demos gallery to see all the possibilities, the manual which describes the grammar, and the examples folder.
This is a conversion of the Clojure Gnuplot library by aphyr. This library has been tested on OS X, Ubuntu 16.04 and CentOS 7.6.
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 - 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.
Lets compare the distributions of the Erlang rand functions:
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:
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}
.
Write two datasets to a PNG file:
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()])
])
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.
Gnuplot.plot([[:plot, 'sin(x)', :title, "Sine Wave"]])
Gnuplot.plot([
~w(set autoscale)a,
~w(set samples 800)a,
[:plot, -30..20, 'sin(x*20)*atan(x)']
])
NB ranges can be used
The multiplot
mode places serveral plots on the same page:
Gnuplot.plot([
[:set, :multiplot, :layout, '2,1'],
[:plot, 'sin(x)/x'],
[:plot, 'cos(x)']
])
This library is available in Hex with documentation and the package can be installed by adding gnuplot
to your project:
def deps do
[
{:gnuplot, "~> 1.22"}
]
end
Some tests create plots which require gnuplot
to be installed. They can be be excluded with:
mix test --exclude gnuplot:true
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 |
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
)
Original design ©2015 Kyle Kingsbury.
Elixir code ©2022 DEVSTOPFIX LTD. Contributions from piisgaaf
Distributed under the Eclipse Public License v2.