Powered by AppSignal & Oban Pro

Choreo MindMap: Comprehensive Walkthrough

livebooks/mind_map_walkthrough.livemd

Choreo MindMap: Comprehensive Walkthrough

Section

Mix.install([
  {:choreo, "~> 0.7.1"},
  {:kino_vizjs, "~> 0.8.0"}
])

> Rendering diagrams: This livebook uses Kino.VizJS to render DOT diagrams inline. You can also copy DOT output into PlantText or run dot -Tpng diagram.dot -o diagram.png locally.


What is Choreo.MindMap?

Choreo.MindMap models hierarchical concept maps where a central idea radiates into topics, sub-topics, and notes. Cross-links (associations) between branches are supported for non-hierarchical relationships.

Unlike free-form graphs, mind maps enforce a single-root invariant:

  • Exactly one root — the central concept node
  • Tree-like hierarchy — branch edges form a directed acyclic graph from root outward
  • Associative links — dashed edges without direction for cross-cutting relationships

These invariants mean you can analyze a mind map structurally: measure depth, detect orphans, enumerate root-to-leaf paths, and validate soundness.

Node Types

Type Shape Purpose
root ⭕ double circle (bold) Central concept — exactly one per map
topic 🔵 ellipse Main branch radiating from root
subtopic ⬛ rounded box Nested idea under a topic
note 📝 note shape Annotation or detail
alias Choreo.MindMap
alias Choreo.MindMap.Analysis

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.VizJS.render(
  MindMap.to_dot(legend, theme: Choreo.MindMap.theme(:default, graph_rankdir: :lr))
)

Example 1: Brainstorming a Talk Outline

Map out the structure of a conference talk. The root is the talk title; topics are major sections; subtopics are key points; notes are speaker reminders.

talk =
  MindMap.new()
  |> MindMap.set_root(:talk, label: "Building\nResilient\nElxir\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)
  # Cross-link: circuit breakers improve telemetry signals
  |> MindMap.associate(:circuit_breaker, :telemetry, label: "improves")

Kino.VizJS.render(MindMap.to_dot(talk), height: "800px")

Structural Metrics

IO.puts("Depth: #{Analysis.depth(talk)}")
IO.puts("Breadth (leaves): #{Analysis.breadth(talk)}")
IO.puts("Max width: #{Analysis.max_width(talk)}")
IO.inspect(Analysis.type_frequencies(talk), label: "Composition")

Depth tells you how many levels of detail the map has. Breadth counts terminal ideas. Max width shows the widest level — useful for estimating how many ideas fit on a single slide.

Root-to-Leaf Paths

Analysis.paths(talk)
|> Enum.each(fn path ->
  IO.puts(Enum.join(path, " → "))
end)

Each path is a complete narrative thread from the central concept to a terminal detail. Use these to sanity-check that every section has depth and no section is under-developed.


Example 2: Domain Knowledge Map

Document the key concepts of a software domain. Cross-links show relationships that cut across the hierarchy.

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)
  # Cross-links
  |> MindMap.associate(:cap, :sharding, label: "influences")
  |> MindMap.associate(:raft, :replication, label: "uses")

Kino.VizJS.render(MindMap.to_dot(domain, theme: :dark), height: "600px", width: "100%")

Validation

Analysis.validate(domain)
|> Enum.each(fn {sev, msg} ->
  icon = if sev == :error, do: "❌", else: "⚠️"
  IO.puts("#{icon} #{msg}")
end)

A valid mind map has exactly one root, no cycles in the hierarchy, and all nodes reachable from the root.


Example 3: Product Feature Planning

Plan a product roadmap as a mind map. Topics are release themes; subtopics are epics; notes are risk reminders.

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(:billing, :stripe)
  |> MindMap.branch(:billing, :usage)
  |> MindMap.branch(:performance, :caching)
  |> MindMap.branch(:performance, :cdn)
  |> MindMap.branch(:integrations, :webhooks)
  |> MindMap.branch(:integrations, :api)
  |> MindMap.branch(:auth, :gdpr)
  |> MindMap.branch(:performance, :load_test)
  # Cross-link: webhooks affect caching strategy
  |> MindMap.associate(:webhooks, :caching, label: "affects")

Kino.VizJS.render(MindMap.to_dot(roadmap, theme: :ocean), height: "600px")

Detect Orphans

Analysis.orphan_nodes(roadmap)
|> case do
  [] -> IO.puts("✅ All nodes reachable from root")
  orphans -> IO.puts("⚠️ Orphan nodes: #{inspect(orphans)}")
end

Orphan nodes are ideas that have been added but not connected to the hierarchy. They’re often forgotten requirements.

Type Composition

Analysis.type_frequencies(roadmap)
|> Enum.each(fn {type, count} ->
  IO.puts("#{type}: #{count}")
end)

Example 4: Deep Hierarchical Map

A deeply nested map demonstrates depth and width analysis.

deep =
  MindMap.new()
  |> MindMap.set_root(:science, label: "Science")
  |> MindMap.add_topic(:physics, label: "Physics")
  |> MindMap.add_topic(:biology, label: "Biology")
  |> MindMap.add_subtopic(:quantum, label: "Quantum Mechanics")
  |> MindMap.add_subtopic(:relativity, label: "Relativity")
  |> MindMap.add_subtopic(:genetics, label: "Genetics")
  |> MindMap.add_subtopic(:ecology, label: "Ecology")
  |> MindMap.add_note(:entanglement, label: "Quantum Entanglement")
  |> MindMap.add_note(:dna, label: "DNA Structure")
  |> MindMap.branch(:science, :physics)
  |> MindMap.branch(:science, :biology)
  |> MindMap.branch(:physics, :quantum)
  |> MindMap.branch(:physics, :relativity)
  |> MindMap.branch(:biology, :genetics)
  |> MindMap.branch(:biology, :ecology)
  |> MindMap.branch(:quantum, :entanglement)
  |> MindMap.branch(:genetics, :dna)

