defmodule Gnuplot do
@moduledoc """
Interface to the Gnuplot graphing library.
Plot a sine function where Gnuplot generates the samples:
Gnuplot.plot([
~w(set autoscale)a,
~w(set samples 800)a,
[:plot, -30..20, 'sin(x*20)*atan(x)']
])
Plot a sine function where your program generates the data:
Gnuplot.plot([
[:plot, "-", :with, :lines :title, "sin(x*20)*atan(x)"]
],
[
for x <- -30_000..20_000, do: [x / 1000.0 , :math.sin(x * 20 / 1000.0) * :math.atan(x / 1000.0) ]
]
)
"""
alias Gnuplot.Commands
import Gnuplot.Dataset
import Gnuplot.Bin
@type command_term :: atom() | charlist() | number() | Range.t() | String.t()
@type command :: nonempty_list(command_term())
@timeout Application.compile_env(:gnuplot, :timeout, {10_000, :ms})
@doc """
Transmit commands without dataset.
"""
@spec plot(list(command())) ::
{:ok, String.t()} | {:error, String.t(), list(String.t())} | :timeout
def plot(commands), do: plot(commands, [])
@doc """
Transmit commands and datasets to Gnuplot.
## Examples
iex> Gnuplot.plot([[:plot, "-", :with, :lines]], [[[0, 0], [1, 2], [2, 4]]])
{:ok, "plot \"-\" with lines"}
"""
@spec plot(list(command()), list(Dataset.t())) ::
{:ok, String.t()} | {:error, String.t(), list(String.t())} | :timeout
def plot(commands, datasets) do
{:ok, path} = gnuplot_bin()
cmd = Commands.format(commands)
args = ["-p", "-e", cmd]
port =
Port.open({:spawn_executable, path}, [:binary, :exit_status, :stderr_to_stdout, args: args])
transmit(port, datasets)
loop(port, cmd)
end
defp loop(port, cmd, output \\ []) do
result =
receive do
{_, {:data, message}} -> loop(port, cmd, [message | output])
{_, {:exit_status, 0}} -> {:ok, cmd}
{_, {:exit_status, _}} -> {:error, cmd, Enum.reverse(output)}
after
timeout() -> :timeout
end
{_, :close} = send(port, {self(), :close})
result
end
defp timeout do
case @timeout do
{t, :ms} -> t
t when is_integer(t) -> t
end
end
@spec transmit(port(), list(Dataset.t())) :: :ok
defp transmit(port, datasets) do
:ok =
datasets
|> format_datasets()
|> Stream.each(fn row -> send(port, {self(), {:command, row}}) end)
|> Stream.run()
end
@doc """
Builds a comma separated list of plot commands that are overlayed in a single plot.
Only useful inside `plot/1`:
import Gnuplot
plot([
[:set, :title, "Sine vs Cosine"],
plots([
['sin(x)'],
['cos(x)']
])
])
is equivalent to:
set title "Sine vs Cosine"
plot sin(x),cos(x)
"""
@spec plots(list(command())) :: list()
def plots(commands) do
[:plot, list(commands)]
end
@doc """
Build a comma separated list of two or more overlayed 3D plots (as 2D projections).
Only useful inside `plot/1`:
import Gnuplot
plot([
[:set, :grid],
splots([
['x**2+y**2'],
['x**2-y**2']
])
])
"""
@spec splots(list(command())) :: list()
def splots(commands) do
[:splot, list(commands)]
end
@doc "Build a comma separated list from a list of terms."
def list(xs) when is_list(xs), do: %Commands.List{xs: xs}
@doc "Build a comma separated list of two terms."
def list(a, b), do: %Commands.List{xs: [a, b]}
@doc "Build a comma separated list of three terms."
def list(a, b, c), do: %Commands.List{xs: [a, b, c]}
@doc "Build a comma separated list of four terms."
def list(a, b, c, d), do: %Commands.List{xs: [a, b, c, d]}
@doc "Build a comma separated list of five terms."
def list(a, b, c, d, e), do: %Commands.List{xs: [a, b, c, d, e]}
@doc "Build a comma separated list of six terms."
def list(a, b, c, d, e, f), do: %Commands.List{xs: [a, b, c, d, e, f]}
end