Powered by AppSignal & Oban Pro

Choreo MindMap: Concept Maps, Root Cause Maps, and Graph Lenses

mind_map_walkthrough.livemd

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.