{"spec_id":"tree-decision","library":"echarts","language":"javascript","code":"// anyplot.ai\n// tree-decision: Decision Tree Visualization with Probabilities\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\nconst W = window.ANYPLOT_SIZE.width;\nconst H = window.ANYPLOT_SIZE.height;\n\n// --- Data: a two-stage product-launch investment decision -------------------\n// Mirrors the spec's node schema directly (node_id/node_type/parent_id/\n// branch_label/probability/payoff/emv/pruned), values in $ thousands. EMV is\n// rolled back from the terminal payoffs: at each chance node it's the\n// probability-weighted average of its children; at each decision node it's\n// the max EMV among its option children (the losing option is \"pruned\").\nconst records = [\n  { node_id: \"d1\", node_type: \"decision\", parent_id: null, name: \"Launch investment?\", branch_label: null, probability: null, payoff: null, emv: 352, pruned: false },\n  { node_id: \"t_hold\", node_type: \"terminal\", parent_id: \"d1\", name: \"Hold — no investment\", branch_label: \"Don't invest\", probability: null, payoff: 0, emv: null, pruned: true },\n  { node_id: \"c1\", node_type: \"chance\", parent_id: \"d1\", name: \"Market demand\", branch_label: \"Invest ($200k)\", probability: null, payoff: null, emv: 352, pruned: false },\n  { node_id: \"t_low\", node_type: \"terminal\", parent_id: \"c1\", name: \"Low demand\", branch_label: \"Low demand\", probability: 0.4, payoff: -80, emv: null, pruned: false },\n  { node_id: \"d2\", node_type: \"decision\", parent_id: \"c1\", name: \"Expand capacity?\", branch_label: \"High demand\", probability: 0.6, payoff: null, emv: 640, pruned: false },\n  { node_id: \"t_noexp\", node_type: \"terminal\", parent_id: \"d2\", name: \"Hold capacity\", branch_label: \"Don't expand\", probability: null, payoff: 300, emv: null, pruned: true },\n  { node_id: \"c2\", node_type: \"chance\", parent_id: \"d2\", name: \"Demand sustained?\", branch_label: \"Expand (+$150k)\", probability: null, payoff: null, emv: 640, pruned: false },\n  { node_id: \"t_decline\", node_type: \"terminal\", parent_id: \"c2\", name: \"Demand declines\", branch_label: \"Declines\", probability: 0.3, payoff: 150, emv: null, pruned: false },\n  { node_id: \"t_sustain\", node_type: \"terminal\", parent_id: \"c2\", name: \"Demand sustained\", branch_label: \"Sustained\", probability: 0.7, payoff: 850, emv: null, pruned: false },\n];\n\n// --- Layout: fixed left-to-right coordinates as fractions of the mount ------\nconst COL = [0.08, 0.28, 0.48, 0.68, 0.87].map((f) => f * W);\nconst ROW = {\n  d1: 0.69, t_hold: 0.84,\n  c1: 0.54, t_low: 0.68,\n  d2: 0.40, t_noexp: 0.52,\n  c2: 0.28, t_decline: 0.36, t_sustain: 0.20,\n};\nconst COL_OF = { d1: 0, t_hold: 1, c1: 1, t_low: 2, d2: 2, t_noexp: 3, c2: 3, t_decline: 4, t_sustain: 4 };\n\n// --- Visual mapping per node type -------------------------------------------\nconst SYMBOL = { decision: \"rect\", chance: \"circle\", terminal: \"triangle\" };\nconst SIZE = { decision: 52, chance: 52, terminal: [40, 40] };\nconst CATEGORY = { decision: 0, chance: 1, terminal: 2 };\nconst money = (v) => `${v < 0 ? \"-\" : \"\"}$${Math.abs(v)}k`;\n\nconst nodes = records.map((r) => ({\n  id: r.node_id,\n  name: r.name,\n  x: COL[COL_OF[r.node_id]],\n  y: ROW[r.node_id] * H,\n  symbol: SYMBOL[r.node_type],\n  symbolSize: SIZE[r.node_type],\n  symbolRotate: r.node_type === \"terminal\" ? 90 : 0, // right-pointing triangle\n  category: CATEGORY[r.node_type],\n  itemStyle: { color: t.palette[CATEGORY[r.node_type]], opacity: r.pruned ? 0.4 : 1 },\n  label: {\n    show: true,\n    position: \"top\",\n    distance: r.node_type === \"terminal\" ? 12 : 14,\n    formatter: r.node_type === \"terminal\" ? `{name|${r.name}}\\n{val|${money(r.payoff)}}` : `{name|${r.name}}\\n{val|EMV ${money(r.emv)}}`,\n    rich: {\n      name: { color: t.ink, fontSize: 14, fontWeight: 600, lineHeight: 18, align: \"center\" },\n      val: { color: t.inkSoft, fontSize: 12, lineHeight: 16, align: \"center\" },\n    },\n  },\n  record: r,\n}));\n\nconst byId = Object.fromEntries(records.map((r) => [r.node_id, r]));\nconst links = records\n  .filter((r) => r.parent_id !== null)\n  .map((r) => {\n    const branchText = r.probability !== null ? `${r.branch_label}\\np = ${r.probability.toFixed(2)}` : r.branch_label;\n    return {\n      source: r.parent_id,\n      target: r.node_id,\n      lineStyle: {\n        color: r.pruned ? t.inkSoft : t.ink,\n        width: r.pruned ? 2 : 2.5,\n        type: r.pruned ? \"dashed\" : \"solid\",\n        opacity: r.pruned ? 0.45 : 0.85,\n        curveness: 0,\n      },\n      label: {\n        show: true,\n        formatter: branchText,\n        fontSize: 12,\n        color: r.pruned ? t.inkSoft : t.ink,\n        opacity: r.pruned ? 0.6 : 1,\n      },\n    };\n  });\n\n// --- Init ---------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\nchart.on(\"finished\", () => {\n  window.__anyplotReady = true;\n});\n\n// --- Option ---------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"tree-decision · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 24,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  legend: {\n    data: [\"Decision node\", \"Chance node\", \"Terminal node\"],\n    top: 70,\n    left: \"center\",\n    textStyle: { color: t.inkSoft, fontSize: 13 },\n    itemWidth: 16,\n    itemHeight: 16,\n  },\n  graphic: [\n    {\n      type: \"text\",\n      left: 40,\n      bottom: 24,\n      style: {\n        text: \"Dashed, faded branches = rejected alternative (not on the optimal path)\",\n        fill: t.inkSoft,\n        fontSize: 13,\n      },\n    },\n  ],\n  series: [\n    {\n      type: \"graph\",\n      layout: \"none\",\n      coordinateSystem: undefined,\n      roam: false,\n      data: nodes,\n      links: links,\n      categories: [\n        { name: \"Decision node\", itemStyle: { color: t.palette[0] } },\n        { name: \"Chance node\", itemStyle: { color: t.palette[1] } },\n        { name: \"Terminal node\", itemStyle: { color: t.palette[2] } },\n      ],\n      edgeSymbol: [\"none\", \"arrow\"],\n      edgeSymbolSize: [0, 10],\n      label: { show: true },\n      emphasis: { disabled: true },\n    },\n  ],\n});\n"}