lib/archeometer/graphs/graphviz.ex

defmodule Archeometer.Graphs.Graphviz do
  @moduledoc """
    Functions for working with Graphviz graphs
  """

  @dot_template """
  digraph G {
    bgcolor="transparent";
    node [color= "#9470db" fillcolor="#ECECFF" shape="rectangle" style="filled,rounded" fontname="Arial Sans"];
    <%= for module <- Map.keys(@xrefs) do %>
      "<%= module %>";
    <% end %>

    <%= for {mod, refs} <- @xrefs do %>
      <%= for ref <- refs do %>
        "<%= mod %>" -> "<%= ref %>";
      <% end %>
    <% end %>
  }
  """

  @doc """
  Renders an graph represented by an adjacency list into .dot format.

  Returns a string containing de rendered graph.

  ## Examples

      iex(2)> graph = %{
      ...(2)>   :a => [:b, :c],
      ...(2)>   :b => [:c, :d],
      ...(2)>   :d => [:a]
      ...(2)> }
      %{a: [:b, :c], b: [:c, :d], d: [:a]}
      iex(3)> dot_str = Graphviz.render_dot(graph)
      iex(3)> assert is_binary(dot_str)
  """
  def render_dot(xrefs) do
    @dot_template
    |> EEx.eval_string(assigns: [xrefs: xrefs])
    |> String.replace(~r/(\n\s+\n)+|\n{2,}/, "\n")
  end

  @doc """
  Renders an graph represented by an adjacency list into .png format.

  Returns a binary containing the rendered graph.

  """
  def render_image(xrefs, format \\ "png") do
    xrefs
    |> render_dot()
    |> write_temp_file()
    |> gen_from_dot(format)
  end

  defp write_temp_file(dot_str) do
    {:ok, fd, file_path} = Mix.Project.config()[:app] |> to_string() |> Temp.open()
    IO.write(fd, dot_str)
    File.close(fd)
    file_path
  end

  defp gen_from_dot(dot_path, format) do
    png_path = Temp.path!(suffix: ".#{format}")
    System.cmd("dot", ["-T#{format}", dot_path, "-o", png_path])
    File.read!(png_path)
  end
end