# `Choreo.DecisionTree`
[🔗](https://github.com/code-shoily/choreo/blob/v0.14.1/lib/choreo/decision_tree.ex#L1)

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

  * [Decision tree learning (Wikipedia)](https://en.wikipedia.org/wiki/Decision_tree_learning)
  * [CART Algorithm](https://en.wikipedia.org/wiki/Predictive_analytics#Classification_and_regression_trees_(CART))
  * [Random forest](https://en.wikipedia.org/wiki/Random_forest)

## 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)
    mermaid = Choreo.DecisionTree.to_mermaid(tree)

## Diagram

<div class="graphviz">
  digraph G {
    graph [rankdir=TB, splines=spline, nodesep=0.7, ranksep=1.2];
    node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"];
    edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0];

    color [label="color", penwidth="2.0", fillcolor="#8b5cf6", shape="diamond"];
    red_stop [label="Stop", fillcolor="#10b981", style="rounded,filled", shape="box"];
    green_go [label="Go", fillcolor="#10b981", style="rounded,filled", shape="box"];

    color -> red_stop [fontcolor="#64748b", color="#64748b", label="red"];
    color -> green_go [fontcolor="#64748b", color="#64748b", label="green"];

    { rank=same; green_go; red_stop; }
  }
</div>

# `t`

```elixir
@type t() :: %Choreo.DecisionTree{
  edge_meta: %{optional({Yog.node_id(), Yog.node_id()}) =&gt; map()},
  graph: Yog.graph(),
  root: Yog.node_id() | nil,
  strict: boolean()
}
```

# `add_decision`

```elixir
@spec add_decision(t(), Yog.node_id(), keyword()) :: t()
```

Adds an internal decision node.

## Options

* `:feature` (`t:String.t/0`)

* `:label` (`t:String.t/0`)

* `:description` (`t:String.t/0`)

* `:shape` (`t:atom/0`)

* `:fillcolor` (`t:String.t/0`)

* `:fontcolor` (`t:String.t/0`)

* `:style` (`t:String.t/0`)

* `:penwidth`

* `:image` (`t:String.t/0`)

## 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
    :decision

## Diagram

<div class="graphviz">
  digraph G {
    graph [rankdir=TB, splines=spline, nodesep=0.7, ranksep=1.2];
    node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"];
    edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0];

    temp [label="temp", fillcolor="#3b82f6", shape="diamond"];
  }
</div>

# `add_outcome`

```elixir
@spec add_outcome(t(), Yog.node_id(), keyword()) :: t()
```

Adds a leaf / outcome node.

## Options

* `:class` (`t:String.t/0`)

* `:probability`

* `:label` (`t:String.t/0`)

* `:description` (`t:String.t/0`)

* `:shape` (`t:atom/0`)

* `:fillcolor` (`t:String.t/0`)

* `:fontcolor` (`t:String.t/0`)

* `:style` (`t:String.t/0`)

* `:penwidth`

* `:image` (`t:String.t/0`)

## 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

<div class="graphviz">
  digraph G {
    graph [rankdir=TB, splines=spline, nodesep=0.7, ranksep=1.2];
    node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"];
    edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0];

    play [label="Play", fillcolor="#10b981", style="rounded,filled", shape="box"];
  }
</div>

# `branch`

```elixir
@spec branch(t(), Yog.node_id(), Yog.node_id(), String.t(), keyword()) :: 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

<div class="graphviz">
  digraph G {
    graph [rankdir=TB, splines=spline, nodesep=0.7, ranksep=1.2];
    node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"];
    edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0];

    stop [label="", fillcolor="#10b981", style="rounded,filled", shape="box"];
    color [label="color", penwidth="2.0", fillcolor="#8b5cf6", shape="diamond"];

    color -> stop [fontcolor="#64748b", color="#64748b", label="red"];
  }
</div>

    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

# `branches`

```elixir
@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"}]

# `condition`

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

# `decisions`

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

# `new`

```elixir
@spec new(keyword()) :: t()
```

Creates a new empty decision tree.

## Options

  * `:strict` — if `true`, `branch/4` raises when parent or child does not
    already exist in the tree. Default `false` (auto-creates missing nodes
    as `:decision`).

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> Choreo.DecisionTree.nodes(tree)
    []
    iex> Choreo.DecisionTree.root(tree)
    nil

# `nodes`

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

# `outcomes`

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

# `root`

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

# `set_root`

```elixir
@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` (`t:String.t/0`)

* `:label` (`t:String.t/0`)

* `:description` (`t:String.t/0`)

* `:shape` (`t:atom/0`)

* `:fillcolor` (`t:String.t/0`)

* `:fontcolor` (`t:String.t/0`)

* `:style` (`t:String.t/0`)

* `:penwidth`

* `:image` (`t:String.t/0`)

## 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

<div class="graphviz">
  digraph G {
    graph [rankdir=TB, splines=spline, nodesep=0.7, ranksep=1.2];
    node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"];
    edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0];

    weather [label="weather", penwidth="2.0", fillcolor="#8b5cf6", shape="diamond"];
  }
</div>

    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 root :a

# `theme`

```elixir
@spec theme(atom(), keyword()) :: Choreo.Theme.t()
```

Returns a theme for `Choreo.DecisionTree`.

Supported theme names: `:default`, `:dark`, `:minimal`, `:warm`, `:forest`, `:ocean`.

## Examples

    iex> theme = Choreo.DecisionTree.theme(:default, graph_rankdir: :lr)
    iex> theme.graph_rankdir
    :lr

# `to_dot`

```elixir
@spec to_dot(t(), keyword()) :: String.t()
```

Renders the decision tree to DOT format.

## Options

  * `:theme` — `:default`, `:dark`, `:minimal`, `:warm`, `:forest`, `:ocean`, or a `Choreo.Theme` struct
  * `:rankdir` — `:tb` (default), `:lr`, etc.
  * `:highlighted_nodes` — list of node IDs to highlight
  * `:highlighted_edges` — list of `{from, to}` tuples to highlight

## 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

# `to_graph`

```elixir
@spec to_graph(t()) :: Yog.graph()
```

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

# `to_mermaid`

```elixir
@spec to_mermaid(t(), keyword()) :: String.t()
```

Renders the decision tree to Mermaid.js flowchart syntax.

## Options

  * `:theme` — `:default`, `:dark`, `:minimal`, `:warm`, `:forest`, `:ocean`, or a `Choreo.Theme` struct
  * `:direction` — `:td` (default), `:lr`, `:bt`, `:rl`
  * `:highlighted_nodes` — list of node IDs to highlight
  * `:highlighted_edges` — list of `{from, to}` tuples to highlight

## 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> mermaid = Choreo.DecisionTree.to_mermaid(tree)
    iex> String.contains?(mermaid, "graph TD")
    true
    iex> String.contains?(mermaid, "red")
    true
    iex> String.contains?(mermaid, "green")
    true

---

*Consult [api-reference.md](api-reference.md) for complete listing*
