• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

code-shoily / choreo / 17b14252751eb2cfad891ce2a504c01050e94f0e

26 Apr 2026 08:17PM UTC coverage: 88.482% (-4.0%) from 92.503%
17b14252751eb2cfad891ce2a504c01050e94f0e

push

github

code-shoily
Add customization on rendering params

88 of 168 new or added lines in 8 files covered. (52.38%)

7 existing lines in 7 files now uncovered.

1498 of 1693 relevant lines covered (88.48%)

18.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.72
/lib/choreo/render/dot.ex
1
defmodule Choreo.Render.DOT do
2
  @moduledoc """
3
  DOT (Graphviz) rendering for `Choreo` architecture diagrams.
4

5
  This module translates a `Choreo` struct into the DOT language with
6
  sensible defaults for infrastructure diagrams:
7

8
  * Databases are rendered as **cylinders**
9
  * Caches as **octagons**
10
  * Services as **3D boxes**
11
  * Networks as **clouds**
12
  * Users as **double circles**
13
  * Load balancers as **hexagons**
14
  * Queues as **components**
15
  * Storage as **tabs**
16

17
  Edges are styled according to their semantic type (`:connection` or
18
  `:dataflow`).
19

20
  Themes (built-in or custom) control colours, shapes, fonts, and layout.
21
  See `Choreo.Theme` for details.
22
  """
23

24
  alias Choreo.Theme
25

26
  @doc """
27
  Renders a `Choreo` to a DOT string.
28

29
  ## Options
30

31
    * `:theme` - `:default`, `:dark`, `:minimal`, or a `Choreo.Theme` struct
32
    * `:subgraphs` - list of subgraph / cluster definitions (manual override)
33
    * `:ranks` - rank constraints
34
    * Any other option accepted by `Yog.Render.DOT.to_dot/2`
35

36
  ## Examples
37

38
      dot = Choreo.Render.DOT.to_dot(system)
39
      dot = Choreo.Render.DOT.to_dot(system, theme: :dark)
40

41
      theme = Choreo.Theme.custom(colors: [database: "#ff0000"])
42
      dot = Choreo.Render.DOT.to_dot(system, theme: theme)
43
  """
44
  @spec to_dot(Choreo.t(), keyword()) :: String.t()
45
  def to_dot(system, opts \\ []) do
46
    theme = resolve_theme(Keyword.get(opts, :theme, :default))
14✔
47

48
    subgraphs = Choreo.Internal.build_cluster_subgraphs(system, theme)
14✔
49

50
    base =
14✔
51
      Yog.Multi.DOT.default_options()
52
      |> Map.put(:node_label, &node_label/2)
53
      |> Map.put(:edge_label, fn _edge_id, weight -> edge_label(weight) end)
3✔
54
      |> Map.put(:node_attributes, node_attributes_fn(system, theme))
55
      |> Map.put(:edge_attributes, edge_attributes_fn(system))
56
      |> Map.merge(theme_graph_attrs(theme))
57
      |> Map.merge(Map.new(opts))
58

59
    base = if subgraphs != [], do: Map.put(base, :subgraphs, subgraphs), else: base
14✔
60

61
    Yog.Multi.DOT.to_dot(system.graph, base)
14✔
62
  end
63

64
  # ============================================================================
65
  # Theme resolution
66
  # ============================================================================
67

68
  defp resolve_theme(%Theme{} = theme), do: theme
4✔
69
  defp resolve_theme(:default), do: Theme.default()
7✔
70
  defp resolve_theme(:dark), do: Theme.dark()
1✔
71
  defp resolve_theme(:minimal), do: Theme.minimal()
2✔
NEW
72
  defp resolve_theme(:warm), do: Theme.warm()
×
NEW
73
  defp resolve_theme(:forest), do: Theme.forest()
×
NEW
74
  defp resolve_theme(:ocean), do: Theme.ocean()
