Packages

Clarity is an interactive introspection and visualization tool for Elixir projects, providing navigable graphs and diagrams for frameworks like Ash, Phoenix, and Ecto.

Current section

Files

Jump to
clarity lib clarity graph util.ex
Raw

lib/clarity/graph/util.ex

# credo:disable-for-this-file Credo.Check.Refactor.Nesting
defmodule Clarity.Graph.Util do
@moduledoc false
@doc """
Return a MapSet of all vertices reachable from `root_vertex` within
**`max_outgoing_steps` forward hops** *or* **`max_incoming_steps` backward hops**.
The traversal is breadth‑first in each direction, so the overall complexity
remains linear in the size of the resulting vertex set (`O(Vₛ)`).
## Parameters
* `original_graph` – the `:digraph` you want to sample from.
* `root_vertex` – where traversal starts.
* `max_outgoing_steps` – how far to follow *outgoing* edges.
* `max_incoming_steps` – how far to follow *incoming* edges.
"""
@spec vertices_within_steps(
original_graph :: :digraph.graph(),
root_vertex :: :digraph.vertex(),
max_outgoing_steps :: non_neg_integer(),
max_incoming_steps :: non_neg_integer()
) :: MapSet.t(:digraph.vertex())
def vertices_within_steps(original_graph, root_vertex, max_outgoing_steps, max_incoming_steps)
when max_outgoing_steps >= 0 and max_incoming_steps >= 0 do
visited_vertices =
bfs_direction(
original_graph,
MapSet.new([root_vertex]),
root_vertex,
max_outgoing_steps,
&:digraph.out_neighbours/2
)
bfs_direction(
original_graph,
visited_vertices,
root_vertex,
max_incoming_steps,
&:digraph.in_neighbours/2
)
end
@spec bfs_direction(
original_graph :: :digraph.graph(),
visited :: MapSet.t(:digraph.vertex()),
start_vertex :: :digraph.vertex(),
max_steps :: non_neg_integer(),
neighbours_fun :: (:digraph.graph(), :digraph.vertex() -> [:digraph.vertex()])
) :: MapSet.t(:digraph.vertex())
defp bfs_direction(_original_graph, visited, _start_vertex, 0, _), do: visited
defp bfs_direction(original_graph, visited, start_vertex, max_steps, neighbours_fun) do
queue = :queue.in({start_vertex, 0}, :queue.new())
bfs_loop(queue, original_graph, visited, max_steps, neighbours_fun)
end
@spec bfs_loop(
queue :: :queue.queue(),
original_graph :: :digraph.graph(),
visited :: MapSet.t(:digraph.vertex()),
max_steps :: non_neg_integer(),
neighbours_fun :: (:digraph.graph(), :digraph.vertex() -> [:digraph.vertex()])
) :: MapSet.t(:digraph.vertex())
defp bfs_loop(queue, original_graph, visited, max_steps, neighbours_fun) do
case :queue.out(queue) do
{:empty, _} ->
visited
{{:value, {current_vertex, current_depth}}, queue_tail} ->
{updated_queue, updated_visited} =
if current_depth < max_steps do
expand(
current_vertex,
current_depth,
original_graph,
visited,
queue_tail,
neighbours_fun
)
else
{queue_tail, visited}
end
bfs_loop(
updated_queue,
original_graph,
updated_visited,
max_steps,
neighbours_fun
)
end
end
@spec expand(
vertex :: :digraph.vertex(),
depth :: non_neg_integer(),
original_graph :: :digraph.graph(),
visited :: MapSet.t(:digraph.vertex()),
queue :: :queue.queue(),
neighbours_fun :: (:digraph.graph(), :digraph.vertex() -> [:digraph.vertex()])
) :: {:queue.queue(), MapSet.t(:digraph.vertex())}
defp expand(vertex, depth, original_graph, visited, queue, neighbours_fun) do
neighbours = neighbours_fun.(original_graph, vertex)
Enum.reduce(neighbours, {queue, visited}, fn neighbour, {q_acc, s_acc} ->
if MapSet.member?(s_acc, neighbour) do
{q_acc, s_acc}
else
{:queue.in({neighbour, depth + 1}, q_acc), MapSet.put(s_acc, neighbour)}
end
end)
end
end