Decision Tree Viz

A top-down decision tree. Internal nodes render as rounded rectangles holding a condition ("x < 5?", "feature == cat"); leaves render as pills holding an outcome label ("buy", 42). Each internal node's outgoing edges are labeled "yes" (left) and "no" (right) by default. A highlightPath prop traces the active root-to-leaf decision in the accent color — ideal for narrating ML decision trees, expression trees, or game-tree search.

yesyesyesnononox < 15?x < 10?x < 5?NNYNdt-_R_31qnpfivabrb_
Customize
Shape
3
Highlight
none

Installation

npx shadcn@latest add https://craftbits.dev/r/decision-tree-viz.json

Usage

import { DecisionTreeViz, type DecisionTreeNode } from "@craft-bits/core";
 
const tree: DecisionTreeNode = {
  condition: "income > 80k?",
  yes: {
    condition: "stay > 5y?",
    yes: {
      condition: "price < 500k?",
      yes: { outcome: "buy" },
      no: { outcome: "rent" },
    },
    no: { outcome: "rent" },
  },
  no: { outcome: "rent" },
};
 
<DecisionTreeViz tree={tree} highlightPath={["yes", "no"]} />

Numeric outcomes (expression tree style):

<DecisionTreeViz
  tree={{
    condition: "x > 0?",
    yes: {
      condition: "x > 10?",
      yes: { outcome: 100 },
      no: { outcome: 10 },
    },
    no: { outcome: 0 },
  }}
  highlightPath={["yes", "yes"]}
/>

Understanding the component

  1. Recursive shape, two-pass layout. The DecisionTreeNode type is recursive — condition + yes / no for internals, outcome for leaves. The renderer first walks DFS to assign each leaf a horizontal slot, then walks top-down placing each internal at the midpoint of its children's centroids. This handles unbalanced trees, single-branch chains, and deep recursion without overlap.
  2. Two node shapes. Internals are rounded rectangles (rx = 6); leaves are pills (rx = h / 2). The shape difference encodes the algorithmic role at a glance — branching question vs. terminal answer.
  3. Highlight path is an array of decisions. highlightPath={["yes", "no", "yes"]} is the sequence of branches taken from the root. The component resolves which nodes + edges lie on that path and gives them the accent treatment; everything off-path drops to ~65% opacity so the active trace pops.
  4. Edge labels live in tiny pills. Each labeled edge has a var(--cb-bg)-filled rounded rect behind its text so the yes / no glyph reads cleanly when it crosses a line. Disable with showLabels={false} for dense or numerically-labeled trees.
  5. Reduced motion. usePrefersReducedMotion() short-circuits every transition to { duration: 0 } — node entries, edge draws, and path-highlight transitions all snap into place.

Props

PropTypeDefaultDescription
treeDecisionTreeNoderequiredRecursive tree definition. Internal nodes use condition + yes / no; leaves use outcome.
highlightPathreadonly ("yes" | "no")[][]Sequence of decisions traversed from root. On-path nodes + edges get the accent treatment.
showLabelsbooleantrueRender the yes / no glyph on each outgoing edge.
compactbooleanfalseSqueeze padding ~15% for embedded usage.
classNamestringMerged onto the outer <svg>.

Accessibility

  • The outer <svg> is role="img". When a highlightPath is supplied, the aria-label flattens the decisions to a screen-readable trail ("Decision tree path: income > 80k? -> yes; stay > 5y? -> no; outcome: rent."); without a path it reads "Decision tree visualization.".
  • Color is never the only signal — every internal node renders its condition string, every leaf renders its outcome string, and every labeled edge carries its yes / no glyph. The accent treatment is layered on top of those labels, not in place of them.
  • Motion respects prefers-reduced-motion: node entries, edge draws, and the path-highlight transition all collapse to instant when the user has opted out.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/viz/DecisionTreeViz.tsx). The original was a backtracking-lesson widget with six explicit node states and a trackHex accent prop. The library version reframes the abstraction around the user-facing concept — internal nodes (condition) and leaves (outcome), with a single highlightPath to drive emphasis — so the same component reads cleanly for ML, expression, and game-tree contexts.