×
UNCOV
75
  defp resolve_theme(_), do: Theme.default()
×
76

77
  defp theme_graph_attrs(%Theme{} = theme) do
78
    %{
79
      rankdir: theme.graph_rankdir,
14✔
80
      splines: theme.graph_splines,
14✔
81
      nodesep: theme.graph_nodesep,
14✔
82
      ranksep: theme.graph_ranksep,
14✔
83
      bgcolor: theme.graph_bgcolor,
14✔
84
      edge_color: theme.edge_color,
14✔
85
      edge_fontcolor: theme.edge_color,
14✔
86
      edge_fontname: theme.edge_fontname,
14✔
87
      edge_fontsize: theme.edge_fontsize,
14✔
88
      edge_penwidth: theme.edge_penwidth,
14✔
89
      node_fontname: theme.node_fontname,
14✔
90
      node_fontsize: theme.node_fontsize,
14✔
91
      node_fontcolor: theme.node_fontcolor
14✔
92
    }
93
    |> Enum.reject(fn {_k, v} -> is_nil(v) end)
182✔
94
    |> Map.new()
14✔
95
  end
96

97
  # ============================================================================
98
  # Node styling
99
  # ============================================================================
100

101
  defp node_attributes_fn(_system, theme) do
102
    fn _id, data ->
14✔
103
      type = Map.get(data, :type, :generic)
22✔
104

105
      shape = Map.get(data, :shape) || Theme.shape(theme, type)
22✔
106
      color = Map.get(data, :fillcolor) || Theme.color(theme, type)
22✔
107
      fontcolor = Map.get(data, :fontcolor) || theme.node_fontcolor
22✔
108
      style = Map.get(data, :style, "filled")
22✔
109

110
      attrs = [
22✔
111
        {:shape, shape},
112
        {:fillcolor, color},
113
        {:fontcolor, fontcolor},
114
        {:style, style}
115
      ]
116

117
      attrs =
22✔
NEW
118
        if penwidth = data[:penwidth] do
×
119
          [{:penwidth, penwidth} | attrs]
120
        else
121
          attrs
22✔
122
        end
123

124
      if desc = data[:description] do
22✔
125
        [{:tooltip, desc} | attrs]
126
      else
127
        attrs
22✔
128
      end
129
    end
130
  end
131

132
  defp node_label(_id, data) do
133
    Map.get(data, :name, "")
22✔
134
  end
135

136
  # ============================================================================
137
  # Edge styling
138
  # ============================================================================
139

140
  defp edge_attributes_fn(system) do
141
    fn _from, to, edge_id, _weight ->
14✔
142
      meta = Map.get(system.edge_meta, edge_id, %{})
3✔
143

144
      base =
3✔
145
        case meta[:type] do
146
          :dataflow ->
×
147
            [{:color, "#6366f1"}, {:penwidth, 1.5}, {:style, "dashed"}]
148

149
          _ ->
3✔
150
            [{:color, "#64748b"}, {:penwidth, 1.0}]
151
        end
152

153
      base = if protocol = meta[:protocol], do: [{:label, to_string(protocol)} | base], else: base
3✔
154

155
      # Smart headport: databases look best when entered from the top.
156
      target_data = Map.get(system.graph.nodes, to, %{})
3✔
157

158
      base =
3✔
159
        if target_data[:type] == :database and is_nil(meta[:headport]) do
3✔
160
          [{:headport, "n"} | base]
161
        else
162
          base
1✔
163
        end
164

165
      # Allow user overrides
166
      base = if port = meta[:headport], do: [{:headport, port} | base], else: base
3✔
167
      base = if port = meta[:tailport], do: [{:tailport, port} | base], else: base
3✔
168

169
      base
3✔
170
    end
171
  end
172

173
  defp edge_label(weight) when is_number(weight) do
174
    to_string(weight)
3✔
175
  end
176

177
  defp edge_label(_) do
×
178
    ""
179
  end
180
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc