{"spec_id":"bar-tornado-sensitivity","library":"muix","language":"javascript","code":"// anyplot.ai\n// bar-tornado-sensitivity: Tornado Diagram for Sensitivity Analysis\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-08-20\nimport { BarChart } from \"@mui/x-charts/BarChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// One-way sensitivity analysis of a project's Net Present Value (NPV): each\n// parameter is varied independently between a low and a high scenario while\n// holding all others at the base case, ranked by the widest resulting swing.\nconst BASE_NPV = 2_400_000;\nconst parameters = [\n  \"Revenue Growth Rate\",\n  \"Discount Rate\",\n  \"Terminal Growth Rate\",\n  \"Operating Margin\",\n  \"Exit Multiple\",\n  \"Capital Expenditure\",\n  \"Tax Rate\",\n  \"Working Capital Change\",\n];\nconst lowValues = [1_750_000, 1_900_000, 2_050_000, 2_000_000, 2_100_000, 2_650_000, 2_550_000, 2_300_000];\nconst highValues = [3_150_000, 3_000_000, 2_900_000, 2_750_000, 2_700_000, 2_150_000, 2_200_000, 2_500_000];\n\n// Bars are stacked as offsets from the base case so they diverge from a\n// shared zero point, which lands exactly on the base-case reference line.\nconst lowOffsets = lowValues.map((v) => v - BASE_NPV);\nconst highOffsets = highValues.map((v) => v - BASE_NPV);\n\nconst currencyFormatter = new Intl.NumberFormat(\"en-US\", {\n  style: \"currency\",\n  currency: \"USD\",\n  notation: \"compact\",\n  maximumFractionDigits: 1,\n});\n\nconst TITLE_HEIGHT = 60;\nconst TITLE = \"NPV Sensitivity Analysis · bar-tornado-sensitivity · javascript · muix · anyplot.ai\";\n// Base multiplier raised from the 22px style-guide default so this long\n// descriptive+mandated title still reads with strong visual presence once\n// scaled down by the length ratio (review feedback: title was ~39% of width).\nconst TITLE_FONT_SIZE = Math.round(30 * Math.min(1, 67 / TITLE.length));\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n\n  return (\n    <Box sx={{ width, height, display: \"flex\", flexDirection: \"column\", paddingTop: \"20px\" }}>\n      <Typography\n        sx={{ color: t.ink, fontSize: TITLE_FONT_SIZE, fontWeight: 500, textAlign: \"center\", lineHeight: 1.2 }}\n      >\n        {TITLE}\n      </Typography>\n      <BarChart\n        width={width}\n        height={height - TITLE_HEIGHT}\n        layout=\"horizontal\"\n        skipAnimation\n        barLabel={(item) =>\n          currencyFormatter.format(item.seriesId === \"low\" ? lowValues[item.dataIndex] : highValues[item.dataIndex])\n        }\n        colors={[t.palette[0], t.palette[1]]}\n        borderRadius={4}\n        yAxis={[{ scaleType: \"band\", data: parameters }]}\n        xAxis={[\n          {\n            label: \"Net Present Value\",\n            valueFormatter: (v) => currencyFormatter.format(BASE_NPV + v),\n            // Widen the vertical grid to ~$0.2M increments (was ~$0.1M / ~15\n            // lines) so the background stays subtle and doesn't compete with\n            // the bars.\n            tickMinStep: 200_000,\n          },\n        ]}\n        series={[\n          {\n            id: \"low\",\n            data: lowOffsets,\n            label: \"Low scenario\",\n            stack: \"delta\",\n            valueFormatter: (v, { dataIndex }) => (v == null ? \"\" : currencyFormatter.format(lowValues[dataIndex])),\n          },\n          {\n            id: \"high\",\n            data: highOffsets,\n            label: \"High scenario\",\n            stack: \"delta\",\n            valueFormatter: (v, { dataIndex }) => (v == null ? \"\" : currencyFormatter.format(highValues[dataIndex])),\n          },\n        ]}\n        grid={{ vertical: true }}\n        margin={{ top: 56, right: 48, bottom: 64, left: 220 }}\n        slotProps={{ legend: { position: { vertical: \"top\", horizontal: \"middle\" } } }}\n        sx={{\n          \"& .MuiChartsAxis-tickLabel\": { fontSize: \"14px\" },\n          \"& .MuiChartsAxis-label\": { fontSize: \"16px\" },\n          \"& .MuiBarLabel-root\": { fontSize: \"13px\", fill: t.ink },\n          \"& .MuiChartsLegend-label\": { fontSize: \"14px\" },\n        }}\n      >\n        <ChartsReferenceLine\n          x={0}\n          label={`Base case: ${currencyFormatter.format(BASE_NPV)}`}\n          labelAlign=\"start\"\n          lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"6 4\" }}\n          labelStyle={{ fill: t.ink, fontSize: 14 }}\n        />\n      </BarChart>\n    </Box>\n  );\n}\n"}