Skip to main content

livebooks/how_to/mermaid_complete_guide.livemd

# How-To: Complete Mermaid.js Guide

```elixir
Mix.install([
  {:yog_ex, path: "/home/mafinar/repos/elixir/yog_ex"},
  {:kino_vizjs, "~> 0.8.0"}
])
```

## Introduction

`Yog.Render.Mermaid` and `Yog.Multi.Mermaid` export graphs to [Mermaid.js](https://mermaid.js.org/) syntax for embedding in Markdown, GitHub, Notion, and documentation. This guide demonstrates every customization option in one place.

---

## 🚀 Quick Start

```elixir
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`

```elixir
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).

```elixir
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.

```elixir
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.

```elixir
g = Yog.Generator.Classic.binary_tree(3)
```

### Default Theme

```elixir
mermaid = Yog.Render.Mermaid.to_mermaid(g, Yog.Render.Mermaid.theme(:default))

IO.puts(mermaid)

Kino.Mermaid.new(mermaid)
```

### Dark Theme

```elixir
mermaid = Yog.Render.Mermaid.to_mermaid(g, Yog.Render.Mermaid.theme(:dark))

Kino.Mermaid.new(mermaid)
```

### Minimal Theme

```elixir
mermaid = Yog.Render.Mermaid.to_mermaid(g, Yog.Render.Mermaid.theme(:minimal))

Kino.Mermaid.new(mermaid)
```

### Presentation Theme

```elixir
mermaid = Yog.Render.Mermaid.to_mermaid(g, Yog.Render.Mermaid.theme(:presentation))

Kino.Mermaid.new(mermaid)
```

---

## 🧭 Directions

Control the layout flow.

```elixir
g = Yog.from_edges(:directed, [{:a, :b, 1}, {:b, :c, 1}])
```

### Top-Down (`:td`)

```elixir
Kino.Mermaid.new(
  Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | direction: :td})
)
```

### Left-to-Right (`:lr`)

```elixir
Kino.Mermaid.new(
  Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | direction: :lr})
)
```

### Bottom-to-Top (`:bt`)

```elixir
Kino.Mermaid.new(
  Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | direction: :bt})
)
```

### Right-to-Left (`:rl`)

```elixir
Kino.Mermaid.new(
  Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | direction: :rl})
)
```

---

## 🔷 Node Shapes

```elixir
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

```elixir
Kino.Mermaid.new(
  Yog.Render.Mermaid.to_mermaid(g, %{
    Yog.Render.Mermaid.default_options()
    | node_shape: :rounded_rect
  })
)
```

### Stadium (Pill)

```elixir
Kino.Mermaid.new(
  Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | node_shape: :stadium})
)
```

### Subroutine

```elixir
Kino.Mermaid.new(
  Yog.Render.Mermaid.to_mermaid(g, %{
    Yog.Render.Mermaid.default_options()
    | node_shape: :subroutine
  })
)
```

### Cylinder (Database)

```elixir
Kino.Mermaid.new(
  Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | node_shape: :cylinder})
)
```

### Circle

```elixir
Kino.Mermaid.new(
  Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | node_shape: :circle})
)
```

### Asymmetric (Flag)

```elixir
Kino.Mermaid.new(
  Yog.Render.Mermaid.to_mermaid(g, %{
    Yog.Render.Mermaid.default_options()
    | node_shape: :asymmetric
  })
)
```

### Rhombus (Decision)

```elixir
Kino.Mermaid.new(
  Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | node_shape: :rhombus})
)
```

### Hexagon

```elixir
Kino.Mermaid.new(
  Yog.Render.Mermaid.to_mermaid(g, %{Yog.Render.Mermaid.default_options() | node_shape: :hexagon})
)
```

### Parallelogram

```elixir
Kino.Mermaid.new(
  Yog.Render.Mermaid.to_mermaid(g, %{
    Yog.Render.Mermaid.default_options()
    | node_shape: :parallelogram
  })
)
```

### Trapezoid

```elixir
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`)

```elixir
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`)

```elixir
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.

```elixir
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.

```elixir
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

```elixir
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

```elixir
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

```elixir
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

```elixir
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

```elixir
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

```elixir
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

```elixir
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`.

```elixir
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

```elixir
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.