How-To: Complete Mermaid.js Guide
Mix.install([
{:yog_ex, "~> 0.98"},
{:kino_vizjs, "~> 0.8.0"}
])
Introduction
Yog.Render.Mermaid and Yog.Multi.Mermaid export graphs to Mermaid.js syntax for embedding in Markdown, GitHub, Notion, and documentation. This guide demonstrates every customization option in one place.
Quick Start
g = Yog.directed()
|> Yog.add_node("Start", nil)
|> Yog.add_node("Process", nil)
|> Yog.add_node("End", nil)
|> Yog.add_edges!([
{"Start", "Process", 1},
{"Process", "End", 1}
])
# Render with defaults
mermaid = Yog.Render.Mermaid.to_mermaid(g)
IO.puts(mermaid)
Kino.Mermaid.new(mermaid)
Default Options & Helpers
default_options/0
opts = Yog.Render.Mermaid.default_options()
IO.inspect(opts.direction, label: "Direction")
IO.inspect(opts.node_shape, label: "Shape")
IO.inspect(opts.highlight_fill, label: "Highlight Fill")
default_options_with_edge_formatter/1
Use this when edge data is not already a string (e.g., integers, structs).
g = Yog.from_edges(:undirected, [{:a, :b, 42}, {:b, :c, 99}])
opts = Yog.Render.Mermaid.default_options_with_edge_formatter(fn weight ->
"#{weight} ms"
end)
mermaid = Yog.Render.Mermaid.to_mermaid(g, opts)
IO.puts(mermaid)
Kino.Mermaid.new(mermaid)
default_options_with/2
Customize both node and edge labels at once.
g = Yog.directed()
|> Yog.add_node(1, %{name: "Alice", role: "Admin"})
|> Yog.add_node(2, %{name: "Bob", role: "User"})
|> Yog.add_edge_ensure(1, 2, 100)
opts = Yog.Render.Mermaid.default_options_with(
node_label: fn _id, data -> "#{data.name} (#{data.role})" end,
edge_label: fn weight -> "#{weight} Mbps" end
)
mermaid = Yog.Render.Mermaid.to_mermaid(g, opts)
IO.puts(mermaid)
Kino.Mermaid.new(mermaid)
Themes
Pre-configured color palettes for different contexts.
g = Yog.Generator.Classic.binary_tree(3)
Default Theme
mermaid = Yog.Render.Mermaid.to_mermaid(g, Yog.Render.Mermaid.theme(:default))
IO.puts(mermaid)
Kino.Mermaid.new(mermaid)
Dark Theme
mermaid = Yog.Render.Mermaid.to_mermaid(g, Yog.Render.Mermaid.theme(:dark))
Kino.Mermaid.new(mermaid)
Minimal Theme
mermaid = Yog.Render.Mermaid.to_mermaid(g, Yog.Render.Mermaid.theme(:minimal))
Kino.Mermaid.new(mermaid)
Presentation Theme
mermaid = Yog.Render.Mermaid.to_mermaid(g, Yog.Render.Mermaid.theme(:presentation))
Kino.Mermaid.new(mermaid)
Directions
Control the layout flow.
g = Yog.from_edges(:directed, [{:a, :b, 1}, {:b, :c, 1}])
Top-Down (:td)
Kino.Mermaid.new(
Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | direction: :td})
)
Left-to-Right (:lr)
Kino.Mermaid.new(
Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | direction: :lr})
)
Bottom-to-Top (:bt)
Kino.Mermaid.new(
Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | direction: :bt})
)
Right-to-Left (:rl)
Kino.Mermaid.new(
Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | direction: :rl})
)
Node Shapes
g = Yog.directed()
|> Yog.add_node(1, "Start")
|> Yog.add_node(2, "Process")
|> Yog.add_node(3, "Decision")
|> Yog.add_node(4, "Database")
|> Yog.add_edges!([{1, 2, 1}, {2, 3, 1}, {3, 4, 1}])
Rounded Rectangle
Kino.Mermaid.new(
Yog.Render.Mermaid.to_mermaid(g, %{
Yog.Render.Mermaid.default_options()
| node_shape: :rounded_rect
})
)
Stadium (Pill)
Kino.Mermaid.new(
Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | node_shape: :stadium})
)
Subroutine
Kino.Mermaid.new(
Yog.Render.Mermaid.to_mermaid(g, %{
Yog.Render.Mermaid.default_options()
| node_shape: :subroutine
})
)
Cylinder (Database)
Kino.Mermaid.new(
Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | node_shape: :cylinder})
)
Circle
Kino.Mermaid.new(
Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | node_shape: :circle})
)
Asymmetric (Flag)
Kino.Mermaid.new(
Yog.Render.Mermaid.to_mermaid(g, %{
Yog.Render.Mermaid.default_options()
| node_shape: :asymmetric
})
)
Rhombus (Decision)
Kino.Mermaid.new(
Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | node_shape: :rhombus})
)
Hexagon
Kino.Mermaid.new(
Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | node_shape: :hexagon})
)
Parallelogram
Kino.Mermaid.new(
Yog.Render.Mermaid.to_mermaid(g, %{
Yog.Render.Mermaid.default_options()
| node_shape: :parallelogram
})
)
Trapezoid
Kino.Mermaid.new(
Yog.Render.Mermaid.to_mermaid(g, %{
Yog.Render.Mermaid.default_options()
| node_shape: :trapezoid
})
)
Per-Element Styling
Per-Node Styling (node_attributes)
g = Yog.directed()
|> Yog.add_node(1, %{name: "Alice", role: "Admin"})
|> Yog.add_node(2, %{name: "Bob", role: "User"})
|> Yog.add_node(3, %{name: "Carol", role: "Guest"})
|> Yog.add_edges!([{1, 2, 1}, {2, 3, 1}])
node_attrs = fn _id, data ->
case data.role do
"Admin" -> [{:fill, "#ef4444"}, {:stroke, "#991b1b"}]
"User" -> [{:fill, "#3b82f6"}, {:stroke, "#1e40af"}]
_ -> [{:fill, "#94a3b8"}, {:stroke, "#475569"}]
end
end
opts = %{
Yog.Render.Mermaid.default_options()
| node_attributes: node_attrs
}
Kino.Mermaid.new(Yog.Render.Mermaid.to_mermaid(g, opts))
Per-Edge Styling (edge_attributes)
g = Yog.from_edges(:directed, [
{:a, :b, %{priority: :high}},
{:b, :c, %{priority: :low}},
{:a, :c, %{priority: :medium}}
])
edge_attrs = fn _from, _to, weight ->
case weight.priority do
:high -> [{:stroke, "#ef4444"}, {:stroke_width, "3px"}]
:medium -> [{:stroke, "#f59e0b"}, {:stroke_width, "2px"}]
:low -> [{:stroke, "#94a3b8"}, {:stroke_width, "1px"}]
end
end
opts = %{
Yog.Render.Mermaid.default_options()
| edge_attributes: edge_attrs
}
Kino.Mermaid.new(Yog.Render.Mermaid.to_mermaid(g, opts))
Subgraphs
Group nodes visually into clusters.
g = Yog.directed()
|> Yog.add_node(:api, "API Gateway")
|> Yog.add_node(:auth, "Auth Service")
|> Yog.add_node(:db, "Database")
|> Yog.add_node(:cache, "Cache")
|> Yog.add_edges!([{:api, :auth, 1}, {:auth, :db, 1}, {:api, :cache, 1}])
opts = %{
Yog.Render.Mermaid.default_options()
| subgraphs: [
%{
name: "backend",
label: "Backend Services",
node_ids: [:auth, :db]
},
%{
name: "infra",
label: "Infrastructure",
node_ids: [:cache]
}
]
}
Kino.Mermaid.new(Yog.Render.Mermaid.to_mermaid(g, opts))
Capstone: System Architecture Diagram
Combine per-node shapes, per-edge styling, and subgraphs to model a real microservices architecture.
system = Yog.directed()
# Users & entry points
|> Yog.add_node(:users, "Mobile / Web")
|> Yog.add_node(:cdn, "CDN")
|> Yog.add_node(:lb, "Load Balancer")
|> Yog.add_node(:api, "API Gateway")
# Domain services
|> Yog.add_node(:auth, "Auth Service")
|> Yog.add_node(:orders, "Order Service")
|> Yog.add_node(:payments, "Payment Service")
|> Yog.add_node(:inventory, "Inventory Service")
|> Yog.add_node(:notifications, "Notification Service")
# Data layer
|> Yog.add_node(:users_db, "Users DB")
|> Yog.add_node(:orders_db, "Orders DB")
|> Yog.add_node(:inventory_db, "Inventory DB")
|> Yog.add_node(:cache, "Redis Cache")
|> Yog.add_node(:queue, "Event Queue")
# Connections with protocol metadata
|> Yog.add_edge_ensure(:users, :cdn, %{protocol: :https, latency: 20})
|> Yog.add_edge_ensure(:cdn, :lb, %{protocol: :https, latency: 10})
|> Yog.add_edge_ensure(:lb, :api, %{protocol: :https, latency: 5})
|> Yog.add_edge_ensure(:api, :auth, %{protocol: :grpc, latency: 15})
|> Yog.add_edge_ensure(:api, :orders, %{protocol: :grpc, latency: 15})
|> Yog.add_edge_ensure(:api, :payments, %{protocol: :grpc, latency: 15})
|> Yog.add_edge_ensure(:orders, :inventory, %{protocol: :grpc, latency: 10})
|> Yog.add_edge_ensure(:orders, :payments, %{protocol: :grpc, latency: 10})
|> Yog.add_edge_ensure(:payments, :queue, %{protocol: :amqp, latency: 5})
|> Yog.add_edge_ensure(:queue, :notifications, %{protocol: :amqp, latency: 5})
|> Yog.add_edge_ensure(:auth, :users_db, %{protocol: :sql, latency: 5})
|> Yog.add_edge_ensure(:auth, :cache, %{protocol: :redis, latency: 2})
|> Yog.add_edge_ensure(:orders, :orders_db, %{protocol: :sql, latency: 5})
|> Yog.add_edge_ensure(:inventory, :inventory_db, %{protocol: :sql, latency: 5})
# Per-node shapes: different shapes for different component types
shape_fn = fn id, _data ->
case id do
:users -> :circle
:cdn -> :hexagon
:lb -> :rhombus
:api -> :asymmetric
n when n in [:auth, :orders, :payments, :inventory, :notifications] -> :rounded_rect
n when n in [:users_db, :orders_db, :inventory_db] -> :cylinder
:cache -> :stadium
:queue -> :subroutine
_ -> :rounded_rect
end
end
# Per-edge colors by protocol
edge_attrs = fn _from, _to, weight ->
case weight.protocol do
:https -> [{:stroke, "#3b82f6"}]
:grpc -> [{:stroke, "#10b981"}]
:sql -> [{:stroke, "#f59e0b"}]
:redis -> [{:stroke, "#ef4444"}]
:amqp -> [{:stroke, "#8b5cf6"}]
_ -> []
end
end
# Edge labels show latency
edge_label = fn weight -> "#{weight.latency} ms" end
opts = %{
Yog.Render.Mermaid.default_options()
| direction: :lr,
node_shape: shape_fn,
node_attributes: fn _id, data ->
case data do
"Auth Service" -> [{:fill, "#dbeafe"}]
"Payment Service" -> [{:fill, "#dcfce7"}]
_ -> []
end
end,
edge_attributes: edge_attrs,
edge_label: edge_label,
subgraphs: [
%{name: "entry", label: "Edge Layer", node_ids: [:users, :cdn, :lb]},
%{name: "services", label: "Services", node_ids: [:api, :auth, :orders, :payments, :inventory, :notifications]},
%{name: "data", label: "Data Layer", node_ids: [:users_db, :orders_db, :inventory_db, :cache, :queue]}
]
}
Kino.Mermaid.new(Yog.Render.Mermaid.to_mermaid(system, opts))
Highlighting
Manual Highlighting
g = Yog.from_edges(:directed, [{:a, :b, 1}, {:b, :c, 1}, {:c, :d, 1}])
opts = %{
Yog.Render.Mermaid.default_options()
| highlighted_nodes: [:a, :b, :c],
highlighted_edges: [{:a, :b}, {:b, :c}]
}
Kino.Mermaid.new(Yog.Render.Mermaid.to_mermaid(g, opts))
path_to_options/2 — Shortest Path Highlighting
g = Yog.Generator.Classic.grid_2d(5, 5)
source = 0
target = 24
{:ok, path} = Yog.Pathfinding.shortest_path(in: g, from: source, to: target)
opts = Yog.Render.Mermaid.path_to_options(path)
Kino.Mermaid.new(Yog.Render.Mermaid.to_mermaid(g, opts))
Algorithm Helper Options
mst_to_options/2 — Minimum Spanning Tree
weighted = Yog.from_edges(:undirected, [
{:a, :b, 4}, {:a, :h, 8}, {:b, :c, 8},
{:c, :d, 7}, {:c, :f, 4}, {:d, :e, 9},
{:e, :f, 10}, {:f, :g, 2}, {:g, :h, 1}
])
{:ok, mst} = Yog.MST.kruskal(in: weighted)
mst_opts = Yog.Render.Mermaid.mst_to_options(mst)
Kino.Mermaid.new(Yog.Render.Mermaid.to_mermaid(weighted, mst_opts))
community_to_options/2 — Community Detection
sbm = Yog.Generator.Random.sbm(10, 2, 0.3, 0.5)
comm = Yog.Community.Louvain.detect(sbm)
comm_opts = Yog.Render.Mermaid.community_to_options(comm)
Kino.Mermaid.new(Yog.Render.Mermaid.to_mermaid(sbm, comm_opts))
cut_to_options/2 — Min-Cut Partitions
flow = Yog.from_edges(:directed, [{:s, :a, 10}, {:s, :b, 10}, {:a, :t, 10}, {:b, :t, 5}])
result = Yog.Flow.MaxFlow.dinic(flow, :s, :t)
min_cut = Yog.Flow.MaxFlow.min_cut(result)
cut_opts = Yog.Render.Mermaid.cut_to_options(min_cut)
Kino.Mermaid.new(Yog.Render.Mermaid.to_mermaid(flow, cut_opts))
matching_to_options/2 — Bipartite Matching
bipartite = Yog.from_edges(:undirected, [
{:a1, :b1, 1}, {:a1, :b2, 1}, {:a2, :b2, 1}, {:a2, :b3, 1}
])
matching = Yog.Matching.hopcroft_karp(bipartite)
match_opts = Yog.Render.Mermaid.matching_to_options(matching)
Kino.Mermaid.new(Yog.Render.Mermaid.to_mermaid(bipartite, match_opts))
Multigraph Mermaid
Yog.Multi.Mermaid mirrors Yog.Render.Mermaid but supports parallel edges.
Basic Multigraph Rendering
alias Yog.Multi
mg = Multi.undirected()
|> Multi.add_node(:london, nil)
|> Multi.add_node(:paris, nil)
{mg, _e1} = Multi.add_edge(mg, :london, :paris, 100) # Flight
{mg, _e2} = Multi.add_edge(mg, :london, :paris, 50) # Train
{mg, e3} = Multi.add_edge(mg, :london, :paris, 300) # Ferry
Kino.Mermaid.new(Yog.Multi.Mermaid.to_mermaid(mg))
Per-Edge Styling with edge_id
Because multigraphs have parallel edges, the edge_attributes callback receives the unique edge_id.
opts = %{
Yog.Multi.Mermaid.default_options()
| edge_attributes: fn _from, _to, edge_id, weight ->
color =
cond do
weight == 50 -> "#10b981" # Train (fastest)
weight == 100 -> "#3b82f6" # Flight
true -> "#f59e0b" # Ferry
end
width = if edge_id == e3, do: "4px", else: "2px"
[{:stroke, color}, {:stroke_width, width}]
end
}
Kino.Mermaid.new(Yog.Multi.Mermaid.to_mermaid(mg, opts))
Subgraphs in Multigraphs
mg = Multi.directed()
|> Multi.add_node(:web, "Web Layer")
|> Multi.add_node(:api, "API Layer")
|> Multi.add_node(:db1, "Primary DB")
|> Multi.add_node(:db2, "Replica DB")
|> (fn g -> {g, _} = Multi.add_edge(g, :web, :api, 1); g end).()
|> (fn g -> {g, _} = Multi.add_edge(g, :api, :db1, 1); g end).()
|> (fn g -> {g, _} = Multi.add_edge(g, :api, :db2, 1); g end).()
opts = %{
Yog.Multi.Mermaid.default_options()
| subgraphs: [
%{
name: "data_layer",
label: "Data Layer",
node_ids: [:db1, :db2]
}
]
}
Kino.Mermaid.new(Yog.Multi.Mermaid.to_mermaid(mg, opts))
Summary
| Capability | Module | Key Function / Option |
|---|---|---|
| Basic export |
Yog.Render.Mermaid |
to_mermaid/2 |
| Basic export (parallel edges) |
Yog.Multi.Mermaid |
to_mermaid/2 |
| Default options | Both |
default_options/0 |
| Custom edge formatter |
Yog.Render.Mermaid |
default_options_with_edge_formatter/1 |
| Custom labels |
Yog.Render.Mermaid |
default_options_with/2 |
| Themes |
Yog.Render.Mermaid |
theme/1 (:default, :dark, :minimal, :presentation) |
| Direction | Both |
direction: (:td, :lr, :bt, :rl) |
| Node shapes | Both |
node_shape: (e.g., :cylinder, :rhombus, :hexagon) |
| Per-node styles | Both |
node_attributes: callback |
| Per-edge styles |
Yog.Render.Mermaid |
edge_attributes: fn f, t, w -> ... |
| Per-edge styles (multi) |
Yog.Multi.Mermaid |
edge_attributes: fn f, t, id, w -> ... |
| Subgraphs | Both |
subgraphs: list |
| Path highlighting |
Yog.Render.Mermaid |
path_to_options/2 |
| MST highlighting |
Yog.Render.Mermaid |
mst_to_options/2 |
| Community colors |
Yog.Render.Mermaid |
community_to_options/2 |
| Min-cut colors |
Yog.Render.Mermaid |
cut_to_options/2 |
| Matching highlight |
Yog.Render.Mermaid |
matching_to_options/2 |
Next, explore Customizing Visualizations for a side-by-side comparison with Graphviz DOT, or Multigraphs & Edge Collapsing for more on parallel-edge workflows.