Current section
Files
Jump to
Current section
Files
src/yog@io@dot.erl
-module(yog@io@dot).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/yog/io/dot.gleam").
-export([default_dot_options/0, to_dot/2, path_to_dot_options/2]).
-export_type([dot_options/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" DOT (Graphviz) format export for visualizing graphs.\n"
"\n"
" This module exports graphs to the [DOT language](https://graphviz.org/doc/info/lang.html),\n"
" which is the native format for [Graphviz](https://graphviz.org/) - a powerful open-source\n"
" graph visualization tool. The exported files can be rendered to PNG, SVG, PDF, and other\n"
" formats using the `dot`, `neato`, `circo`, or other Graphviz layout engines.\n"
"\n"
" ## Quick Start\n"
"\n"
" ```gleam\n"
" import yog/io/dot\n"
"\n"
" // Export with default styling\n"
" let dot_string = dot.to_string(my_graph)\n"
"\n"
" // Write to file and render with Graphviz CLI\n"
" // $ dot -Tpng output.dot -o graph.png\n"
" ```\n"
"\n"
" ## Customization\n"
"\n"
" Use `DotOptions` to customize:\n"
" - Node labels and shapes\n"
" - Edge labels and styles\n"
" - Highlight specific nodes or paths\n"
" - Graph direction (LR, TB, etc.)\n"
"\n"
" ## Rendering Options\n"
"\n"
" | Engine | Best For |\n"
" |--------|----------|\n"
" | `dot` | Hierarchical layouts (DAGs, trees) |\n"
" | `neato` | Spring-based layouts (undirected) |\n"
" | `circo` | Circular layouts |\n"
" | `fdp` | Force-directed layouts |\n"
" | `sfdp` | Large graphs |\n"
"\n"
" ## References\n"
"\n"
" - [Graphviz Documentation](https://graphviz.org/documentation/)\n"
" - [DOT Language Guide](https://graphviz.org/doc/info/lang.html)\n"
" - [Node Shapes](https://graphviz.org/doc/info/shapes.html)\n"
" - [Arrow Styles](https://graphviz.org/doc/info/arrows.html)\n"
).
-type dot_options() :: {dot_options,
fun((integer(), binary()) -> binary()),
fun((binary()) -> binary()),
gleam@option:option(list(integer())),
gleam@option:option(list({integer(), integer()})),
binary(),
binary()}.
-file("src/yog/io/dot.gleam", 72).
?DOC(" Creates default DOT options with simple labeling.\n").
-spec default_dot_options() -> dot_options().
default_dot_options() ->
{dot_options,
fun(Id, _) -> erlang:integer_to_binary(Id) end,
fun(Weight) -> Weight end,
none,
none,
<<"ellipse"/utf8>>,
<<"red"/utf8>>}.
-file("src/yog/io/dot.gleam", 112).
?DOC(
" Converts a graph to DOT (Graphviz) syntax.\n"
"\n"
" The graph's node data and edge data must be convertible to strings.\n"
" Use the options to customize labels and highlighting.\n"
"\n"
" **Time Complexity:** O(V + E)\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let graph =\n"
" model.new(Directed)\n"
" |> model.add_node(1, \"Start\")\n"
" |> model.add_node(2, \"Process\")\n"
" |> model.add_edge(from: 1, to: 2, with: \"5\")\n"
"\n"
" let diagram = dot.to_dot(graph, default_dot_options())\n"
" // io.println(diagram)\n"
" ```\n"
"\n"
" This output can be processed by Graphviz tools (e.g., `dot -Tpng -o graph.png`):\n"
" ````dot\n"
" digraph G {\n"
" node [shape=ellipse];\n"
" 1 [label=\"Start\"];\n"
" 2 [label=\"Process\"];\n"
" 1 -> 2 [label=\"5\"];\n"
" }\n"
" ````\n"
).
-spec to_dot(yog@model:graph(binary(), binary()), dot_options()) -> binary().
to_dot(Graph, Options) ->
Graph_type = case erlang:element(2, Graph) of
directed ->
<<"digraph G {\n"/utf8>>;
undirected ->
<<"graph G {\n"/utf8>>
end,
Base_node_style = <<<<" node [shape="/utf8,
(erlang:element(6, Options))/binary>>/binary,
"];\n"/utf8>>,
Base_edge_style = <<" edge [fontname=\"Helvetica\", fontsize=10];\n"/utf8>>,
Nodes = begin
_pipe = gleam@dict:fold(
erlang:element(3, Graph),
[],
fun(Acc, Id, Data) ->
Label = (erlang:element(2, Options))(Id, Data),
Id_str = erlang:integer_to_binary(Id),
Mut_attrs = case erlang:element(4, Options) of
{some, Highlighted} ->
case gleam@list:contains(Highlighted, Id) of
true ->
<<<<" fillcolor=\""/utf8,
(erlang:element(7, Options))/binary>>/binary,
"\", style=filled"/utf8>>;
false ->
<<""/utf8>>
end;
none ->
<<""/utf8>>
end,
[<<<<<<<<<<<<" "/utf8, Id_str/binary>>/binary,
" [label=\""/utf8>>/binary,
Label/binary>>/binary,
"\""/utf8>>/binary,
Mut_attrs/binary>>/binary,
"];"/utf8>> |
Acc]
end
),
gleam@string:join(_pipe, <<"\n"/utf8>>)
end,
Edges = begin
_pipe@1 = gleam@dict:fold(
erlang:element(4, Graph),
[],
fun(Acc@1, From_id, Targets) ->
Inner_edges = gleam@dict:fold(
Targets,
[],
fun(Inner_acc, To_id, Weight) ->
Is_valid = case erlang:element(2, Graph) of
undirected ->
From_id =< To_id;
directed ->
true
end,
case Is_valid of
false ->
Inner_acc;
true ->
Connector = case erlang:element(2, Graph) of
directed ->
<<" -> "/utf8>>;
undirected ->
<<" -- "/utf8>>
end,
Is_highlighted = case erlang:element(5, Options) of
{some, Highlighted@1} ->
gleam@list:contains(
Highlighted@1,
{From_id, To_id}
)
orelse gleam@list:contains(
Highlighted@1,
{To_id, From_id}
);
none ->
false
end,
Mut_attrs@1 = case Is_highlighted of
true ->
<<<<" color=\""/utf8,
(erlang:element(7, Options))/binary>>/binary,
"\", penwidth=2"/utf8>>;
false ->
<<""/utf8>>
end,
Edge_def = <<<<<<<<<<<<<<<<" "/utf8,
(erlang:integer_to_binary(
From_id
))/binary>>/binary,
Connector/binary>>/binary,
(erlang:integer_to_binary(
To_id
))/binary>>/binary,
" [label=\""/utf8>>/binary,
((erlang:element(3, Options))(
Weight
))/binary>>/binary,
"\""/utf8>>/binary,
Mut_attrs@1/binary>>/binary,
"];"/utf8>>,
[Edge_def | Inner_acc]
end
end
),
lists:append([Inner_edges, Acc@1])
end
),
gleam@string:join(_pipe@1, <<"\n"/utf8>>)
end,
<<<<<<<<<<<<Graph_type/binary, Base_node_style/binary>>/binary,
Base_edge_style/binary>>/binary,
Nodes/binary>>/binary,
"\n"/utf8>>/binary,
Edges/binary>>/binary,
"\n}"/utf8>>.
-file("src/yog/io/dot.gleam", 239).
-spec path_to_edges(list(integer())) -> list({integer(), integer()}).
path_to_edges(Nodes) ->
case Nodes of
[] ->
[];
[_] ->
[];
[First, Second | Rest] ->
[{First, Second} | path_to_edges([Second | Rest])]
end.
-file("src/yog/io/dot.gleam", 224).
?DOC(
" Converts a shortest path result to highlighted DOT options.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let path = pathfinding.shortest_path(\n"
" in: graph,\n"
" from: 1,\n"
" to: 5,\n"
" with_zero: \"0\",\n"
" with_add: string_add, // Assume these exist or map to int/float\n"
" with_compare: string_compare,\n"
" )\n"
"\n"
" case path {\n"
" Some(p) -> {\n"
" let options = dot.path_to_dot_options(p, default_dot_options())\n"
" let diagram = dot.to_dot(graph, options)\n"
" io.println(diagram)\n"
" }\n"
" None -> io.println(\"No path found\")\n"
" }\n"
" ```\n"
).
-spec path_to_dot_options(yog@pathfinding@utils:path(any()), dot_options()) -> dot_options().
path_to_dot_options(Path, Base_options) ->
Nodes = erlang:element(2, Path),
Edges = path_to_edges(Nodes),
{dot_options,
erlang:element(2, Base_options),
erlang:element(3, Base_options),
{some, Nodes},
{some, Edges},
erlang:element(6, Base_options),
erlang:element(7, Base_options)}.