Choreo MindMap: Concept Maps, Root Cause Maps, and Graph Lenses
Mix.install([
# {:choreo, "~> 0.11.0"},
{:choreo, path: Path.expand("~/repos/elixir/choreo"), force: true},
{:kino_vizjs, "~> 0.9.0"}
])
Why this Livebook exists
Choreo.MindMap models structured thinking as code. Use it for:
- brainstorming and knowledge maps;
- product planning and documentation outlines;
- incident/root-cause analysis;
- validated idea hierarchies that are diffable, testable, and renderable in multiple ways.
Mind maps are intentionally simple:
- one root concept;
- branch edges for hierarchy;
- optional associate edges for cross-links;
- analysis functions for depth, breadth, leaves, paths, orphans, cycles, and validation.
Mermaid syntax options
MindMap.to_mermaid/2 supports three views:
| Syntax | Best for | Notes |
|---|---|---|
:flowchart |
Full-fidelity Choreo rendering | Styled nodes, cross-links, themes, highlights |
:mindmap |
Native Mermaid hierarchy | Compact hierarchy; branch edges only |
:ishikawa |
Root-cause/cause-and-effect maps | Mermaid 11.12.3+; branch edges only |
Choreo.Lab.Siren and Choreo.Lab.Sketch render Mermaid strings interactively in Livebook. Kino.VizJS renders Graphviz DOT.
alias Choreo.MindMap
alias Choreo.MindMap.Analysis
alias Choreo.View
mermaid_source = fn source ->
fence = String.duplicate("`", 3)
Kino.Markdown.new(fence <> "\n" <> source <> "\n" <> fence)
end
render_mind_map = fn map, opts ->
syntax = Keyword.get(opts, :syntax, :flowchart)
mermaid = MindMap.to_mermaid(map, opts)
tabs = [
Siren: Choreo.Lab.Siren.new(mermaid),
Graphviz: Kino.VizJS.render(MindMap.to_dot(map), height: Keyword.get(opts, :height, "800px")),
Sketch: Choreo.Lab.Sketch.new(mermaid),
Source: mermaid_source.(mermaid)
]
title = "#{syntax}"
Kino.Layout.tabs([{title, Kino.Layout.tabs(tabs)}])
end
1. Legend: Node Types and Edges
A mind map has four node types:
| Type | Meaning |
|---|---|
:root |
central idea or problem |
:topic |
major branch |
:subtopic |
nested idea |
:note |
annotation/detail |
legend =
MindMap.new()
|> MindMap.set_root(:root, label: "Root")
|> MindMap.add_topic(:topic, label: "Topic")
|> MindMap.add_subtopic(:subtopic, label: "Subtopic")
|> MindMap.add_note(:note, label: "Note")
|> MindMap.branch(:root, :topic)
|> MindMap.branch(:topic, :subtopic)
|> MindMap.branch(:subtopic, :note)
Kino.Layout.tabs(
Flowchart: Choreo.Lab.Siren.new(MindMap.to_mermaid(legend, syntax: :flowchart, direction: :lr)),
Mindmap: Choreo.Lab.Siren.new(MindMap.to_mermaid(legend, syntax: :mindmap)),
Ishikawa: Choreo.Lab.Siren.new(MindMap.to_mermaid(legend, syntax: :ishikawa)),
Graphviz: Kino.VizJS.render(
MindMap.to_dot(legend, theme: Choreo.MindMap.theme(:default, graph_rankdir: :lr))
)
)
2. Brainstorming: Talk Outline
This map is best viewed as a native Mermaid mindmap: it is a clean hierarchy and the central goal is idea organization.
talk =
MindMap.new()
|> MindMap.set_root(:talk, label: "Building\nResilient\nElixir\nSystems")
|> MindMap.add_topic(:supervision, label: "Supervision\nTrees")
|> MindMap.add_topic(:patterns, label: "Fault-Tolerance\nPatterns")
|> MindMap.add_topic(:observability, label: "Observability")
|> MindMap.add_subtopic(:one_for_one, label: "One-for-One")
|> MindMap.add_subtopic(:one_for_all, label: "One-for-All")
|> MindMap.add_subtopic(:circuit_breaker, label: "Circuit Breaker")
|> MindMap.add_subtopic(:bulkhead, label: "Bulkhead")
|> MindMap.add_subtopic(:telemetry, label: "Telemetry")
|> MindMap.add_subtopic(:tracing, label: "OpenTelemetry")
|> MindMap.add_note(:demo, label: "Live demo at 15 min")
|> MindMap.add_note(:story, label: "Tell outage story")
|> MindMap.branch(:talk, :supervision)
|> MindMap.branch(:talk, :patterns)
|> MindMap.branch(:talk, :observability)
|> MindMap.branch(:supervision, :one_for_one)
|> MindMap.branch(:supervision, :one_for_all)
|> MindMap.branch(:patterns, :circuit_breaker)
|> MindMap.branch(:patterns, :bulkhead)
|> MindMap.branch(:observability, :telemetry)
|> MindMap.branch(:observability, :tracing)
|> MindMap.branch(:circuit_breaker, :demo)
|> MindMap.branch(:bulkhead, :story)
|> MindMap.associate(:circuit_breaker, :telemetry, label: "improves")
Kino.Layout.tabs(
Mindmap: Choreo.Lab.Siren.new(MindMap.to_mermaid(talk, syntax: :mindmap)),
Flowchart: Choreo.Lab.Siren.new(MindMap.to_mermaid(talk, syntax: :flowchart)),
Graphviz: Kino.VizJS.render(MindMap.to_dot(talk), height: "850px")
)
The native :mindmap view omits the associate/4 cross-link. Use :flowchart when cross-links matter.
%{
depth: Analysis.depth(talk),
leaves: Analysis.breadth(talk),
max_width: Analysis.max_width(talk),
composition: Analysis.type_frequencies(talk),
validation: Analysis.validate(talk)
}
Root-to-leaf paths become complete narrative threads:
Analysis.paths(talk)
|> Enum.map(fn path -> Enum.join(path, " → ") end)
3. Knowledge Map: Flowchart vs Native Mindmap
A knowledge map often needs both hierarchy and cross-links. Here, CAP theorem, replication, and sharding are related beyond the tree.
domain =
MindMap.new()
|> MindMap.set_root(:dist_sys, label: "Distributed\nSystems")
|> MindMap.add_topic(:consistency, label: "Consistency")
|> MindMap.add_topic(:availability, label: "Availability")
|> MindMap.add_topic(:partitioning, label: "Partitioning")
|> MindMap.add_subtopic(:cap, label: "CAP Theorem")
|> MindMap.add_subtopic(:paxos, label: "Paxos")
|> MindMap.add_subtopic(:raft, label: "Raft")
|> MindMap.add_subtopic(:sharding, label: "Sharding")
|> MindMap.add_subtopic(:replication, label: "Replication")
|> MindMap.add_note(:brewer, label: "Eric Brewer, 2000")
|> MindMap.branch(:dist_sys, :consistency)
|> MindMap.branch(:dist_sys, :availability)
|> MindMap.branch(:dist_sys, :partitioning)
|> MindMap.branch(:consistency, :cap)
|> MindMap.branch(:consistency, :paxos)
|> MindMap.branch(:consistency, :raft)
|> MindMap.branch(:partitioning, :sharding)
|> MindMap.branch(:partitioning, :replication)
|> MindMap.branch(:cap, :brewer)
|> MindMap.associate(:cap, :sharding, label: "influences")
|> MindMap.associate(:raft, :replication, label: "uses")
Kino.Layout.tabs(
"Flowchart — full fidelity": Choreo.Lab.Siren.new(MindMap.to_mermaid(domain, syntax: :flowchart)),
"Mindmap — hierarchy only": Choreo.Lab.Siren.new(MindMap.to_mermaid(domain, syntax: :mindmap)),
Graphviz: Kino.VizJS.render(MindMap.to_dot(domain), height: "750px", width: "100%")
)
Analysis.validate(domain)
|> case do
[] -> Kino.Markdown.new("✅ **Knowledge map is structurally sound.**")
issues -> issues |> inspect(pretty: true) |> Kino.Text.new()
end
4. Root Cause Analysis with Mermaid Ishikawa
Ishikawa diagrams are cause-and-effect diagrams. In Choreo.MindMap, the root is the effect/problem and branch children are causes.
This makes MindMap useful for incident reviews:
incident =
MindMap.new()
|> MindMap.set_root(:checkout_latency, label: "Checkout Latency")
|> MindMap.add_topic(:people, label: "People")
|> MindMap.add_topic(:process, label: "Process")
|> MindMap.add_topic(:platform, label: "Platform")
|> MindMap.add_topic(:data, label: "Data")
|> MindMap.add_topic(:environment, label: "Environment")
|> MindMap.add_subtopic(:missing_runbook, label: "Missing Runbook")
|> MindMap.add_subtopic(:unclear_ownership, label: "Unclear Ownership")
|> MindMap.add_subtopic(:manual_release, label: "Manual Release")
|> MindMap.add_subtopic(:no_canary, label: "No Canary")
|> MindMap.add_subtopic(:slow_queries, label: "Slow Queries")
|> MindMap.add_subtopic(:missing_index, label: "Missing Index")
|> MindMap.add_subtopic(:cold_cache, label: "Cold Cache")
|> MindMap.add_subtopic(:regional_spike, label: "Regional Traffic Spike")
|> MindMap.add_note(:action_runbook, label: "Action: write checkout runbook")
|> MindMap.add_note(:action_index, label: "Action: add order_items index")
|> MindMap.branch(:checkout_latency, :people)
|> MindMap.branch(:checkout_latency, :process)
|> MindMap.branch(:checkout_latency, :platform)
|> MindMap.branch(:checkout_latency, :data)
|> MindMap.branch(:checkout_latency, :environment)
|> MindMap.branch(:people, :unclear_ownership)
|> MindMap.branch(:people, :missing_runbook)
|> MindMap.branch(:process, :manual_release)
|> MindMap.branch(:process, :no_canary)
|> MindMap.branch(:platform, :cold_cache)
|> MindMap.branch(:data, :slow_queries)
|> MindMap.branch(:slow_queries, :missing_index)
|> MindMap.branch(:environment, :regional_spike)
|> MindMap.branch(:missing_runbook, :action_runbook)
|> MindMap.branch(:missing_index, :action_index)
|> MindMap.associate(:manual_release, :cold_cache, label: "amplified")
ishikawa = MindMap.to_mermaid(incident, syntax: :ishikawa)
Kino.Layout.tabs(
Ishikawa: Choreo.Lab.Siren.new(ishikawa),
Flowchart: Choreo.Lab.Siren.new(MindMap.to_mermaid(incident, syntax: :flowchart)),
Graphviz: Kino.VizJS.render(MindMap.to_dot(incident, theme: :warm), height: "950px"),
Source: mermaid_source.(ishikawa)
)
Use :ishikawa when the question is “why did this happen?” Use :flowchart when cross-links and visual styling matter.
%{
validation: Analysis.validate(incident),
root_to_leaf_causes: Analysis.paths(incident),
leaves: Analysis.leaves(incident)
}
5. Product Planning Map
Product planning benefits from the full-fidelity flowchart renderer because cross-links often matter.
roadmap =
MindMap.new()
|> MindMap.set_root(:product, label: "SaaS Platform v2")
|> MindMap.add_topic(:auth, label: "Auth & Security")
|> MindMap.add_topic(:billing, label: "Billing")
|> MindMap.add_topic(:performance, label: "Performance")
|> MindMap.add_topic(:integrations, label: "Integrations")
|> MindMap.add_subtopic(:sso, label: "SSO (SAML)")
|> MindMap.add_subtopic(:mfa, label: "MFA")
|> MindMap.add_subtopic(:stripe, label: "Stripe Checkout")
|> MindMap.add_subtopic(:usage, label: "Usage-Based Pricing")
|> MindMap.add_subtopic(:caching, label: "Redis Caching")
|> MindMap.add_subtopic(:cdn, label: "CDN")
|> MindMap.add_subtopic(:webhooks, label: "Webhooks")
|> MindMap.add_subtopic(:api, label: "REST API v2")
|> MindMap.add_note(:gdpr, label: "GDPR review required")
|> MindMap.add_note(:load_test, label: "Load test before release")
|> MindMap.branch(:product, :auth)
|> MindMap.branch(:product, :billing)
|> MindMap.branch(:product, :performance)
|> MindMap.branch(:product, :integrations)
|> MindMap.branch(:auth, :sso)
|> MindMap.branch(:auth, :mfa)
|> MindMap.branch(:auth, :gdpr)
|> MindMap.branch(:billing, :stripe)
|> MindMap.branch(:billing, :usage)
|> MindMap.branch(:performance, :caching)
|> MindMap.branch(:performance, :cdn)
|> MindMap.branch(:performance, :load_test)
|> MindMap.branch(:integrations, :webhooks)
|> MindMap.branch(:integrations, :api)
|> MindMap.associate(:webhooks, :caching, label: "affects")
|> MindMap.associate(:stripe, :gdpr, label: "data review")
Kino.Layout.tabs(
Flowchart: Choreo.Lab.Siren.new(MindMap.to_mermaid(roadmap, syntax: :flowchart, theme: :ocean)),
Mindmap: Choreo.Lab.Siren.new(MindMap.to_mermaid(roadmap, syntax: :mindmap)),
Graphviz: Kino.VizJS.render(MindMap.to_dot(roadmap, theme: :ocean), height: "550px")
)
Analysis.orphan_nodes(roadmap)
|> case do
[] -> Kino.Markdown.new("✅ **All roadmap items are reachable from the product root.**")
orphans -> Kino.Markdown.new("⚠️ Orphans: `#{inspect(orphans)}`")
end
6. Fixing Broken Maps
The builder lets you iterate quickly, then Analysis.validate/1 catches structural issues.
broken =
MindMap.new()
|> MindMap.set_root(:a, label: "A")
|> MindMap.add_topic(:b, label: "B")
|> MindMap.add_topic(:c, label: "Orphan C")
|> MindMap.add_topic(:d, label: "D")
|> MindMap.branch(:a, :b)
|> MindMap.branch(:b, :d)
|> MindMap.branch(:a, :d)
Analysis.validate(broken)
|> Enum.map(fn {severity, message} -> "* `#{severity}` — #{message}" end)
|> Enum.join("\n")
|> Kino.Markdown.new()
The validator catches:
- missing root;
- cycles;
- orphan nodes;
- nodes with multiple branch parents.
Native :mindmap and :ishikawa renderers reject cycles because Mermaid hierarchy syntaxes cannot represent them safely.
cyclic =
MindMap.new()
|> MindMap.set_root(:a)
|> MindMap.add_topic(:b)
|> MindMap.branch(:a, :b)
|> MindMap.branch(:b, :a)
try do
MindMap.to_mermaid(cyclic, syntax: :ishikawa)
rescue
error in ArgumentError -> error.message
end
7. Graph Lenses with Choreo.View
Choreo.View lets you derive smaller maps without mutating the original.
zoom_by = fn level -> View.zoom(talk, level: level) end
Kino.Layout.tabs(
"Level 1": Choreo.Lab.Siren.new(MindMap.to_mermaid(zoom_by.(1), syntax: :mindmap)),
"Level 2": Choreo.Lab.Siren.new(MindMap.to_mermaid(zoom_by.(2), syntax: :mindmap)),
"Level 3": Choreo.Lab.Siren.new(MindMap.to_mermaid(zoom_by.(3), syntax: :mindmap))
)
Focus on a branch and its immediate neighborhood:
focused = View.focus(talk, :patterns, radius: 1)
Kino.Layout.tabs(
Flowchart: Choreo.Lab.Siren.new(MindMap.to_mermaid(focused, syntax: :flowchart)),
Graphviz: Kino.VizJS.render(MindMap.to_dot(focused), height: "650px")
)
Trace a narrative path between two ideas:
path_view = View.focus_between(talk, :talk, :demo, radius: 1)
Kino.Layout.tabs(
Flowchart: Choreo.Lab.Siren.new(MindMap.to_mermaid(path_view, syntax: :flowchart)),
Graphviz: Kino.VizJS.render(MindMap.to_dot(path_view), height: "650px")
)
Remove notes for a clean stakeholder deck:
clean = View.filter(talk, fn _id, data -> data[:node_type] != :note end)
Choreo.Lab.Siren.new(MindMap.to_mermaid(clean, syntax: :mindmap))
8. Custom Theming
Themes apply to the :flowchart and DOT renderers. Native Mermaid :mindmap and :ishikawa use Mermaid’s own renderer styles.
brand_theme =
Choreo.Theme.custom(
colors: %{
root: "#8b5cf6",
topic: "#3b82f6",
subtopic: "#06b6d4",
note: "#f59e0b"
},
node_fontcolor: "white",
edge_color: "#94a3b8",
graph_bgcolor: "#0f172a"
)
Kino.Layout.tabs(
Flowchart: Choreo.Lab.Siren.new(MindMap.to_mermaid(talk, theme: brand_theme, syntax: :flowchart)),
Graphviz: Kino.VizJS.render(MindMap.to_dot(talk, theme: brand_theme), height: "750px")
)
Summary
| Question | Function / Syntax |
|---|---|
| Full-fidelity styled rendering? |
MindMap.to_mermaid(map, syntax: :flowchart) |
| Compact native hierarchy? |
MindMap.to_mermaid(map, syntax: :mindmap) |
| Cause-and-effect/root-cause view? |
MindMap.to_mermaid(map, syntax: :ishikawa) |
| DOT / Graphviz rendering? |
MindMap.to_dot/2 |
| How deep is the map? |
Analysis.depth/1 |
| How many leaf ideas? |
Analysis.breadth/1 |
| What is the widest level? |
Analysis.max_width/1 |
| Which ideas are terminal? |
Analysis.leaves/1 |
| What are all narrative/root-cause paths? |
Analysis.paths/1 |
| Which ideas are disconnected? |
Analysis.orphan_nodes/1 |
| Is the map structurally sound? |
Analysis.validate/1 |
| Zoom/focus/filter/collapse? |
Choreo.View |
Mind maps as code turn brainstorming, planning, and root-cause analysis into version-controlled artifacts that can be rendered in multiple visual forms and checked before they go stale.