johnhamelink
2/17/2016 - 10:10 PM

gnuplot.ex

defmodule Api.Slack.Gnuplot do
  alias Porcelain.Process, as: Proc
  alias Porcelain.Result
  require Logger

  @type line :: %{title: String.t, dataset: [List.t]}

  @doc """
  Produces a line-graph PNG. The result is in binary.

  iex> Api.Slack.Gnuplot.line_graph([%{title: "Test", dataset: [[0,1], [1,5], [2,3]]}])
  iex> Api.Slack.Gnuplot.line_graph([ %{title: "A", dataset: [[0,1], [1,5], [2,3]]}, %{title: "B", dataset: [[0,3], [1,5], [2,9]]}, %{title: "C", dataset: [[0,4], [1,1], [2,6]]}, ])

  """
  @spec line_graph([line]) :: binary
  def line_graph(lines) do
    lines
    |> plot_series
    |> plot
  end

  @doc """
  Generate the inline plot & dataset configuration
  to send to gnuplot
  """
  def plot_series(series) do
    series = do_plot_series(series)

    require IEx
    IEx.pry

    axes = series
    |> Enum.map(fn(axis) -> axis.axis end)
    |> Enum.reduce("plot ", &(&2 <> &1 <> ", \n"))

    datasets = series
    |> Enum.map(fn(axis) -> axis.data end)
    |> Enum.reduce("", &(&2 <> &1 <> "\nEOF\n"))

    """
    #{axes}
    #{datasets}

    """
  end
  def do_plot_series([series | tail]) do
    [do_plot_series(series), do_plot_series(tail)]
    |> Enum.reject(&(&1 == nil))
  end
  def do_plot_series([series = %{}]), do: do_plot_series(series)
  def do_plot_series([]), do: nil
  def do_plot_series(%{title: title, dataset: dataset}) do

    # TODO: figure out the size of each array instead of
    # hardcoding 0 and 1
    data_string =
      dataset
      |> Enum.map(fn (set) ->
        "#{Enum.fetch!(set, 0)} #{Enum.fetch!(set, 1)}"
      end)
      |> Enum.join("\n")

    %{
      axis: ~s('-' using 1:2 title "#{title}" with lp),
      data: data_string
    }
  end

  @doc """
  plot will prepend generic styling rules to gnuplot, then send
  the final configuration to gnuplot to process. It'll then retrieve
  the response body and return it.
  """
  def plot(msg) do
    content = [
      "# Clean basis for SVG",
      "set encoding utf8",
      "set terminal pngcairo enhanced transparent font 'Verdana,10' size 640,480",

      "# Line width of the axes",
      "set border linewidth 1.5",

      "# line styles",
      "set style line 1 lt 1 lc rgb '#D53E4F' # red",
      "set style line 2 lt 1 lc rgb '#F46D43' # orange",
      "set style line 3 lt 1 lc rgb '#FDAE61' # pale orange",
      "set style line 4 lt 1 lc rgb '#FEE08B' # pale yellow-orange",
      "set style line 5 lt 1 lc rgb '#E6F598' # pale yellow-green",
      "set style line 6 lt 1 lc rgb '#ABDDA4' # pale green",
      "set style line 7 lt 1 lc rgb '#66C2A5' # green",
      "set style line 8 lt 1 lc rgb '#3288BD' # blue",

      "# palette",
      "set palette defined ( \\",
        "0 '#D53E4F',\\",
        "1 '#F46D43',\\",
        "2 '#FDAE61',\\",
        "3 '#FEE08B',\\",
        "4 '#E6F598',\\",
        "5 '#ABDDA4',\\",
        "6 '#66C2A5',\\",
        "7 '#3288BD'\\",
      ")",
      msg
    ] |> Enum.join("\n")
      |> String.replace(~s("), ~s(\\"))

    cmd = ~s(echo "#{content}" | gnuplot)

    IO.puts(cmd)

    %Porcelain.Result{err: nil, out: out, status: 0} =
      cmd
      |> Porcelain.shell

      File.write("/Users/john/out.png", out)

    out
  end

end