{"spec_id":"bar-tornado-sensitivity","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// bar-tornado-sensitivity: Tornado Diagram for Sensitivity Analysis\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst BASE_VALUE = 5.0;\n\n// One-way NPV sensitivity for a renewable energy project ($ millions, base = $5M)\n// Sorted by total range (|high - low|) descending — widest bar at top\nconst parameters = [\n  { label: \"Electricity Price\",   low: 1.2, high: 9.8 },\n  { label: \"Capacity Factor\",     low: 2.3, high: 7.9 },\n  { label: \"Discount Rate\",       low: 7.2, high: 3.1 },\n  { label: \"Construction Cost\",   low: 6.5, high: 2.8 },\n  { label: \"Project Lifetime\",    low: 3.2, high: 6.8 },\n  { label: \"Carbon Credit Price\", low: 3.8, high: 6.4 },\n  { label: \"Operating Cost\",      low: 5.8, high: 3.9 },\n  { label: \"Financing Rate\",      low: 5.4, high: 4.2 },\n  { label: \"Grid Connection Fee\", low: 4.7, high: 5.3 },\n];\n\n// Three-dataset stacked approach: transparent filler + left segment + right segment.\n// Regular (non-floating) stacked bars guarantee correct x-axis positioning.\n// Left segment = green (low scenario effect), right segment = purple (high scenario effect).\n// For inverted parameters (low NPV > base), segments swap sides — handled via per-bar colors.\nconst lowColor  = t.palette[0];  // Imprint green — first series, low scenario\nconst highColor = t.palette[1];  // Imprint purple — high scenario\n\nconst fillerData = parameters.map(p => Math.min(p.low, p.high));\nconst leftData   = parameters.map(p => BASE_VALUE - Math.min(p.low, p.high));\nconst rightData  = parameters.map(p => Math.max(p.low, p.high) - BASE_VALUE);\n\n// Left segment: green if p.low is on the left of base (normal), purple if inverted\nconst leftColors  = parameters.map(p => p.low <= BASE_VALUE ? lowColor : highColor);\n// Right segment: purple if p.high is on the right of base (normal), green if inverted\nconst rightColors = parameters.map(p => p.high >= BASE_VALUE ? highColor : lowColor);\n\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\nconst bgPlugin = {\n  id: \"bg\",\n  beforeDraw({ ctx, width, height }) {\n    ctx.save();\n    ctx.fillStyle = t.pageBg;\n    ctx.fillRect(0, 0, width, height);\n    ctx.restore();\n  },\n};\n\nconst baseLinePlugin = {\n  id: \"baseLine\",\n  afterDraw({ ctx, scales: { x, y } }) {\n    const bx = x.getPixelForValue(BASE_VALUE);\n    ctx.save();\n    ctx.beginPath();\n    ctx.moveTo(bx, y.top);\n    ctx.lineTo(bx, y.bottom);\n    ctx.strokeStyle = t.ink;\n    ctx.lineWidth = 2.5;\n    ctx.setLineDash([8, 5]);\n    ctx.stroke();\n    ctx.setLineDash([]);\n    ctx.font = \"bold 13px sans-serif\";\n    ctx.fillStyle = t.inkSoft;\n    ctx.textAlign = \"center\";\n    ctx.fillText(\"Base: $5M\", bx, y.top - 6);\n    ctx.restore();\n  },\n};\n\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: parameters.map(p => p.label),\n    datasets: [\n      {\n        label: \"_filler\",\n        data: fillerData,\n        backgroundColor: \"transparent\",\n        borderWidth: 0,\n      },\n      {\n        label: \"Low Scenario\",\n        data: leftData,\n        backgroundColor: leftColors,\n        borderWidth: 0,\n      },\n      {\n        label: \"High Scenario\",\n        data: rightData,\n        backgroundColor: rightColors,\n        borderWidth: 0,\n      },\n    ],\n  },\n  options: {\n    indexAxis: \"y\",\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"bar-tornado-sensitivity · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"bold\" },\n        padding: { top: 10, bottom: 18 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          padding: 24,\n          boxWidth: 20,\n          boxHeight: 14,\n          filter: (item) => !item.text.startsWith(\"_\"),\n        },\n      },\n    },\n    scales: {\n      x: {\n        stacked: true,\n        min: 0,\n        max: 11,\n        title: {\n          display: true,\n          text: \"Net Present Value ($ millions)\",\n          color: t.ink,\n          font: { size: 18 },\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (v) => `$${v}M`,\n        },\n        grid: { color: t.grid },\n      },\n      y: {\n        stacked: true,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n        },\n        grid: { display: false },\n      },\n    },\n    layout: {\n      padding: { left: 16, right: 40, top: 20, bottom: 16 },\n    },\n  },\n  plugins: [bgPlugin, baseLinePlugin],\n});\n"}