Current section
Files
Jump to
Current section
Files
src/yog@render@dot.erl
-module(yog@render@dot).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/yog/render/dot.gleam").
-export([default_dot_options/0, path_to_dot_options/2, to_dot/2]).
-export_type([layout/0, rank_dir/0, node_shape/0, style/0, splines/0, arrow_style/0, overlap/0, 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/render/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 layout() :: dot |
neato |
circo |
fdp |
sfdp |
twopi |
osage |
{custom_layout, binary()}.
-type rank_dir() :: top_to_bottom |
left_to_right |
bottom_to_top |
right_to_left.
-type node_shape() :: box |
circle |
ellipse |
diamond |
hexagon |
pentagon |
octagon |
triangle |
rectangle |
square |
rect |
inv_triangle |
house |
inv_house |
parallelogram |
trapezoid |
{custom_shape, binary()}.
-type style() :: solid |
dashed |
dotted |
bold |
filled |
rounded |
diagonals |
striped |
wedged.
-type splines() :: line | polyline | curved | ortho | spline | splines_none.
-type arrow_style() :: normal |
arrow_dot |
arrow_diamond |
o_diamond |
arrow_box |
crow |
vee |
inv |
tee |
arrow_none |
{custom_arrow, binary()}.
-type overlap() :: overlap_true |
overlap_false |
scale |
scale_x_y |
prism |
{custom_overlap, binary()}.
-type dot_options() :: {dot_options,
fun((integer(), binary()) -> binary()),
fun((binary()) -> binary()),
gleam@option:option(list(integer())),
gleam@option:option(list({integer(), integer()})),
binary(),
gleam@option:option(layout()),
gleam@option:option(rank_dir()),
gleam@option:option(binary()),
gleam@option:option(splines()),
gleam@option:option(overlap()),
gleam@option:option(float()),
gleam@option:option(float()),
node_shape(),
binary(),
style(),
binary(),
integer(),
binary(),
binary(),
style(),
binary(),
integer(),
float(),
gleam@option:option(arrow_style()),
gleam@option:option(arrow_style()),
binary(),
float()}.
-file("src/yog/render/dot.gleam", 265).
?DOC(
" Creates default DOT options with simple labeling and sensible styling.\n"
"\n"
" Default configuration:\n"
" - Layout: Auto-detected by Graphviz\n"
" - Direction: Top-to-bottom\n"
" - Node shape: Ellipse\n"
" - Colors: Light blue nodes, black edges\n"
" - Font: Helvetica 12pt\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,
<<"G"/utf8>>,
none,
{some, top_to_bottom},
none,
none,
none,
none,
none,
ellipse,
<<"lightblue"/utf8>>,
filled,
<<"Helvetica"/utf8>>,
12,
<<"black"/utf8>>,
<<"black"/utf8>>,
solid,
<<"Helvetica"/utf8>>,
10,
1.0,
none,
none,
<<"red"/utf8>>,
2.0}.
-file("src/yog/render/dot.gleam", 502).
-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/render/dot.gleam", 487).
?DOC(" Converts a shortest path result to highlighted DOT options.\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),
erlang:element(8, Base_options),
erlang:element(9, Base_options),
erlang:element(10, Base_options),
erlang:element(11, Base_options),
erlang:element(12, Base_options),
erlang:element(13, Base_options),
erlang:element(14, Base_options),
erlang:element(15, Base_options),
erlang:element(16, Base_options),
erlang:element(17, Base_options),
erlang:element(18, Base_options),
erlang:element(19, Base_options),
erlang:element(20, Base_options),
erlang:element(21, Base_options),
erlang:element(22, Base_options),
erlang:element(23, Base_options),
erlang:element(24, Base_options),
erlang:element(25, Base_options),
erlang:element(26, Base_options),
erlang:element(27, Base_options),
erlang:element(28, Base_options)}.
-file("src/yog/render/dot.gleam", 517).
?DOC(" Convert Layout to Graphviz string\n").
-spec layout_to_string(layout()) -> binary().
layout_to_string(Layout) ->
case Layout of
dot ->
<<"dot"/utf8>>;
neato ->
<<"neato"/utf8>>;
circo ->
<<"circo"/utf8>>;
fdp ->
<<"fdp"/utf8>>;
sfdp ->
<<"sfdp"/utf8>>;
twopi ->
<<"twopi"/utf8>>;
osage ->
<<"osage"/utf8>>;
{custom_layout, S} ->
S
end.
-file("src/yog/render/dot.gleam", 531).
?DOC(" Convert RankDir to Graphviz string\n").
-spec rankdir_to_string(rank_dir()) -> binary().
rankdir_to_string(Rd) ->
case Rd of
top_to_bottom ->
<<"TB"/utf8>>;
left_to_right ->
<<"LR"/utf8>>;
bottom_to_top ->
<<"BT"/utf8>>;
right_to_left ->
<<"RL"/utf8>>
end.
-file("src/yog/render/dot.gleam", 541).
?DOC(" Convert NodeShape to Graphviz string\n").
-spec node_shape_to_string(node_shape()) -> binary().
node_shape_to_string(Shape) ->
case Shape of
box ->
<<"box"/utf8>>;
circle ->
<<"circle"/utf8>>;
ellipse ->
<<"ellipse"/utf8>>;
diamond ->
<<"diamond"/utf8>>;
hexagon ->
<<"hexagon"/utf8>>;
pentagon ->
<<"pentagon"/utf8>>;
octagon ->
<<"octagon"/utf8>>;
triangle ->
<<"triangle"/utf8>>;
rectangle ->
<<"rectangle"/utf8>>;
square ->
<<"square"/utf8>>;
rect ->
<<"rect"/utf8>>;
inv_triangle ->
<<"invtriangle"/utf8>>;
house ->
<<"house"/utf8>>;
inv_house ->
<<"invhouse"/utf8>>;
parallelogram ->
<<"parallelogram"/utf8>>;
trapezoid ->
<<"trapezoid"/utf8>>;
{custom_shape, S} ->
S
end.
-file("src/yog/render/dot.gleam", 564).
?DOC(" Convert Style to Graphviz string\n").
-spec style_to_string(style()) -> binary().
style_to_string(Style) ->
case Style of
solid ->
<<"solid"/utf8>>;
dashed ->
<<"dashed"/utf8>>;
dotted ->
<<"dotted"/utf8>>;
bold ->
<<"bold"/utf8>>;
filled ->
<<"filled"/utf8>>;
rounded ->
<<"rounded"/utf8>>;
diagonals ->
<<"diagonals"/utf8>>;
striped ->
<<"striped"/utf8>>;
wedged ->
<<"wedged"/utf8>>
end.
-file("src/yog/render/dot.gleam", 579).
?DOC(" Convert Splines to Graphviz string\n").
-spec splines_to_string(splines()) -> binary().
splines_to_string(Splines) ->
case Splines of
line ->
<<"line"/utf8>>;
polyline ->
<<"polyline"/utf8>>;
curved ->
<<"curved"/utf8>>;
ortho ->
<<"ortho"/utf8>>;
spline ->
<<"spline"/utf8>>;
splines_none ->
<<"none"/utf8>>
end.
-file("src/yog/render/dot.gleam", 591).
?DOC(" Convert ArrowStyle to Graphviz string\n").
-spec arrow_style_to_string(arrow_style()) -> binary().
arrow_style_to_string(Arrow) ->
case Arrow of
normal ->
<<"normal"/utf8>>;
arrow_dot ->
<<"dot"/utf8>>;
arrow_diamond ->
<<"diamond"/utf8>>;
o_diamond ->
<<"odiamond"/utf8>>;
arrow_box ->
<<"box"/utf8>>;
crow ->
<<"crow"/utf8>>;
vee ->
<<"vee"/utf8>>;
inv ->
<<"inv"/utf8>>;
tee ->
<<"tee"/utf8>>;
arrow_none ->
<<"none"/utf8>>;
{custom_arrow, S} ->
S
end.
-file("src/yog/render/dot.gleam", 608).
?DOC(" Convert Overlap to Graphviz string\n").
-spec overlap_to_string(overlap()) -> binary().
overlap_to_string(Overlap) ->
case Overlap of
overlap_true ->
<<"true"/utf8>>;
overlap_false ->
<<"false"/utf8>>;
scale ->
<<"scale"/utf8>>;
scale_x_y ->
<<"scalexy"/utf8>>;
prism ->
<<"prism"/utf8>>;
{custom_overlap, S} ->
S
end.
-file("src/yog/render/dot.gleam", 330).
?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 "/utf8, (erlang:element(6, Options))/binary>>/binary,
" {\n"/utf8>>;
undirected ->
<<<<"graph "/utf8, (erlang:element(6, Options))/binary>>/binary,
" {\n"/utf8>>
end,
Graph_attrs = begin
_pipe = [gleam@option:map(
erlang:element(7, Options),
fun(V) -> <<"layout="/utf8, (layout_to_string(V))/binary>> end
),
gleam@option:map(
erlang:element(8, Options),
fun(V@1) ->
<<"rankdir="/utf8, (rankdir_to_string(V@1))/binary>>
end
),
gleam@option:map(
erlang:element(9, Options),
fun(V@2) ->
<<<<"bgcolor=\""/utf8, V@2/binary>>/binary, "\""/utf8>>
end
),
gleam@option:map(
erlang:element(10, Options),
fun(V@3) ->
<<"splines="/utf8, (splines_to_string(V@3))/binary>>
end
),
gleam@option:map(
erlang:element(11, Options),
fun(V@4) ->
<<"overlap="/utf8, (overlap_to_string(V@4))/binary>>
end
),
gleam@option:map(
erlang:element(12, Options),
fun(V@5) ->
<<"nodesep="/utf8,
(gleam_stdlib:float_to_string(V@5))/binary>>
end
),
gleam@option:map(
erlang:element(13, Options),
fun(V@6) ->
<<"ranksep="/utf8,
(gleam_stdlib:float_to_string(V@6))/binary>>
end
)],
gleam@list:filter_map(_pipe, fun(Opt) -> case Opt of
{some, Attr} ->
{ok, Attr};
none ->
{error, nil}
end end)
end,
Graph_attr_line = case Graph_attrs of
[] ->
<<""/utf8>>;
Attrs ->
<<<<" graph ["/utf8,
(gleam@string:join(Attrs, <<", "/utf8>>))/binary>>/binary,
"];\n"/utf8>>
end,
Node_attrs = [<<"shape="/utf8,
(node_shape_to_string(erlang:element(14, Options)))/binary>>,
<<"style="/utf8, (style_to_string(erlang:element(16, Options)))/binary>>,
<<<<"fillcolor=\""/utf8, (erlang:element(15, Options))/binary>>/binary,
"\""/utf8>>,
<<<<"fontname=\""/utf8, (erlang:element(17, Options))/binary>>/binary,
"\""/utf8>>,
<<"fontsize="/utf8,
(erlang:integer_to_binary(erlang:element(18, Options)))/binary>>,
<<<<"fontcolor=\""/utf8, (erlang:element(19, Options))/binary>>/binary,
"\""/utf8>>],
Base_node_style = <<<<" node ["/utf8,
(gleam@string:join(Node_attrs, <<", "/utf8>>))/binary>>/binary,
"];\n"/utf8>>,
Edge_attrs = [<<<<"color=\""/utf8, (erlang:element(20, Options))/binary>>/binary,
"\""/utf8>>,
<<"style="/utf8, (style_to_string(erlang:element(21, Options)))/binary>>,
<<<<"fontname=\""/utf8, (erlang:element(22, Options))/binary>>/binary,
"\""/utf8>>,
<<"fontsize="/utf8,
(erlang:integer_to_binary(erlang:element(23, Options)))/binary>>,
<<"penwidth="/utf8,
(gleam_stdlib:float_to_string(erlang:element(24, Options)))/binary>>],
Edge_attrs_with_arrows = case {erlang:element(25, Options),
erlang:element(26, Options)} of
{{some, Head}, {some, Tail}} ->
[<<"arrowhead="/utf8, (arrow_style_to_string(Head))/binary>>,
<<"arrowtail="/utf8, (arrow_style_to_string(Tail))/binary>> |
Edge_attrs];
{{some, Head@1}, none} ->
[<<"arrowhead="/utf8, (arrow_style_to_string(Head@1))/binary>> |
Edge_attrs];
{none, {some, Tail@1}} ->
[<<"arrowtail="/utf8, (arrow_style_to_string(Tail@1))/binary>> |
Edge_attrs];
{none, none} ->
Edge_attrs
end,
Base_edge_style = <<<<" edge ["/utf8,
(gleam@string:join(Edge_attrs_with_arrows, <<", "/utf8>>))/binary>>/binary,
"];\n"/utf8>>,
Nodes = begin
_pipe@1 = 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),
Highlight_attrs = case erlang:element(4, Options) of
{some, Highlighted} ->
case gleam@list:contains(Highlighted, Id) of
true ->
<<<<", fillcolor=\""/utf8,
(erlang:element(27, Options))/binary>>/binary,
"\""/utf8>>;
false ->
<<""/utf8>>
end;
none ->
<<""/utf8>>
end,
[<<<<<<<<<<<<" "/utf8, Id_str/binary>>/binary,
" [label=\""/utf8>>/binary,
Label/binary>>/binary,
"\""/utf8>>/binary,
Highlight_attrs/binary>>/binary,
"];"/utf8>> |
Acc]
end
),
gleam@string:join(_pipe@1, <<"\n"/utf8>>)
end,
Edges = begin
_pipe@2 = 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 = case Is_highlighted of
true ->
<<<<<<" color=\""/utf8,
(erlang:element(27, Options))/binary>>/binary,
"\", penwidth="/utf8>>/binary,
(gleam_stdlib:float_to_string(
erlang:element(28, Options)
))/binary>>;
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/binary>>/binary,
"];"/utf8>>,
[Edge_def | Inner_acc]
end
end
),
lists:append([Inner_edges, Acc@1])
end
),
gleam@string:join(_pipe@2, <<"\n"/utf8>>)
end,
<<<<<<<<<<<<<<Graph_type/binary, Graph_attr_line/binary>>/binary,
Base_node_style/binary>>/binary,
Base_edge_style/binary>>/binary,
Nodes/binary>>/binary,
"\n"/utf8>>/binary,
Edges/binary>>/binary,
"\n}"/utf8>>.