Decision-tree builder on top of Yog.
Choreo.DecisionTree models classification and choice trees where
internal nodes are decisions (tests on features) and leaf nodes are
outcomes (class labels or actions). Paths from root to leaf represent
complete decision chains.
When to use
Use Choreo.DecisionTree when you need to document, validate, or
visualize decision logic — business rules, ML classification trees,
troubleshooting guides, or configuration selectors. It enforces tree
invariants and detects logically inconsistent paths automatically.
The builder enforces tree invariants:
- exactly one root node
- every non-root node has exactly one parent
- no cycles
Node types
:root— the starting decision node (exactly one per tree):decision— internal node testing a feature / attribute:outcome— terminal leaf with a class label or action
Further reading
Quick Start
tree =
Choreo.DecisionTree.new()
|> Choreo.DecisionTree.set_root(:color, feature: "color")
|> Choreo.DecisionTree.add_outcome(:red_stop, label: "Stop")
|> Choreo.DecisionTree.add_outcome(:green_go, label: "Go")
|> Choreo.DecisionTree.branch(:color, :red_stop, "red")
|> Choreo.DecisionTree.branch(:color, :green_go, "green")
Choreo.DecisionTree.Analysis.decide(tree, %{"color" => "red"})
#=> {:ok, [:color, :red_stop], "Stop"}
dot = Choreo.DecisionTree.to_dot(tree)Diagram
Summary
Functions
Adds an internal decision node.
Adds a leaf / outcome node.
Creates a branch from parent to child with a condition label.
Returns all branches as {parent, child, condition} tuples.
Returns the condition label on the branch from parent to child.
Returns all decision (internal) node IDs.
Creates a new empty decision tree.
Returns all node IDs in the tree.
Returns all outcome (leaf) node IDs.
Returns the root node id, or nil if not set.
Sets the root decision node of the tree.
Renders the decision tree to DOT format.
Returns the raw Yog.Graph struct underpinning the tree.
Types
@type t() :: %Choreo.DecisionTree{ edge_meta: %{optional({Yog.node_id(), Yog.node_id()}) => map()}, graph: Yog.graph(), root: Yog.node_id() | nil }
Functions
@spec add_decision(t(), Yog.node_id(), keyword()) :: t()
Adds an internal decision node.
Options
:feature— the attribute being tested:label— display label (defaults to the node id):description— tooltip text
Examples
iex> tree = Choreo.DecisionTree.new()
iex> tree = Choreo.DecisionTree.add_decision(tree, :temp, feature: "temp")
iex> :temp in Choreo.DecisionTree.nodes(tree)
true
iex> Yog.node(tree.graph, :temp).node_type
:decisionDiagram
@spec add_outcome(t(), Yog.node_id(), keyword()) :: t()
Adds a leaf / outcome node.
Options
:class— class label or action name:label— display label (defaults to the node id):description— tooltip text:probability— optional probability / confidence score
Examples
iex> tree = Choreo.DecisionTree.new()
iex> tree = Choreo.DecisionTree.add_outcome(tree, :play, label: "Play", class: "yes")
iex> :play in Choreo.DecisionTree.nodes(tree)
true
iex> Yog.node(tree.graph, :play).node_type
:outcome
iex> Yog.node(tree.graph, :play).class
"yes"Diagram
@spec branch(t(), Yog.node_id(), Yog.node_id(), String.t()) :: t()
Creates a branch from parent to child with a condition label.
Enforces tree invariants:
- child must not already have a parent
- adding the edge must not create a cycle
Returns the updated tree. Raises ArgumentError on invariant violation.
Examples
iex> tree = Choreo.DecisionTree.new()
iex> tree = tree
...> |> Choreo.DecisionTree.set_root(:color, feature: "color")
...> |> Choreo.DecisionTree.add_outcome(:stop)
...> |> Choreo.DecisionTree.branch(:color, :stop, "red")
iex> Yog.has_edge?(tree.graph, :color, :stop)
true
iex> Choreo.DecisionTree.condition(tree, :color, :stop)
"red"Diagram
iex> tree = Choreo.DecisionTree.new()
iex> tree = tree
...> |> Choreo.DecisionTree.set_root(:a, feature: "a")
...> |> Choreo.DecisionTree.add_outcome(:x)
...> |> Choreo.DecisionTree.add_outcome(:y)
...> |> Choreo.DecisionTree.branch(:a, :x, "1")
iex> Choreo.DecisionTree.branch(tree, :y, :x, "2")
** (ArgumentError) Node :x already has a parent
@spec branches(t()) :: [{Yog.node_id(), Yog.node_id(), number()}]
Returns all branches as {parent, child, condition} tuples.
Examples
iex> tree = Choreo.DecisionTree.new()
iex> tree = tree
...> |> Choreo.DecisionTree.set_root(:a, feature: "a")
...> |> Choreo.DecisionTree.add_outcome(:x)
...> |> Choreo.DecisionTree.branch(:a, :x, "yes")
iex> Choreo.DecisionTree.branches(tree)
[{:a, :x, "yes"}]
@spec condition(t(), Yog.node_id(), Yog.node_id()) :: String.t() | nil
Returns the condition label on the branch from parent to child.
Examples
iex> tree = Choreo.DecisionTree.new()
iex> tree = tree
...> |> Choreo.DecisionTree.set_root(:a, feature: "a")
...> |> Choreo.DecisionTree.add_outcome(:x)
...> |> Choreo.DecisionTree.branch(:a, :x, "yes")
iex> Choreo.DecisionTree.condition(tree, :a, :x)
"yes"
iex> Choreo.DecisionTree.condition(tree, :a, :missing)
nil
@spec decisions(t()) :: [Yog.node_id()]
Returns all decision (internal) node IDs.
Examples
iex> tree = Choreo.DecisionTree.new()
iex> tree = tree
...> |> Choreo.DecisionTree.set_root(:a, feature: "a")
...> |> Choreo.DecisionTree.add_decision(:b, feature: "b")
...> |> Choreo.DecisionTree.add_outcome(:x)
...> |> Choreo.DecisionTree.branch(:a, :b, "1")
...> |> Choreo.DecisionTree.branch(:b, :x, "2")
iex> Enum.sort(Choreo.DecisionTree.decisions(tree))
[:a, :b]
@spec new() :: t()
Creates a new empty decision tree.
Examples
iex> tree = Choreo.DecisionTree.new()
iex> Choreo.DecisionTree.nodes(tree)
[]
iex> Choreo.DecisionTree.root(tree)
nil
@spec nodes(t()) :: [Yog.node_id()]
Returns all node IDs in the tree.
Examples
iex> tree = Choreo.DecisionTree.new()
iex> tree = tree
...> |> Choreo.DecisionTree.set_root(:a, feature: "a")
...> |> Choreo.DecisionTree.add_outcome(:x)
iex> Enum.sort(Choreo.DecisionTree.nodes(tree))
[:a, :x]
@spec outcomes(t()) :: [Yog.node_id()]
Returns all outcome (leaf) node IDs.
Examples
iex> tree = Choreo.DecisionTree.new()
iex> tree = tree
...> |> Choreo.DecisionTree.set_root(:a, feature: "a")
...> |> Choreo.DecisionTree.add_decision(:b, feature: "b")
...> |> Choreo.DecisionTree.add_outcome(:x)
...> |> Choreo.DecisionTree.add_outcome(:y)
...> |> Choreo.DecisionTree.branch(:a, :b, "1")
...> |> Choreo.DecisionTree.branch(:b, :x, "2")
...> |> Choreo.DecisionTree.branch(:b, :y, "3")
iex> Enum.sort(Choreo.DecisionTree.outcomes(tree))
[:x, :y]
@spec root(t()) :: Yog.node_id() | nil
Returns the root node id, or nil if not set.
Examples
iex> tree = Choreo.DecisionTree.new()
iex> Choreo.DecisionTree.root(tree)
nil
iex> tree = Choreo.DecisionTree.set_root(tree, :a, feature: "a")
iex> Choreo.DecisionTree.root(tree)
:a
@spec set_root(t(), Yog.node_id(), keyword()) :: t()
Sets the root decision node of the tree.
Can only be called once. Raises ArgumentError if the tree already has a root.
Options
:feature— the attribute being tested (rendered as label):label— override display label:description— tooltip text
Examples
iex> tree = Choreo.DecisionTree.new()
iex> tree = Choreo.DecisionTree.set_root(tree, :weather, feature: "weather")
iex> Choreo.DecisionTree.root(tree)
:weather
iex> Yog.node(tree.graph, :weather).node_type
:root
iex> Yog.node(tree.graph, :weather).feature
"weather"Diagram
iex> tree = Choreo.DecisionTree.new() |> Choreo.DecisionTree.set_root(:a, feature: "x")
iex> Choreo.DecisionTree.set_root(tree, :b, feature: "y")
** (ArgumentError) Tree already has a root
Renders the decision tree to DOT format.
Options
:theme—:default,:dark, or aChoreo.Themestruct
Examples
iex> tree = Choreo.DecisionTree.new()
iex> tree = tree
...> |> Choreo.DecisionTree.set_root(:color, feature: "color")
...> |> Choreo.DecisionTree.add_outcome(:stop, label: "Stop")
...> |> Choreo.DecisionTree.add_outcome(:go, label: "Go")
...> |> Choreo.DecisionTree.branch(:color, :stop, "red")
...> |> Choreo.DecisionTree.branch(:color, :go, "green")
iex> dot = Choreo.DecisionTree.to_dot(tree)
iex> String.contains?(dot, "digraph")
true
iex> String.contains?(dot, "red")
true
iex> String.contains?(dot, "green")
true
Returns the raw Yog.Graph struct underpinning the tree.
Examples
iex> tree = Choreo.DecisionTree.new()
iex> graph = Choreo.DecisionTree.to_graph(tree)
iex> graph.kind
:directed