defmodule Graphic do @moduledoc """ Build directed graphs, query them, and compile into common diagramming languages. [![Run in Livebook](https://livebook.dev/badge/v1/black.svg)](https://livebook.dev/run?url=https%3A%2F%2Fshare.operand.online%2Fgram%2Fgraphic%2Fdigraph.livemd) iex> alias Graphic, as: G iex> G.new() |> G.bridge("a", "b") |> G.edges() [ {"a", "b", []} ] Edges can also be assigned a label. iex> alias Graphic, as: G iex> G.new() |> G.bridge("a", "b", "broken!") |> G.edges() [ {"a", "b", "broken!"} ] """ @moduledoc since: "0.1.2" @dg :digraph def new(), do: :digraph.new() @doc "add a plain edge, and any missing nodes." def bridge(g, a, b) do if !(g|>@dg.vertex(a)), do: g|>@dg.add_vertex(a) if !(g|>@dg.vertex(b)), do: g|>@dg.add_vertex(b) g|>@dg.add_edge(a, b) g end @doc "add a labeled edge, and any missing nodes." def bridge(g, a, b, label) do if !(g|>@dg.vertex(a)), do: g|>@dg.add_vertex(a) if !(g|>@dg.vertex(b)), do: g|>@dg.add_vertex(b) g|>@dg.add_edge(a, b, label) g end @doc "query all edges in graph." def edges(g) do g |>@dg.edges() |>Enum.map(& g|>@dg.edge(&1)) |>Enum.map(fn {_, a, b, l} -> {a, b, l} end) end @doc "query all edges from a node." def edges(g, n) do g |>@dg.out_edges(n) |>Enum.map(& g|>@dg.edge(&1)) |>Enum.map(fn {_, a, b, l} -> {a, b, l} end) end # choose a logical order, used in rendering. defp order(g), do: g|>:digraph_utils.topsort() @doc ~S""" render graph as a [d2 diagram](https://d2lang.com). iex>alias Graphic, as: G iex> G.new() ...> |>G.bridge("a", "b") ...> |>G.bridge("a", "c") ...> |>G.bridge("c", "d", "broken!") ...> |>G.bridge("b", "d") ...> |>G.as_d2() \""" a -> b a -> c c -> d: 'broken!' b -> d \""" """ @doc since: "0.1.2" def as_d2(g) do g |>order() |>Enum.map(& g|>edges(&1)) |>List.flatten() |>Enum.map(fn {a, b, label} -> "#{a} -> #{b}#{if (label != []), do: ": '#{label}'"}" end) |>Enum.concat([""]) |>Enum.join("\n") end @doc "describe neighboring nodes." def neighbors(g, n), do: g|>@dg.out_neighbours(n) end