{"spec_id":"tree-decision","library":"muix","language":"javascript","code":"// anyplot.ai\n// tree-decision: Decision Tree Visualization with Probabilities\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-08-26\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n// ANYPLOT_TOKENS has no \"muted\" entry — derive the theme-adaptive muted anchor\n// (default-style-guide.md \"Semantic anchors\") straight from ANYPLOT_THEME.\nconst MUTED = window.ANYPLOT_THEME === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\nconst FONT = \"Inter, system-ui, -apple-system, sans-serif\";\n\n// --- Data: a two-stage product-launch investment decision -------------------\n// decision = square (choose an option), chance = circle (uncertain outcome),\n// terminal = triangle (payoff realized). Each non-root node carries the label\n// and (for chance branches) probability of the edge leading to it.\nconst TREE = [\n  { id: \"D0\", type: \"decision\", parent: null, title: \"Launch Product?\" },\n  { id: \"C1\", type: \"chance\", parent: \"D0\", title: \"Initial Demand\", branchLabel: \"Launch\" },\n  { id: \"T6\", type: \"terminal\", parent: \"D0\", branchLabel: \"Don't Launch\", payoff: 0 },\n  { id: \"D1\", type: \"decision\", parent: \"C1\", title: \"Expand Capacity?\", branchLabel: \"High Demand\", probability: 0.6 },\n  { id: \"D2\", type: \"decision\", parent: \"C1\", title: \"Discount Price?\", branchLabel: \"Low Demand\", probability: 0.4 },\n  { id: \"C2\", type: \"chance\", parent: \"D1\", title: \"Sustained Demand\", branchLabel: \"Expand\" },\n  { id: \"T3\", type: \"terminal\", parent: \"D1\", branchLabel: \"Don't Expand\", payoff: 500 },\n  { id: \"T4\", type: \"terminal\", parent: \"D2\", branchLabel: \"Discount\", payoff: 150 },\n  { id: \"T5\", type: \"terminal\", parent: \"D2\", branchLabel: \"Hold Price\", payoff: 80 },\n  { id: \"T1\", type: \"terminal\", parent: \"C2\", branchLabel: \"High\", probability: 0.7, payoff: 850 },\n  { id: \"T2\", type: \"terminal\", parent: \"C2\", branchLabel: \"Low\", probability: 0.3, payoff: 200 },\n];\n\nconst byId = Object.fromEntries(TREE.map((n) => [n.id, n]));\nconst childrenOf = (id) => TREE.filter((n) => n.parent === id);\n\nfunction depthOf(id) {\n  let depth = 0;\n  let cur = byId[id];\n  while (cur.parent) {\n    depth += 1;\n    cur = byId[cur.parent];\n  }\n  return depth;\n}\nTREE.forEach((n) => {\n  n.depth = depthOf(n.id);\n});\n\n// Leaf nodes get sequential y slots top-to-bottom; every internal node sits at\n// the average y of its children, so the tree self-arranges without overlap.\nlet nextLeafY = 0;\nfunction assignY(id) {\n  const kids = childrenOf(id);\n  if (kids.length === 0) {\n    byId[id].y = nextLeafY;\n    nextLeafY += 1;\n    return byId[id].y;\n  }\n  const ys = kids.map((k) => assignY(k.id));\n  byId[id].y = (Math.min(...ys) + Math.max(...ys)) / 2;\n  return byId[id].y;\n}\nassignY(\"D0\");\n\n// EMV rollback: terminals carry their payoff; chance nodes take the\n// probability-weighted average of children (both branches stay \"live\" — a\n// chance outcome is never rejected); decision nodes take the best child EMV\n// and prune every other immediate branch. Pruning is local to the decision\n// that rejected it — it must not cascade into the chosen branch's own\n// subtree, which has its own (possibly different) optimal choices below it.\nfunction rollback(id) {\n  const node = byId[id];\n  const kids = childrenOf(id);\n  if (node.type === \"terminal\") {\n    node.emv = node.payoff;\n    return node.emv;\n  }\n  const kidEmvs = kids.map((k) => rollback(k.id));\n  if (node.type === \"chance\") {\n    node.emv = kids.reduce((sum, k, i) => sum + k.probability * kidEmvs[i], 0);\n    kids.forEach((k) => {\n      k.pruned = false;\n    });\n    return node.emv;\n  }\n  let bestIdx = 0;\n  kidEmvs.forEach((v, i) => {\n    if (v > kidEmvs[bestIdx]) bestIdx = i;\n  });\n  node.emv = kidEmvs[bestIdx];\n  kids.forEach((k, i) => {\n    k.pruned = i !== bestIdx;\n  });\n  return node.emv;\n}\nrollback(\"D0\");\n\nconst money = (v) => `$${Math.round(v)}`;\nconst maxDepth = Math.max(...TREE.map((n) => n.depth));\nconst minY = Math.min(...TREE.map((n) => n.y));\nconst maxY = Math.max(...TREE.map((n) => n.y));\nconst X_DOMAIN = [-0.35, maxDepth + 0.85];\nconst Y_DOMAIN = [minY - 0.65, maxY + 0.65];\n\nfunction edgeLabel(node) {\n  if (node.probability != null) return `${node.branchLabel} · p=${node.probability.toFixed(2)}`;\n  return node.branchLabel;\n}\n\n// --- Custom marks — drawn on the MUI X coordinate system --------------------\nfunction TreeEdges() {\n  const xs = useXScale();\n  const ys = useYScale();\n  return (\n    <g fontFamily={FONT}>\n      {TREE.filter((n) => n.parent).map((n) => {\n        const parent = byId[n.parent];\n        const x1 = xs(parent.depth);\n        const y1 = ys(parent.y);\n        const x2 = xs(n.depth);\n        const y2 = ys(n.y);\n        const mx = (x1 + x2) / 2;\n        const my = (y1 + y2) / 2;\n        const angle = Math.atan2(y2 - y1, x2 - x1);\n        const nx = -Math.sin(angle);\n        const ny = Math.cos(angle);\n        const lx = mx + nx * 16;\n        const ly = my + ny * 16;\n        return (\n          <g key={n.id}>\n            <line\n              x1={x1}\n              y1={y1}\n              x2={x2}\n              y2={y2}\n              stroke={n.pruned ? MUTED : t.inkSoft}\n              strokeWidth={n.pruned ? 1.8 : 2.4}\n              strokeDasharray={n.pruned ? \"7 5\" : undefined}\n              strokeOpacity={n.pruned ? 0.55 : 0.9}\n            />\n            {n.pruned && (\n              <g stroke={MUTED} strokeWidth={2} strokeOpacity={0.85}>\n                <line x1={mx - 7} y1={my - 7} x2={mx + 7} y2={my + 7} />\n                <line x1={mx - 7} y1={my + 7} x2={mx + 7} y2={my - 7} />\n              </g>\n            )}\n            <text\n              x={lx}\n              y={ly}\n              fontSize={13.5}\n              fill={n.pruned ? MUTED : t.inkSoft}\n              textAnchor=\"middle\"\n              dominantBaseline=\"middle\"\n            >\n              {edgeLabel(n)}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nfunction DecisionMark({ cx, cy, fill }) {\n  const s = 17;\n  return <rect x={cx - s} y={cy - s} width={s * 2} height={s * 2} fill={fill} stroke={t.pageBg} strokeWidth={2.5} />;\n}\n\nfunction ChanceMark({ cx, cy, fill }) {\n  return <circle cx={cx} cy={cy} r={19} fill={fill} stroke={t.pageBg} strokeWidth={2.5} />;\n}\n\nfunction TerminalMark({ cx, cy }) {\n  const w = 15;\n  const h = 15;\n  const points = `${cx - w},${cy - h} ${cx - w},${cy + h} ${cx + w * 1.15},${cy}`;\n  return <polygon points={points} fill={t.elevatedBg} stroke={t.ink} strokeWidth={2} />;\n}\n\nfunction TreeNodes() {\n  const xs = useXScale();\n  const ys = useYScale();\n  return (\n    <g fontFamily={FONT}>\n      {TREE.map((n) => {\n        const cx = xs(n.depth);\n        const cy = ys(n.y);\n        const dimmed = n.pruned;\n        return (\n          <g key={n.id} opacity={dimmed ? 0.5 : 1}>\n            {n.type === \"decision\" && <DecisionMark cx={cx} cy={cy} fill={t.palette[0]} />}\n            {n.type === \"chance\" && <ChanceMark cx={cx} cy={cy} fill={t.palette[1]} />}\n            {n.type === \"terminal\" && <TerminalMark cx={cx} cy={cy} />}\n\n            {n.type !== \"terminal\" && (\n              <>\n                <text x={cx} y={cy - 32} fontSize={15} fontWeight={600} fill={t.ink} textAnchor=\"middle\">\n                  {n.title}\n                </text>\n                <text x={cx} y={cy + 36} fontSize={14} fontWeight={600} fill={t.inkSoft} textAnchor=\"middle\">\n                  EMV {money(n.emv)}\n                </text>\n              </>\n            )}\n            {n.type === \"terminal\" && (\n              <text x={cx + 26} y={cy} fontSize={14.5} fontWeight={600} fill={t.ink} dominantBaseline=\"middle\">\n                {money(n.payoff)}\n              </text>\n            )}\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nfunction Legend() {\n  const items = [\n    { label: \"Decision node\", shape: \"square\", color: t.palette[0] },\n    { label: \"Chance node\", shape: \"circle\", color: t.palette[1] },\n    { label: \"Terminal payoff\", shape: \"triangle\", color: t.ink },\n  ];\n  return (\n    <g transform=\"translate(26, 24)\" fontFamily={FONT}>\n      {items.map((item, i) => (\n        <g key={item.label} transform={`translate(0, ${i * 28})`}>\n          {item.shape === \"square\" && <rect x={0} y={0} width={16} height={16} fill={item.color} />}\n          {item.shape === \"circle\" && <circle cx={8} cy={8} r={8} fill={item.color} />}\n          {item.shape === \"triangle\" && (\n            <polygon points=\"0,0 0,16 15,8\" fill={t.elevatedBg} stroke={item.color} strokeWidth={2} />\n          )}\n          <text x={26} y={13} fontSize={14.5} fill={t.ink}>\n            {item.label}\n          </text>\n        </g>\n      ))}\n      <g transform={`translate(0, ${items.length * 28 + 6})`}>\n        <line x1={0} y1={8} x2={16} y2={8} stroke={MUTED} strokeWidth={2} strokeDasharray=\"5 4\" />\n        <text x={26} y={13} fontSize={14.5} fill={t.ink}>\n          Pruned branch\n        </text>\n      </g>\n    </g>\n  );\n}\n\nconst TITLE_H = 66;\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  return (\n    <div\n      style={{\n        width: W,\n        height: H,\n        background: t.pageBg,\n        fontFamily: FONT,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <div style={{ height: TITLE_H, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}>\n        <span style={{ fontSize: 22, fontWeight: 600, color: t.ink }}>\n          tree-decision · javascript · muix · anyplot.ai\n        </span>\n      </div>\n      <ChartContainer\n        width={W}\n        height={H - TITLE_H}\n        skipAnimation\n        series={[]}\n        margin={{ top: 40, right: 90, bottom: 40, left: 60 }}\n        xAxis={[{ min: X_DOMAIN[0], max: X_DOMAIN[1], scaleType: \"linear\" }]}\n        yAxis={[{ min: Y_DOMAIN[0], max: Y_DOMAIN[1], scaleType: \"linear\" }]}\n      >\n        <TreeEdges />\n        <TreeNodes />\n        <Legend />\n      </ChartContainer>\n    </div>\n  );\n}\n"}