IO.puts("Depth: #{Analysis.depth(deep)}")
IO.puts("Max width: #{Analysis.max_width(deep)}")
IO.inspect(Analysis.leaves(deep), label: "Leaves")
Kino.VizJS.render(MindMap.to_dot(deep, theme: :forest), height: "800px", engine: "twopi")

Example 5: Fixing a Broken Map

The builder does not enforce tree invariants at construction time (unlike DecisionTree), so validation is especially useful.

broken =
  MindMap.new()
  |> MindMap.set_root(:a)
  |> MindMap.add_topic(:b)
  |> MindMap.add_topic(:c)
  |> MindMap.add_topic(:d)
  # c is orphaned — never branched from root
  |> MindMap.branch(:a, :b)
  |> MindMap.branch(:b, :d)
  # d has two branch parents (b and a) — unusual but allowed
  |> MindMap.branch(:a, :d)

Analysis.validate(broken)
|> Enum.each(fn {sev, msg} ->
  icon = if sev == :error, do: "❌", else: "⚠️"
  IO.puts("#{icon} #{msg}")
end)

The validator catches:

  • Missing root
  • Cycles in the hierarchy
  • Orphan nodes (not reachable from root)
  • Nodes with multiple branch parents

Cycle Detection

cyclic =
  MindMap.new()
  |> MindMap.set_root(:a)
  |> MindMap.add_topic(:b)
  |> MindMap.add_topic(:c)
  |> MindMap.branch(:a, :b)
  |> MindMap.branch(:b, :c)
  |> MindMap.branch(:c, :b)

Analysis.cyclic?(cyclic)

Cycles in a mind map hierarchy are usually a mistake. They break depth calculation and root-to-leaf path enumeration.


Example 6: Graph Lenses with Choreo.View

Choreo.View lets you look at the same mind map through different focal lengths — zoom out for an overview, focus on a specific branch, or filter out details.

Zoom Levels

Show only the big picture (root + topics):

alias Choreo.View

overview = View.zoom(talk, level: 1)
Kino.VizJS.render(MindMap.to_dot(overview), engine: "circo")

Show root, topics, and subtopics (hide notes):

mid = View.zoom(talk, level: 2)
Kino.VizJS.render(MindMap.to_dot(mid), height: "600px")

Focus on a Branch

Highlight one topic and its neighbourhood:

focused = View.focus(talk, :patterns, radius: 1)
Kino.VizJS.render(MindMap.to_dot(focused), height: "600px")

Focus with a larger radius to see second-degree connections:

wider = View.focus(talk, :supervision, radius: 2)
Kino.VizJS.render(MindMap.to_dot(wider), height: "600px")

Trace a Path

Show only the shortest path between two ideas — useful for tracing a narrative thread:

path_view = View.focus_between(talk, :talk, :demo, radius: 1)
Kino.VizJS.render(MindMap.to_dot(path_view), height: "600px")

Transitive Zoom

When zooming out, hidden intermediate nodes can break the visual connection between remaining concepts. Enable transitive: true to draw virtual edges through collapsed intermediates:

overview = View.zoom(talk, level: 1, transitive: true)
Kino.VizJS.render(MindMap.to_dot(overview), height: "600px")

Collapse — Aggregate Nodes

Merge multiple related nodes into a single aggregate. All edges are rewired automatically:

# Merge all subtopics under "Concurrency" into one billing system node
billing =
  View.collapse(talk, fn id, _data ->
    id in [:circuit_breaker, :bulkhead]
  end, :resilience_patterns,
    label: "Resilience Patterns",
    data: %{node_type: :topic}
  )

Kino.VizJS.render(MindMap.to_dot(billing), height: "600px")

Custom Filtering

Remove all notes for a clean stakeholder deck:

clean = View.filter(talk, fn _id, data -> data[:node_type] != :note end)
Kino.VizJS.render(MindMap.to_dot(clean), height: "600px")

Show only the left half of the map:

left_half = View.filter(talk, fn id, _data -> id in [:talk, :supervision, :patterns, :one_for_one, :one_for_all, :circuit_breaker, :bulkhead, :demo, :story] end)
Kino.VizJS.render(MindMap.to_dot(left_half), height: "600px")

Advanced: Custom Theming

brand_theme =
  Choreo.Theme.custom(
    colors: %{
      root: "#8b5cf6",
      topic: "#3b82f6",
      subtopic: "#06b6d4",
      note: "#f59e0b"
    },
    node_fontcolor: "white",
    edge_color: "#94a3b8",
    graph_bgcolor: "#0f172a"
  )

Kino.VizJS.render(MindMap.to_dot(talk, theme: brand_theme), height: "600px")

Summary

Question Function
“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 have no children?” Analysis.leaves/1
“What are all root-to-leaf paths?” Analysis.paths/1
“Which ideas are disconnected?” Analysis.orphan_nodes/1
“What is the composition?” Analysis.type_frequencies/1
“Is there a cycle?” Analysis.cyclic?/1
“Is the map structurally sound?” Analysis.validate/1
“Zoom out to overview” View.zoom/2
“Focus on a branch” View.focus/3
“Trace a path between two nodes” View.focus_between/4
“Filter nodes” View.filter/3
“Collapse into aggregate” View.collapse/4
“Render to DOT” MindMap.to_dot/2

Mind maps as code mean your brainstorming sessions, documentation, and planning artefacts are version-controlled, diffable, and automatically validated. Every addition is checked for reachability from the root and structural soundness — before it ever becomes a static diagram.