• 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

72.73
/lib/choreo/theme.ex
1
defmodule Choreo.Theme do
2
  @moduledoc """
3
  Visual themes for `Choreo` architecture diagrams.
4

5
  A theme controls node shapes, colours, fonts, edge styling, graph layout,
6
  and cluster defaults. Use `custom/1` to build your own, or use the built-in
7
  atom shortcuts (`:default`, `:dark`, `:minimal`).
8

9
  ## Built-in themes
10

11
      # Warm, top-down layout with type-coloured nodes (default)
12
      Choreo.to_dot(system, theme: :default)
13

14
      # Dark background with neon accents
15
      Choreo.to_dot(system, theme: :dark)
16

17
      # Wireframe look — monochrome, no fills
18
      Choreo.to_dot(system, theme: :minimal)
19

20
  ## Custom themes
21

22
      theme =
23
        Choreo.Theme.custom(
24
          name: "brand",
25
          colors: [database: "#1e3a8a", service: "#047857"],
26
          graph_bgcolor: "#f8fafc",
27
          graph_rankdir: :lr
28
        )
29

30
      Choreo.to_dot(system, theme: theme)
31

32
  ## Per-type overrides
33

34
  The `shapes` and `colors` maps accept any node `:type` as a key. Missing
35
  types fall back to the built-in defaults (`:box` shape, `"#9ca3af"` colour).
36

37
  ## Cluster theming
38

39
  You can set default cluster styling so every `add_cluster/3` inherits a
40
  look unless it provides its own `:fillcolor`, `:style`, or `:color`.
41

42
      Choreo.Theme.custom(
43
        cluster_fillcolor: "#e2e8f0",
44
        cluster_style: :rounded,
45
        cluster_color: "#64748b"
46
      )
47
  """
48

49
  alias __MODULE__
50

51
  @type t :: %__MODULE__{
52
          name: atom() | String.t(),
53
          shapes: %{atom() => Yog.Render.DOT.node_shape()},
54
          colors: %{atom() => String.t()},
55
          node_fontname: String.t(),
56
          node_fontsize: integer(),
57
          node_fontcolor: String.t(),
58
          edge_color: String.t(),
59
          edge_fontname: String.t(),
60
          edge_fontsize: integer(),
61
          edge_penwidth: float(),
62
          graph_rankdir: Yog.Render.DOT.rank_dir(),
63
          graph_splines: Yog.Render.DOT.splines(),
64
          graph_bgcolor: String.t() | nil,
65
          graph_nodesep: float(),
66
          graph_ranksep: float(),
67
          cluster_style: Yog.Render.DOT.style() | nil,
68
          cluster_fillcolor: String.t() | nil,
69
          cluster_color: String.t() | nil
70
        }
71

72
  @default_shapes %{
73
    database: :cylinder,
74
    cache: :octagon,
75
    service: :box3d,
76
    network: :cloud,
77
    user: :doublecircle,
78
    load_balancer: :hexagon,
79
    queue: :component,
80
    storage: :tab,
81
    generic: :box
82
  }
83

84
  @default_colors %{
85
    database: "#3b82f6",
86
    cache: "#f59e0b",
87
    service: "#10b981",
88
    network: "#6366f1",
89
    user: "#ef4444",
90
    load_balancer: "#8b5cf6",
91
    queue: "#ec4899",
92
    storage: "#14b8a6",
93
    generic: "#9ca3af"
94
  }
95

96
  defstruct [
97
    :name,
98
    shapes: %{},
99
    colors: %{},
100
    node_fontname: "Helvetica",
101
    node_fontsize: 12,
102
    node_fontcolor: "white",
103
    edge_color: "#64748b",
104
    edge_fontname: "Helvetica",
105
    edge_fontsize: 10,
106
    edge_penwidth: 1.0,
107
    graph_rankdir: :tb,
108
    graph_splines: :spline,
109
    graph_bgcolor: nil,
110
    graph_nodesep: 0.6,
111
    graph_ranksep: 1.2,
112
    cluster_style: nil,
113
    cluster_fillcolor: nil,
114
    cluster_color: nil
115
  ]
116

117
  @doc """
118
  Returns the default shape palette.
119

120
  ## Examples
121

122
      iex> shapes = Choreo.Theme.default_shapes()
123
      iex> shapes[:database]
124
      :cylinder
125
      iex> shapes[:service]
126
      :box3d
127
  """
128
  @spec default_shapes() :: %{atom() => Yog.Render.DOT.node_shape()}
129
  def default_shapes, do: @default_shapes
1✔
130

131
  @doc """
132
  Returns the default colour palette.
133

134
  ## Examples
135

136
      iex> colors = Choreo.Theme.default_colors()
137
      iex> colors[:database]
138
      "#3b82f6"
139
      iex> colors[:service]
140
      "#10b981"
141
  """
142
  @spec default_colors() :: %{atom() => String.t()}
143
  def default_colors, do: @default_colors
1✔
144

145
  @doc """
146
  Builds a custom theme from keyword options.
147

148
  Only the fields you specify are overridden; everything else falls back to
149
  the default theme values.
150

151
  ## Examples
152

153
      iex> theme = Choreo.Theme.custom(name: :brand, graph_rankdir: :lr)
154
      iex> theme.name
155
      :brand
156
      iex> theme.graph_rankdir
157
      :lr
158
  """
159
  @spec custom(keyword()) :: t()
160
  def custom(opts \\ []) do
161
    struct!(__MODULE__, [{:name, :custom} | opts])
8✔
162
  end
163

164
  @doc """
165
  The default theme — type-coloured nodes, top-down layout.
166

167
  ## Examples
168

169
      iex> theme = Choreo.Theme.default()
170
      iex> theme.name
171
      :default
172
      iex> theme.graph_rankdir
173
      :tb
174
  """
