-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, <<<<<<<<<<<<<>/binary, Base_node_style/binary>>/binary, Base_edge_style/binary>>/binary, Nodes/binary>>/binary, "\n"/utf8>>/binary, Edges/binary>>/binary, "\n}"/utf8>>.