175
  @spec default() :: t()
176
  def default, do: struct!(__MODULE__, name: :default)
8✔
177

178
  @doc """
179
  Dark theme — dark background, neon accents.
180

181
  ## Examples
182

183
      iex> theme = Choreo.Theme.dark()
184
      iex> theme.name
185
      :dark
186
      iex> theme.graph_bgcolor
187
      "#0f172a"
188
  """
189
  @spec dark() :: t()
190
  def dark do
191
    struct!(__MODULE__,
2✔
192
      name: :dark,
193
      graph_bgcolor: "#0f172a",
194
      edge_color: "#94a3b8",
195
      node_fontname: "Helvetica",
196
      node_fontcolor: "white"
197
    )
198
  end
199

200
  @doc """
201
  Minimal theme — monochrome wireframe, thin edges, no fills.
202

203
  ## Examples
204

205
      iex> theme = Choreo.Theme.minimal()
206
      iex> theme.name
207
      :minimal
208
      iex> theme.colors[:database]
209
      "#ffffff"
210
      iex> theme.shapes[:service]
211
      :box
212
  """
213
  @spec minimal() :: t()
214
  def minimal do
215
    struct!(__MODULE__,
3✔
216
      name: :minimal,
217
      colors: %{
218
        database: "#ffffff",
219
        cache: "#ffffff",
220
        service: "#ffffff",
221
        network: "#ffffff",
222
        user: "#ffffff",
223
        load_balancer: "#ffffff",
224
        queue: "#ffffff",
225
        storage: "#ffffff",
226
        generic: "#ffffff"
227
      },
228
      shapes: %{
229
        database: :box,
230
        cache: :box,
231
        service: :box,
232
        network: :box,
233
        user: :box,
234
        load_balancer: :box,
235
        queue: :box,
236
        storage: :box,
237
        generic: :box
238
      },
239
      node_fontcolor: "black",
240
      edge_color: "#334155",
241
      edge_penwidth: 0.8
242
    )
243
  end
244

245
  @doc """
246
  Warm theme — sunset palette, warm background.
247
  """
248
  @spec warm() :: t()
249
  def warm do
NEW
250
    struct!(__MODULE__,
×
251
      name: :warm,
252
      colors: %{
253
        database: "#f43f5e",
254
        cache: "#f97316",
255
        service: "#fbbf24",
256
        network: "#ec4899",
257
        user: "#ef4444",
258
        load_balancer: "#ea580c",
259
        queue: "#dc2626",
260
        storage: "#db2777",
261
        generic: "#78716c"
262
      },
263
      graph_bgcolor: "#fef2f2",
264
      edge_color: "#78716c",
265
      node_fontname: "Helvetica",
266
      node_fontcolor: "white"
267
    )
268
  end
269

270
  @doc """
271
  Forest theme — lush green palette, earth tones.
272
  """
273
  @spec forest() :: t()
274
  def forest do
NEW
275
    struct!(__MODULE__,
×
276
      name: :forest,
277
      colors: %{
278
        database: "#15803d",
279
        cache: "#166534",
280
        service: "#65a30d",
281
        network: "#14b8a6",
282
        user: "#84cc16",
283
        load_balancer: "#047857",
284
        queue: "#4d7c0f",
285
        storage: "#0f766e",
286
        generic: "#4b5563"
287
      },
288
      graph_bgcolor: "#f0fdf4",
289
      edge_color: "#4b5563",
290
      node_fontname: "Helvetica",
291
      node_fontcolor: "white"
292
    )
293
  end
294

295
  @doc """
296
  Ocean theme — deep blue palette, cool background.
297
  """
298
  @spec ocean() :: t()
299
  def ocean do
NEW
300
    struct!(__MODULE__,
×
301
      name: :ocean,
302
      colors: %{
303
        database: "#1d4ed8",
304
        cache: "#0369a1",
305
        service: "#0891b2",
306
        network: "#2563eb",
307
        user: "#0284c7",
308
        load_balancer: "#0e7490",
309
        queue: "#1e3a8a",
310
        storage: "#008080",
311
        generic: "#64748b"
312
      },
313
      graph_bgcolor: "#f0f9ff",
314
      edge_color: "#64748b",
315
      node_fontname: "Helvetica",
316
      node_fontcolor: "white"
317
    )
318
  end
319

320
  @doc """
321
  Resolves the effective shape for a node type, falling back to defaults.
322

323
  ## Examples
324

325
      iex> theme = Choreo.Theme.custom(shapes: %{database: :box})
326
      iex> Choreo.Theme.shape(theme, :database)
327
      :box
328
      iex> Choreo.Theme.shape(theme, :service)
329
      :box3d
330
  """
331
  @spec shape(t(), atom()) :: Yog.Render.DOT.node_shape()
332
  def shape(%Theme{shapes: shapes}, type) do
333
    Map.get(shapes, type) || Map.get(@default_shapes, type, :box)
24✔
334
  end
335

336
  @doc """
337
  Resolves the effective colour for a node type, falling back to defaults.
338

339
  ## Examples
340

341
      iex> theme = Choreo.Theme.custom(colors: %{database: "#ff0000"})
342
      iex> Choreo.Theme.color(theme, :database)
343
      "#ff0000"
344
      iex> Choreo.Theme.color(theme, :service)
345
      "#10b981"
346
  """
347
  @spec color(t(), atom()) :: String.t()
348
  def color(%Theme{colors: colors}, type) do
349
    Map.get(colors, type) || Map.get(@default_colors, type, "#9ca3af")
24✔
350
  end
351
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